预期 SwiftUI DynamicProperty 属性包装器的内部更新会触发视图更新是否正确?

huangapple go评论88阅读模式
英文:

Is it correct to expect internal updates of a SwiftUI DynamicProperty property wrapper to trigger a view update?

问题

I'm attempting to create a custom property wrapper supported by SwiftUI, meaning that changes to the corresponding properties values would cause an update to the SwiftUI view. Here is a simplified version of what I have:

@propertyWrapper
public struct Foo: DynamicProperty {
    @ObservedObject var observed: SomeObservedObject
    
    public var wrappedValue: [SomeValue] {
        return observed.value
    }
}

I see that even if my ObservedObject is contained inside of my custom property wrapper, SwiftUI still catches the changes to SomeObservedObject as long as:

  • My property wrapper is a struct
  • My property wrapper conforms to DynamicProperty

Unfortunately the docs are sparse and I have a hard time telling if this only works out of luck with the current SwiftUI implementation.

The docs of DynamicProperty (within Xcode, not online) seem to indicate that such a property is a property that is changed from the outside causing the view to redraw, but there is no guarantee about what happens when you conform your own types to this protocol.

Can I expect this to continue working in future SwiftUI releases?

英文:

I'm attempting to create a custom property wrapper supported by SwiftUI, meaning that changes to the corresponding properties values would cause an update to the SwiftUI view. Here is a simplified version of what I have:

@propertyWrapper
public struct Foo: DynamicProperty {
    @ObservedObject var observed: SomeObservedObject
    
    public var wrappedValue: [SomeValue] {
        return observed.value
    }
}

I see that even if my ObservedObject is contained inside of my custom property wrapper, SwiftUI still catches the changes to SomeObservedObject as long as:

  • My property wrapper is a struct
  • My property wrapper conforms to DynamicProperty

Unfortunately the docs are sparse and I have a hard time telling if this only works out of luck with the current SwiftUI implementation.

The docs of DynamicProperty (within Xcode, not online) seem to indicate that such a property is a property that is changed from the outside causing the view to redraw, but there is no guarantee about what happens when you conform your own types to this protocol.

Can I expect this to continue working in future SwiftUI releases?

答案1

得分: 17

以下是您提供的代码的翻译部分:

好的... 这是一个获取类似功能的替代方法... 但只有结构体 `DynamicProperty` 包装了 `@State`(以强制视图刷新)。

这是一个简单的包装器,但可以封装任何自定义计算,并在视图刷新后使用值类型。

这是演示(已在 Xcode 11.2 / iOS 13.2 中测试):

![DynamicProperty 作为 @State 的包装器][1]

[1]: https://i.stack.imgur.com/nfhTC.gif

以下是代码:

