'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?
Solution 1:[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):
Here is code:
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
}
}
}
}
Solution 2:[2]
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'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.
Solution 3:[3]
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: "foo") var value: String
// For your view model or SwiftUI View
$value
Your full custom Combine publisher as a @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()
}
}
Solution 4:[4]
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("\(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()
}
}
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Cosyn |
Solution 3 | Roland Lariotte |
Solution 4 | malhal |