How to Write custom Property Wrappers with the help of the SwiftUI?
Hitesh Chauhan
Posted on February 22, 2022
Have you any idea how you can wrap your head around Swift's property wrappers? However, I am digging more about the Swift UI and its related work. And found one challenge, which is passing dependencies from SwiftUI's environment into a custom property wrapper. During the research, I learned that the DynamicProperty protocol is the one that confirms your property wrappers too. When your property wrapper complies with the DynamicProperty protocol, it effectively becomes a component of your SwiftUI view. Therefore, it implies that your SwiftUI view will ask your property wrapper to update itself anytime it is about to evaluate your view's body, and your property wrapper will extract data from your SwiftUI environment.
So, to trigger the updates in your views, you can use the @StateObject properties in your property wrapper. Even the iOS developers make changes in the property wrapper during the development. So, before going deep into the topic, let us first discuss what dynamicProperty is.
Understanding the fundamentals of DynamicProperty
It is as simple as conforming a property wrapper to the DynamicProperty protocol to define a dynamic property. Even this protocol's only requirement is to implement an update method that your view calls anytime it is about to evaluate its body. The following is an example of how to define a property wrapper:
@proprtyWrapper
Struct MyPropertyWrapper: DynamicProperty {
Var Wrappedvalue: String
Func update() {
// called whenever the view will evaluate its body
}
}
Of course, this example is not helpful on its own. In a moment, I will show you a more custom property wrapper. Let us take a moment to review what we have defined so far.
We have defined a property wrapper that is quite basic. Further, this wrapper's wrapped value is a string, which implies that it is used in our SwiftUI view as a string.
However, in this post, I have defined the property wrapper as a structure, not as a class. Perhaps, it is allowed to define DynamicProperrtty wrapper as a class too, and it works to some extent only. If I talk about my experience, it delivers very inconsistent results that might not even work. So, not well assured why Swift UI breaks property wrappers that are represented as classes.
SwiftUI has an update method that is called whenever the body is about to evaluate. You can use this function to update the state that resides outside of your property wrapper. Unless you are doing a lot of more intricate work in your property wrapper, you probably do not need to implement this function at all.
Using your dynamic property to trigger view updates
On their own, dynamic properties can't inform a SwiftUI view to update. We may, however, utilize @State, @ObservedObject, @StateObject, and other SwiftUI property wrappers to trigger view updates from within a custom property wrapper using @State, @ObservedObject, @StateObject, and other SwiftUI property wrappers.
It will look like this:
@proprtyWrapper
Struct Custom Property: DynamicProperty {
@State private var value = 0
Var Wrappedvalue: Int {
Get {
Return value
}
Nonmutating set {
Value - newValue
}
}
}
An Int value gets wrapped in this property wrapper. The @State property value is changed whenever this value receives a new value, which causes a view update. The following is an example of how to use this property wrapper in a view:
Struct ContentView: View {
@CustomProperty var customProperty
Var body: some View{
Text(“Count:\(customProperty)”)
Button (“Increment”) {
cutomProperty +=1
}
}
}
When a button is clicked, the value of customProperty gets an update in the SwiftUI view. So, this does not trigger reevaluation of the body on its own. Although, the value that represents our wrapped item is marked with @State, which is our view updates. What is best about this is that our view will update whenever the value changes for whatever reason.
Although a property wrapper like this isn't practical, we can use it to create some pretty cool abstractions around various types of data access. For example, create a UserDefaults abstraction that provides a key path-based version of AppStorage:
class SettingKeys: ObservableObject {
@AppStorage ("onboardingCompleted™) var onboardingCompleted = false
@AppStorage ("promptedForProVersion") var promptedForProVersion = false
}
@propertyWrapper
struct Setting<T>: DynamicProperty {
@StateObject private var keys = SettingKeys()
private let key: ReferenceWritableKeyPath‹SettingKeys,T>
var wrappedValue: T{
get {
keys keyPath: key]
}
nonmutating set {
keys[keyPath: key] = newValue
}
}
init(_ key: ReferenceWritableKeyPath‹SettingKeys, T>) {
Self.key = key
}
}
When you use this property, it will look like this:
struct ContentView: View {
@Setting(\-onboardingCompleted) var didonboard
var body: some View {
Text("Onboarding completed: \ (didOnboard ? "Yes" : "No"') "')
Button("Complete onboarding"){
didonboard = true
}
}
}
Any place in the app that uses @Setting(.onboardingCompleted) var didOnboard will automatically update when the value for onboarding completed in user defaults updated, regardless of where / how this happened. So, it is the same as how @AppStorage works. In fact, my custom property wrapper relies heavily on @AppStorage under the hood.
My SettingsKeys object has all of the individual keys in UserDefaults that I want to write to. And the @AppStorage property wrapper provides for easy observation and lets me specify SettingsKeys as an ObservableObject without any problems.
I can easily read values from user defaults or create a new value by assigning them to the proper key path in SettingsKeys by creating a custom get and set on my Setting's wrappedValue.
Simple property wrappers like this are helpful when you want to expedite some of your data access or if you want to give your property wrapper some semantic meaning and make it easier to discover.
Use SwiftUI environment values in the property wrapper.
It is possible to access the SwiftUI environment for the view that your property wrapper is used in, in addition to triggering view updates using some of SwiftUI's property wrappers. However, it is especially beneficial if your property wrapper is more complicated and relies on things like a managed object context, a networking object, or other similar objects.
The SwiftUI environment can be accessed from within your property wrapper in the same manner as you do it in your views:
@propertyWrapper
struct CustomFetcher<T>: DynamicProperty {
@Environment (\.managedobjectContext) var managedobjectContext
// …
}
Alternatively, you can use the .environmentObject view modifier to read environment objects that are allotted through your view:
@propertyWrapper
struct UsesEnvironmentObject<T: ObservableObject›: DynamicProperty {
@Environmentobject var envObject:T
// …
}
Moreover, you can pass dependencies to your property wrappers through the environment in a more convenient way. Let us imagine you are creating a property wrapper that retrieves data from the internet. You may have a Networking object that can make network calls to get the data you need. You could use the environment to inject this object into the property wrapper:
@propertyWrapper
struct FeedLoader‹Feed: FeedType›: DynamicProperty {
@Environment (\.network) var network
var wrappedvalue: [Feed.ObjectType] = []
}
I added the network environment key to my SwiftUI environment as a custom key. We need the means to collect data from the network and assign it to anything in order to update our wrapped value now that we've defined this property wrapper. To do this, you can hire iOS developer, who will use the update method, which allows us to update data that our property wrapper references if necessary.
Sum up
We discussed a few helpful property wrappers in today's article and observed how they are limited in terms of composability and testability. And to address the complex use case of publishing a value and storing it in an abstract Storage, we created a custom property wrapper.
Posted on February 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.