Комментарий и предупреждение
По сути, этот вопрос составляет в поисках замены для didSet
в операциях var seedDate
.
Я использовал один из своих запросы поддержки с Apple по этому же вопросу несколько месяцев go. Последний ответ от них состоял в том, что они получили несколько таких вопросов, но у них пока нет «хорошего» решения. Я поделился решением, приведенным ниже, и они ответили: «Поскольку это работает, используйте его».
То, что следует ниже, довольно «вонючее», но оно работает. Надеемся, мы увидим улучшения в iOS 14, устраняющие необходимость чего-то подобного.
Концепция
Мы можем воспользоваться тем, что body
является единственной точкой входа для просмотр рендеринга. Таким образом, мы можем отслеживать изменения входов нашего представления с течением времени и изменять внутреннее состояние, основываясь на этом. Мы просто должны быть осторожны с тем, как мы обновляем вещи, чтобы идея SwiftUI о State
не изменялась неправильно.
Мы можем сделать это, используя struct
, который содержит два ссылочных значения:
- Значение, которое мы хотим отслеживать
- Значение, которое мы хотим изменить при изменении # 1
Если мы хотим, чтобы SwiftUI обновлялся, мы заменяем эталонное значение. Если мы хотим обновить на основе изменений # 1 внутри тела, мы обновляем значение , удерживаемое опорным значением.
Реализация
Суть здесь
Сначала мы хотим обернуть любое значение в ссылочный тип. Это позволяет нам сохранять значение без запуска механизмов обновления SwiftUI.
// A class that lets us wrap any value in a reference type
class ValueHolder<Value> {
init(_ value: Value) { self.value = value }
var value: Value
}
Теперь, если мы объявим @State var valueHolder = ValueHolder(0)
, мы можем сделать:
Button("Tap me") {
self.valueHolder.value = 0 // **Doesn't** trigger SwiftUI update
self.valueHolder = ValueHolder(0) // **Does** trigger SwiftUI update
}
Во-вторых, создайте оболочку свойства, которая содержит два из них, один для нашего внешнего входного значения и один для нашего внутреннего состояния.
См. этот ответ для объяснения того, почему я использую State
в оболочке свойства.
// A property wrapper that holds a tracked value, and a value we'd like to update when that value changes.
@propertyWrapper
struct TrackedValue<Tracked, Value>: DynamicProperty {
var trackedHolder: State<ValueHolder<Tracked>>
var valueHolder: State<ValueHolder<Value>>
init(wrappedValue value: Value, tracked: Tracked) {
self.trackedHolder = State(initialValue: ValueHolder(tracked))
self.valueHolder = State(initialValue: ValueHolder(value))
}
var wrappedValue: Value {
get { self.valueHolder.wrappedValue.value }
nonmutating set { self.valueHolder.wrappedValue = ValueHolder(newValue) }
}
var projectedValue: Self { return self }
}
И, наконец, добавьте удобный метод, который позволит нам эффективно обновлять информацию по мере необходимости. Так как это возвращает View
, вы можете использовать его внутри любого ViewBuilder
.
extension TrackedValue {
@discardableResult
public func update(tracked: Tracked, with block:(Tracked, Value) -> Value) -> some View {
self.valueHolder.wrappedValue.value = block(self.trackedHolder.wrappedValue.value, self.valueHolder.wrappedValue.value)
self.trackedHolder.wrappedValue.value = tracked
return EmptyView()
}
}
Использование
Если вы запустите приведенный ниже код, childCount
будет сбрасываться в 0 каждый раз masterCount
изменения.
struct ContentView: View {
@State var count: Int = 0
var body: some View {
VStack {
Button("Master Count: \(self.count)") {
self.count += 1
}
ChildView(masterCount: self.count)
}
}
}
struct ChildView: View {
var masterCount: Int
@TrackedValue(tracked: 0) var childCount: Int = 0
var body: some View {
self.$childCount.update(tracked: self.masterCount) { (old, myCount) -> Int in
if self.masterCount != old {
return 0
}
return myCount
}
return Button("Child Count: \(self.childCount)") {
self.childCount += 1
}
}
}