Я пытаюсь понять, когда SwiftUI на самом деле вызывает перерисовку представлений с использованием зависимостей @Observable/property и случайных цветов фона, чтобы просто получить визуальное представление о вещах.
Я прочитал https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app - и насколько я понимаю, представления, которые используют свойства объектов @Observable, имеют механизм макросов в место для обновления только в случае изменения свойства. Другие представления в дереве не должны обновляться, если их свойство не обновилось.
Из документов Apple:
Вы также можете поделиться объектом данных наблюдаемой модели с другим представлением. Принимающее представление формирует зависимость, если оно считывает какие-либо свойства объект в своем теле. Например, в следующем коде LibraryView использует экземпляр Book совместно с BookView и BookView. отображает название книги. Если название книги изменится, SwiftUI обновляет только BookView, а не LibraryView, потому что только BookView читает свойство title.
Но я не могу добиться единообразия для всех типов представлений?
В приведенном ниже коде я добавил несколько статических представлений, которые случайным образом рисуют цвет фона, и несколько «встроенных» представлений, которые также случайным образом рисуют фон. У меня есть простая ручная анимация на основе таймера, которая просто обновляет наблюдаемое значение свойства в качестве «прокси» для изменений, чтобы проверить отслеживание наблюдений/зависимостей.
Вот ссылка на видео предварительного просмотра Xcode, показывающее то, что я вижу:
https://x.com/_vade/status/1828116344916844806?s=61
Что я ожидал увидеть:
Самый верхний «встроенный» созданный текст не должен менять цвет переднего плана — он не зависит от наблюдаемого свойства. На самом деле оно меняется. Почему?
Аналогично, представление StaticText под ним не должно менять цвет переднего плана. Это не так, и работает так, как ожидалось. Почему это отличается от представленного выше?
Я ожидаю, что кнопка StaticView Start Clock не изменит свой фон. Это не так, я полагаю, по той же причине, что и приведенный выше StaticText не меняется. Тем не менее, представление взаимодействует с часами, но не с основным телом рисования. Ура.
Аналогично, StaticView ниже не меняется должным образом. Ура.
Я ожидаю, что SimpleSliderView будет обновляться, поскольку он напрямую привязан к часам и наблюдает за свойством, которое обновляется на основе таймера.
Я не ожидаю обновления внешнего цвета фона ZStacks, хотя я могу быть убежден, что, поскольку необходимо перерисовать дочернее представление, возможно, потребуется перерисовать его.
В чем причина того, что весь ZStack пересчитывает свой фон, а некоторые его дочерние элементы - нет?
Если вышеизложенное является «правильным» поведением, может ли любое встроенное родительское представление, используемое для композиции в SwiftUI (например, HStack/VStack/ZStack), быть написано таким образом, чтобы перерисовка подпредставлений не вызывала перерисовку родителя?
С точки зрения графики я хочу ограничить любую скорость перерисовки/заполнения и обеспечить минимальное количество обновлений за каждое наблюдаемое обновление через наблюдение.
Я тестирую бета-версию macOS 15.1 Beta 2 и хочу полностью использовать любые новые возможности повышения производительности SwiftUI, какие только смогу. Я не ориентируюсь на старые версии. ЙОЛО.
Спасибо за любую информацию! Кажется, очень легко создать плохо работающий код SwiftUI.
Тестовый код:
import SwiftUI
public func remap(input:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double
{
return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin;
}
public func remap(input:Float, inMin:Float, inMax:Float, outMin:Float, outMax:Float) -> Float
{
return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin;
}
@Observable class BPMClock : Identifiable, Hashable, Equatable
{
var id: UUID = .init()
static func == (lhs: BPMClock, rhs: BPMClock) -> Bool
{
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher)
{
hasher.combine(self.id)
}
var bpm:Double = 60.0
var timeValue:Double = 0.0
@ObservationIgnored private var rawTimeValue:Double = 0.0
@ObservationIgnored private var lastTime:Double = 0.0
@ObservationIgnored var timer:Timer? = nil
func startTimer()
{
self.timer = Timer.scheduledTimer(timeInterval: 1/60,
target: self,
selector: #selector(calcTimeAbsolute),
userInfo: nil,
repeats: true)
}
@objc func calcTimeAbsolute()
{
let now = Date.timeIntervalSinceReferenceDate
self.calcTime(usingNow: now)
}
func calcTime(usingNow now:Double)
{
let delta = now - self.lastTime
self.rawTimeValue = fmod(delta + self.rawTimeValue, self.bpmToS(self.bpm) )
self.timeValue = remap(input: self.rawTimeValue, inMin: 0.0, inMax:self.bpmToS(self.bpm), outMin: 0.0, outMax: 1.0)
self.lastTime = now
}
private func bpmToS(_ bpm:Double) -> Double
{
let bps = 60.0 / bpm
return bps
}
private func sToBpm(_ s:Double) -> Double
{
let bpm = s * 60.0
return bpm
}
}
struct SimpleSliderView : View
{
var value: Double
// @Binding var value:Double
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View
{
ZStack
{
colors.randomElement()!
Rectangle().fill(Color.red)
.frame(width: 200 * value)
Text("This should obviously redraw")
}
.frame(width:200, height: 100 )
}
}
struct StaticView: View {
let text:String
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
ZStack {
colors.randomElement()!
Text(text)
}
}
}
struct StaticText: View
{
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
Text("Why does the background Redraw?")
.foregroundStyle(colors.randomElement()!)
}
}
struct ContentView: View {
let clock = BPMClock()
// @State var clock = BPMClock()
// @Bindable var clock = BPMClock()
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
ZStack
{
colors.randomElement()!
VStack {
Text("Why do I change color?")
.foregroundStyle(colors.randomElement()!)
StaticText()
StaticView(text: "Start Clock \n (this does not redraw)")
.frame(width: 100, height: 100)
.onTapGesture {
self.clock.startTimer()
}
StaticView(text: "I should not redraw")
.frame(width: 100, height: 100)
SimpleSliderView(value: clock.timeValue)
.frame(height: 100)
}
.frame(width: 400, height: 400)
}
}
}
#Preview {
ContentView()
}
К вашему сведению, SwiftUI не рисует, это просто структуры данных, которые различаются, и только если есть разница, что-то рисуется (и на самом деле рисование выполняют объекты UIKit)
@malhal - не имеет значения, какая базовая структура выполняет рисунок. AppKit/UIKit может выполнять рисование по трафарету внутри подпунктов представления (грязные прямоугольники) - дело в том, что платформа SwiftUI заставляет отрисовывать представления, которые не должны быть такими, что, насколько я понимаю, противоречит документации.
@loremipsum Никакой разницы. Проблема, насколько я могу судить, заключается не в наблюдаемом объекте (в данном случае в часах), а в том, что основное представление контента, имеющее дочерние представления, имеющие зависимости от наблюдения, иногда запускает рисование в других несвязанных дочерних элементах. Кроме того, проблема не в анимации, а в отображении сетевых данных, потоковой передаче данных и данных датчиков. Все это вызовет ненужное рисование вида с вышеуказанной настройкой. Я пытаюсь найти, как избежать этого случая.
@vade, я пытаюсь объяснить, что не имеет значения, сколько раз объявляется модель Color View, она рисуется только в том случае, если цвет отличается. При инициализации структур модели View практически отсутствуют накладные расходы, это похоже на инициализацию Int. Если вы хотите повысить производительность, не инициализируйте объекты внутри структур View таким образом let clock = BPMClock() это утечка памяти.
Дело не в том, сколько раз «это объявляется» — я пытаюсь подчеркнуть, что вся структура представления перестраивается/переинициализируется. Это единственная причина, по которой цвет меняется. Я не инициализирую подобные модели, учитывая реальную кодовую базу. Я просто изолировал некоторое поведение, при котором представления перерисовываются без необходимости в соответствии с документацией SwiftUI.
Я думаю, дело здесь в том, что два представления с одинаковым контентом ведут себя по-разному. Один определен встроенно, другой нет. Один обновляется, когда другое дочернее представление в другом поддереве получает наблюдаемое изменение свойства. другой нет. Я хочу знать, почему.





