Правильно ли ожидать, что внутренние обновления оболочки свойств SwiftUI DynamicProperty будут запускать обновление представления?
Я пытаюсь создать настраиваемую оболочку свойств, поддерживаемую SwiftUI, а это означает, что изменения соответствующих значений свойств вызовут обновление представления SwiftUI. Вот упрощенная версия того, что у меня есть:
@propertyWrapper
public struct Foo: DynamicProperty {
@ObservedObject var observed: SomeObservedObject
public var wrappedValue: [SomeValue] {
return observed.value
}
}
Я вижу это, даже если мой ObservedObject
содержится внутри моей оболочки настраиваемого свойства, SwiftUI все еще улавливает изменения в SomeObservedObject
так долго как:
- Моя оболочка свойств - это структура
- Моя оболочка свойств соответствует
DynamicProperty
К сожалению, документов немного, и мне трудно сказать, работает ли это только при текущей реализации SwiftUI.
Документы DynamicProperty
(в Xcode, а не в сети), похоже, указывает на то, что такое свойство является свойством, которое изменяется извне, вызывая перерисовку представления, но нет никакой гарантии того, что произойдет, когда вы приведете свои собственные типы в соответствие с этим протоколом.
Могу ли я ожидать, что это продолжит работать в будущих выпусках SwiftUI?
4 ответа
Хорошо... вот альтернативный подход для получения аналогичной вещи... но только как структура DynamicProperty
завернутый @State
(для принудительного обновления просмотра).
Это простая оболочка, но дает возможность инкапсулировать любые пользовательские вычисления с последующим обновлением представления... и, как сказано, с использованием типов только для значений.
Вот демо (протестировано с Xcode 11.2 / iOS 13.2):
Вот код:
import SwiftUI
@propertyWrapper
struct Refreshing<Value> : DynamicProperty {
let storage: State<Value>
init(wrappedValue value: Value) {
self.storage = State<Value>(initialValue: value)
}
public var wrappedValue: Value {
get { storage.wrappedValue }
nonmutating set { self.process(newValue) }
}
public var projectedValue: Binding<Value> {
storage.projectedValue
}
private func process(_ value: Value) {
// do some something here or in background queue
DispatchQueue.main.async {
self.storage.wrappedValue = value
}
}
}
struct TestPropertyWrapper: View {
@Refreshing var counter: Int = 1
var body: some View {
VStack {
Text("Value: \(counter)")
Divider()
Button("Increase") {
self.counter += 1
}
}
}
}
struct TestPropertyWrapper_Previews: PreviewProvider {
static var previews: some View {
TestPropertyWrapper()
}
}
На вопрос в заголовке - нет. Например, вот код, который не работает:
class Storage {
var value = 0
}
@propertyWrapper
struct MyState: DynamicProperty {
var storage = Storage()
var wrappedValue: Int {
get { storage.value }
nonmutating set {
storage.value = newValue // This doesn't work
}
}
}
Так что, по-видимому, вам все равно нужно поставить
State
или
ObservedObject
и т.д. внутри вас, чтобы запустить обновление, как это сделал Аспери,
DynamicProperty
сам по себе не требует никаких обновлений.
Вы можете создать и использовать издателя Combine так же, как и
@Published
объект из SwiftUI подойдет.
Передавая издателя своего
@porpertyWrapper
к
projectedValue
, у вас будет пользовательский издатель Combine, который вы можете использовать в своем представлении SwiftUI и вызвать
$
чтобы отслеживать изменения значений с течением времени.
Использование в вашем представлении SwiftUI или внутри модели представления:
@Foo(defaultValue: "foo") var value: String
// For your view model or SwiftUI View
$value
Ваш полноценный пользовательский издатель Combine в качестве
@propertyWrapper
:
import Combine
@propertyWrapper
struct Foo<Value> {
var defaultValue: Value
// Combine publisher to project value over time.
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
return defaultValue
}
set {
defaultValue = newValue
publisher.send(newValue)
}
}
// Project the updated value using the Combine publisher.
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
Да, это правильно, вот пример:
class SomeObservedObject : ObservableObject {
@Published var counter = 0
}
@propertyWrapper struct Foo: DynamicProperty {
@StateObject var object = SomeObservedObject()
public var wrappedValue: Int {
get {
object.counter
}
nonmutating set {
object.counter = newValue
}
}
}
Когда представление с использованием
@Foo
воссоздается, SwiftUI передает структуре Foo тот же объект, что и в прошлый раз, поэтому имеет то же значение счетчика. При установке foo var это устанавливается в
ObservableObject
с
@Published
который SwiftUI обнаруживает как изменение и вызывает пересчет тела.
Попробуйте!
struct ContentView: View {
@State var counter = 0
var body: some View {
VStack {
Text("\(counter) Hello, world!")
Button("Increment") {
counter = counter + 1
}
ContentView2()
}
.padding()
}
}
struct ContentView2: View {
@Foo var foo
var body: some View {
VStack {
Text("\(foo) Hello, world!")
Button("Increment") {
foo = foo + 1
}
}
.padding()
}
}
При нажатии второй кнопки счетчик хранится в
Foo
обновлено. Когда нажата первая кнопка,
ContentView
тело называется и
ContentView2()
воссоздается, но сохраняет тот же счетчик, что и в прошлый раз. В настоящее время
SomeObservedObject
может быть
NSObject
и реализовать
delegate
протокол например.