```swift
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) {
        // 在这里执行一些操作或在后台队列中执行
        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
            }
        }
    }
}

请注意,代码中包含一些特殊字符(如 <>),在翻译时保留了原样。如果需要进一步的帮助或解释,请告诉我。

英文:

Ok... here is alternate approach to get similar thing... but as struct only DynamicProperty wrapped around @State (to force view refresh).

It is simple wrapper but gives possibility to incapsulate any custom calculations with following view refresh... and as said using value-only types.

Here is demo (tested with Xcode 11.2 / iOS 13.2):

预期 SwiftUI DynamicProperty 属性包装器的内部更新会触发视图更新是否正确?

Here is code:

import SwiftUI

@propertyWrapper
struct Refreshing&lt;Value&gt; : DynamicProperty {
    let storage: State&lt;Value&gt;

    init(wrappedValue value: Value) {
        self.storage = State&lt;Value&gt;(initialValue: value)
    }
    
    public var wrappedValue: Value {
        get { storage.wrappedValue }
        
        nonmutating set { self.process(newValue) }
    }
    
    public var projectedValue: Binding&lt;Value&gt; {
        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(&quot;Value: \(counter)&quot;)
            Divider()
            Button(&quot;Increase&quot;) {
                self.counter += 1
            }
        }
    }
}

答案2

得分: 1

对于标题中的问题,答案是否定的。例如,这里有一些不起作用的代码:

class Storage {
    var value = 0
}

@propertyWrapper
struct MyState: DynamicProperty {
    var storage = Storage()

    var wrappedValue: Int {
        get { storage.value }

        nonmutating set {
            storage.value = newValue // 这部分不起作用
        }
    }
}

所以显然,你仍然需要在你的DynamicProperty内部放置一个StateObservedObject等来触发更新,就像Asperi所做的一样,DynamicProperty本身不会强制进行任何更新。

英文:

For the question in your title, no. For example, here is some code that doesn't work:

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&#39;t work
        }
    }
}

So apparently you still need to put a State or ObservedObject etc. inside your DynamicProperty to trigger an update as Asperi did, a DynamicProperty itself doesn't enforce any update.

答案3

得分: 1

你可以创建并使用一个与 SwiftUI 中的 @Published 对象相同的 Combine 发布者,用于你的 @propertyWrapper

通过将你的 @propertyWrapper 的发布者传递给 projectedValue,你将拥有一个自定义的 Combine 发布者,可以在 SwiftUI 视图中使用,并调用 $ 来跟踪值随时间的变化。

在你的 SwiftUI 视图或视图模型中的使用:

@Foo(defaultValue: "foo") var value: String

// 对于你的视图模型或 SwiftUI 视图
$value

你的完整自定义 Combine 发布者作为 @propertyWrapper

import Combine

@propertyWrapper
struct Foo<Value> {

  var defaultValue: Value

  // 使用 Combine 发布者来随时间投射值。
  private let publisher = PassthroughSubject<Value, Never>()

  var wrappedValue: Value {
    get {
      return defaultValue
    }
    set {
      defaultValue = newValue
      publisher.send(newValue)
    }
  }

  // 使用 Combine 发布者来投射更新的值。
  var projectedValue: AnyPublisher<Value, Never> {
    publisher.eraseToAnyPublisher()
  }
}
英文:

You can create and use a Combine publisher with your @propertyWrapper the same way a @Published object from SwiftUI would do.

By passing the publisher of your @porpertyWrapper to a projectedValue, you will have a custom Combine publisher that you can use within your SwiftUI view and call the $ to keep track of the value changes over time.

The use in your SwiftUI view or inside a view model:

@Foo(defaultValue: &quot;foo&quot;) var value: String

// For your view model or SwiftUI View
$value

Your full custom Combine publisher as a @propertyWrapper:

import Combine

@propertyWrapper
struct Foo&lt;Value&gt; {

  var defaultValue: Value

  // Combine publisher to project value over time. 
  private let publisher = PassthroughSubject&lt;Value, Never&gt;()

  var wrappedValue: Value {
    get {
      return defaultValue
    }
    set {
      defaultValue = newValue
      publisher.send(newValue)
    }
  }

  // Project the updated value using the Combine publisher.
  var projectedValue: AnyPublisher&lt;Value, Never&gt; {
    publisher.eraseToAnyPublisher()
  }
}

答案4

得分: 0

是的,这是正确的,这里有一个示例:

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变量时,这将设置在ObservableObject的@Published属性上,SwiftUI检测到这是一个更改,并导致重新计算body。

尝试一下!

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的body,并且ContentView2()会被重新创建,但保留与上次相同的计数器。现在,SomeObservedObject可以是NSObject并实现一个delegate协议,例如。

英文:

Yes this is correct, here is an example:

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
        }
    }
}

When a View using @Foo is recreated, SwiftUI passes the Foo struct the same object as last time, so has the same counter value. When setting the foo var, this is set on the ObservableObject's @Published which SwiftUI detects as a change and causes the body to be recomputed.

Try it out!

struct ContentView: View {
    @State var counter = 0
    var body: some View {
        VStack {
            Text(&quot;\(counter) Hello, world!&quot;)
            Button(&quot;Increment&quot;) {
                counter = counter + 1
            }
            ContentView2()
        }
        .padding()
    }
}

struct ContentView2: View {
    @Foo var foo
    
    var body: some View {
        VStack {
            Text(&quot;\(foo) Hello, world!&quot;)
            Button(&quot;Increment&quot;) {
                foo = foo + 1
            }
        }
        .padding()
    }
}

When the second button is tapped the counter stored in Foo is updated. When first button is tapped, ContentView's body is called and ContentView2() is recreated but keeps the same counter as last time. Now SomeObservedObject can be a NSObject and implement a delegate protocol for example.

huangapple
  • 本文由 发表于 2020年1月3日 22:14:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/59580065.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定