Правильная формулировка стека представлений приведена ниже.
Решение состоит в том, что текущее представление фактически обращается к динамически изменяющемуся свойству ( clock.timeValue) при передаче инициализатору подпредставления. Это не «изолировано», его видит макрос @Observable, и весь ContentView помечен как «грязный».
Решение состоит в том, чтобы StaticSlider принимал в качестве экземпляра часы, а не двойное значение.
Фу
Вот слегка очищенное решение, работающее как положено.
import SwiftUI
public func remap(input:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double
{
return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin;
}
public func remap(input:Float, inMin:Float, inMax:Float, outMin:Float, outMax:Float) -> Float
{
return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin;
}
@Observable class BPMClock
{
var bpm:Double = 60.0
var timeValue:Double = 0.0
@ObservationIgnored private var rawTimeValue:Double = 0.0
@ObservationIgnored private var lastTime:Double = 0.0
@ObservationIgnored var timer:Timer? = nil
func startTimer()
{
self.timer = Timer.scheduledTimer(timeInterval: 1/60,
target: self,
selector: #selector(calcTimeAbsolute),
userInfo: nil,
repeats: true)
}
@objc func calcTimeAbsolute()
{
let now = Date.timeIntervalSinceReferenceDate
self.calcTime(usingNow: now)
}
func calcTime(usingNow now:Double)
{
let delta = now - self.lastTime
self.rawTimeValue = fmod(delta + self.rawTimeValue, self.bpmToS(self.bpm) )
self.timeValue = remap(input: self.rawTimeValue, inMin: 0.0, inMax:self.bpmToS(self.bpm), outMin: 0.0, outMax: 1.0)
self.lastTime = now
}
private func bpmToS(_ bpm:Double) -> Double
{
let bps = 60.0 / bpm
return bps
}
private func sToBpm(_ s:Double) -> Double
{
let bpm = s * 60.0
return bpm
}
}
struct SimpleSliderView : View
{
@State var clock: BPMClock
// @Binding var value:Double
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View
{
ZStack
{
colors.randomElement()!
Rectangle().fill(Color.red)
.frame(width: 200 * clock.timeValue)
Text("This should obviously redraw")
}
.frame(width:200, height: 100 )
}
}
struct StaticView: View {
let text:String
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
ZStack {
colors.randomElement()!
Text(text)
}
}
}
struct StaticText: View
{
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
Text("Why do I not change color?")
.foregroundStyle(colors.randomElement()!)
}
}
// Doesnt matter if @State or @Bindable, local or global var / let
struct ContentView: View {
@State var clock:BPMClock
private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple]
var body: some View {
VStack {
ZStack {
colors.randomElement()!
Text("Why do I change color?")
}
Text("Why do I change color?")
.foregroundStyle(colors.randomElement()!)
StaticText()
StaticView(text: "Start Clock \n (this does not redraw)")
.frame(width: 100, height: 100)
.onTapGesture {
clock.startTimer()
}
StaticView(text: "I should not redraw")
.frame(width: 100, height: 100)
SimpleSliderView(clock: clock)
.frame(height: 100)
}
// uncomment me for even more epillepsy
// .background( colors.randomElement()! )
.frame(width: 400, height: 400)
}
}
#Preview {
let bpmClock = BPMClock()
ContentView(clock: bpmClock)
}
Попробуйте избавиться от хэшируемых и равнозначных реализаций. Это всегда вызывает проблемы со SwiftUI. Также лучше использовать TimelineView вместо Timer.