Правильно ли ожидать, что внутренние обновления оболочки свойств 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):

DynamicProperty как оболочка в @State

Вот код:

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 протокол например.

Другие вопросы по тегам