DynamicProperty
iOS SwiftUI propertyWrapper Estimated reading time: 5 minutesWith SwiftUI
we are already faced with a bunch of specially designed @propertyWrappers
. But, the number of developers who use SwiftUI
constantly increasing, the problems that they are trying to solve also increasing quite fast. As result, we would like to combine different functions and provide our specific wrapper that should also trigger an update of View
in SwiftUI
world.
Example may be analog to
@FetchRequest
but for photos or fetch request but for a custom database such asFirebase
. Or simply for some internal logic that wraps some aspect into compact representation.
To do this - we may inspect existing triggers (@propertyWrappers
) for View
updates. All of them conform to one protocol - DynamicProperty
.
DynamicProperty
If we check documentation there is not too much - An interface for a stored variable that updates an external property of a view. At least we can get an idea - if adopt this protocol, a view should react to any changes when property receives updates (changes).
If we go inside the header and check requirements, we can find, that:
The only requirements - func update()
. As we can see from both header and official doc - update
is already implemented for us.
from doc Required. Default implementation provided.
Looks like everything simple - we create our own @propertyWrapper
, conform to DynamicProperty
protocol, and View
will respect any changes in it and display appropriate changes on the screen for us. Pretty simple, let’s try it.
Note here - I’m talking about
@propertyWrapper
andDynamicProperty
, but actuallyDynamicProperty
hasn’t any limitation, and in theory, anything can adopt it.Both will compile successfully:
Example
We can test DynamicProperty
by creating our custom @propertyWrapper
that simply stores some Value and as result, any change should be reflected in View
(something similar to @State
).
View for test purpose will show some text and action that will update our value. In the same moment text appearance depends on value:
update
should be called automatically by the SwiftUI
engine. But, when we launch the app and press the button - nothing happens.
Why? Here we have a few moments:
- how
SwiftUI
determine that something is changed? - how the
SwiftUI
call theupdate
function (as for struct it doesn’t allow you to call a mutating function)?
The first question is very interesting. As u can see from the example above, we got no success in making workable DynamicProperty
. But, as soon as we can change a bit implementation, by injecting usage of existing SwiftUI
any DynamicProperty
as a kind of storage for our value, it works as expected:
The answer to question number 1 (how SwiftUI
determine that something is changed?) maybe next: SwiftUI
introspect children in type and check values for change events using some internal mechanism.
A bit more deep dive in how difference is determined by inspecting
@State
is available here
And as a sample we may refer to discussion on Swift forum related to this topic and custom implementation of update
function:
The same idea used for this approach - check the type and update the view.
But still, we have unanswered question number 2 (how the SwiftUI
calls the update
function?). On the same thread on the Swift forum we may found a few suggestions on how it can be done, and as mentioned by Helge_Hess1:
To make the View’s value “mutable” one just casts the pointer to a mutable one. Which explains why this works even if the properties are defined as let. Not sure whether that is sound (optimizer might layout the properties differently?), but I guess the layout should be stable as part of the “ABI”.
Now, it’s become more clear - why only SwiftUI
DynamicProperties
(an initial name DynamicViewProperty
) works in such way: all data stored in some external memory and all changes tracked by some internal mechanism, change is done in swift runtime. A bit complex idea, but the result is great (in my opinion).
Update
19.04.2022
Thanks to comment from @Chris Eidhof -
StoredValue
implementation you should be using aStateObject
instead ofObservedObject
.
With usage of @ObservedObject
property storage will be shared (in our case counter), with @StateObject
- independen.
Here is a small test code:
this also require update init of
@StoredValue
with_storage = StateObject(wrappedValue: Storage(value))
Result:
Resources
Share on: