Property Wrapper - Simplified!
Introduction:
There have been just so much fuzz around the Property Wrappers. Almost all the recent addition(@State, @Published, @Binding, @BindableObject, @ObservedObject, @EnvironmentObject, @Environment) to the SwiftUI framework is just based upon this single feature that was introduced in Swift 5.1 called ‘Property Wrappers’
There are just so many articles already written on Property Wrappers and some of them are just really great. I’m writing this piece not because I also wanted to write about it and join the bandwagon but because not everyone understands a topic written in a certain way.
Just a different mix of words or with a different set of contextual examples, basically the point is that not everyone likes Vanilla ice cream and the majority of people prefer flavors, so please don’t consider this article as a redundant one and do give it a chance to take it up as a different flavor. Btw, mine is chocolate, what’s yours? ;)
Background:
From Apple’s engineers’ own words, it looks like they realized the power of this feature only after they started using it and it was interesting to see how they started with a replacement of lazy initialized computed property and then built upon it the other layers and what we get to know them today as Property Wrappers(Initially proposed as Property Delegates, not a bad name though).
Let’s see what problem it solves.
I’m pretty sure that as a developer, we all would’ve definitely come across this problem at least once in our lifetime.
We use properties on classes or structs to store our data and we have requirements to process the data stored in the properties, immediately whenever the data in the property is changed/manipulated/modified. There are just tons of examples for this and the most popular one circulating around with property wrappers are UserDefault property wrappers mostly because it is a straight-forward use case that’s required/used by most apps and partly because Apple’s engineers demo’ed property wrapper in WWDC 2019 just using this example.
So, let us take another use case today(A different flavor) and see how we can apply the property wrapper to it.
Example:
For our example, imagine a scenario, let’s say we have a form in the app that’s not supposed to be used by kids, which means that we need to get the age of the user to verify if he/she’s an adult.
struct User {
var age: Int
}
A struct that represents the object/data of the User
A property ‘age’ that takes in an Int value
Now, our goal is to get notified whenever the ‘age’ property is modified, so that we can instantly check if the user is an adult or not.
For that, we already have a few mechanisms like using property observers ‘willSet’/’didSet’ but they have their own drawbacks like not able to manipulate/mutate the property value or not being called during the initial value being set respectively. Although this is not directly/really applicable to our example, this is just a note here.
With our example, ideally, we’ll have an extension on Int or a boolean variable inside the struct that’d hold the data/state of the result derived from the validation we perform on the ‘age’ property, the ideal solution with our existing API’s would look something like this:
struct User {
var age: Int
static func userConfigWithoutWrapper() {
var s1 = User(age: 10)
print("isAdult: \(s1.age.isAdult)")//prints 'false'
s1.age = 20
print("isAdult: \(s1.age.isAdult)")//prints 'true'
}
}
extension Int {
var isAdult: Bool {
get {
if self > 18 {
return true
}
else {
return false
}
}
}
}
But, the problem with this approach is that it is not specific to this problem aka this validation is not bound to this specific functionality, or to put it in other words, the ‘isAdult’ property is exposed to all the Int types which are unnecessary overhead.
Now, let’s see how we can do this with a property wrapper which bounds the functionality and also encapsulates things.
Enter Property Wrapper:
To create a property wrapper, all we’ve to do is to annotate the struct with ‘@propertyWrapper’ and all it asks for is to have a ‘wrappedValue’ to be implemented, so let’s declare our wrapper as ‘Adult’ and this is how it’d look like:
@propertyWrapper struct Adult<Value> {
var defaultValue: Value //1
var isAdult: Bool = false //2
var wrappedValue: Value { //3
get {
return false as? Value ?? defaultValue
}
set {
if newValue as? Int ?? 0 > 18 {
isAdult = true
}
else {
isAdult = false
}
}
}
}
Value is the type that represents Int in our case, ideally, it could be generic so that any type can be used but let’s have Value for our example here.
1.) Notice how we have a defaultValue which is required when dealing with nonoptional wrapper but there are ways to have optional property wrapper as well, but we’ll ignore it in this article for the sake of simplicity
2.) ‘isAdult’ is the internal property that holds the result of our validation
3.) ‘wrappedValue’, this is where everything happens, all it gets to have is a getter and setter if required, and notice how we do the validation and store the result in the ‘isAdult’
And finally, this is how we get to use our property wrapper for the age property in the User struct:
struct User {
@Adult(defaultValue: 0) var age: Int //1
static func userConfigWithWrapper() {
var s1 = User()
print("isAdult: \(s1.age)") //prints '0' //2
s1.age = 10 //3
print("isAdult: \(s1._age.isAdult)") //prints 'false' //4
print("isAdult: \(s1._age)") //prints 'Adult(defaultValue: 0, isAdult: false)' //5
s1.age = 20 //6
print("isAdult: \(s1._age.isAdult)") //prints 'true' //7
print("isAdult: \(s1._age)") //prints 'Adult(defaultValue: 0, isAdult: true)'
}
}
- @Adult is our property wrapper and annotating a property with it binds it with our wrapping functionality and notice how the defaultValue is being passed in the initializer which is required when dealing with non-optional
- We create a User object and print the age property
- We modify the value of the age property to a non-adult age
- We print the ‘isAdult’ property that’s placed inside the wrapper and hence we prefix it with the underscore, but we shouldn’t be doing this, I’ll be telling you why in a bit
- We print the wrapper itself, which displays all the properties and its values
- We modify the value of the age property again to an adult age
- We print the value and then the wrapper itself, which displays all the properties and its values
projectedValue:
Now, we have created a property wrapper and have used it to validate if the user is an adult or not using the same. One thing we did wrong here was accessing the ‘_age’ property directly which is placed inside the wrapper which isn’t the ideal way to do it. Just to avoid this and overcome any issues that could arise out of this, we can use ‘projectedValue’ property of the wrapper, which typically returns the wrapper itself, so our updated wrapper should now look like this:
@propertyWrapper struct Adult<Value> {
var defaultValue: Value
var isAdult: Bool = false
var wrappedValue: Value {
get {
return false as? Value ?? defaultValue
}
set {
if newValue as? Int ?? 0 > 18 {
isAdult = true
}
else {
isAdult = false
}
}
}
var projectedValue: Adult<Value> {
return self
}
}
and it can be referenced by the users/clients/callee by prefixing the ‘$’ symbol and our updated User struct looks like this:
struct User {
@Adult(defaultValue: 0) var age: Int
static func userConfigWithWrapper() {
var s1 = User()
print("isAdult: \(s1.age)") //prints '0'
print("isAdult: \(s1.$age)") //prints 'Adult(defaultValue: 0, isAdult: false)'
s1.age = 10
print("isAdult: \(s1.$age.isAdult)") //prints 'false'
print("isAdult: \(s1.$age)") //prints 'Adult(defaultValue: 0, isAdult: false)'
s1.age = 20
print("isAdult: \(s1.$age.isAdult)") //prints 'true'
print("isAdult: \(s1.$age)") //prints 'Adult(defaultValue: 0, isAdult: true)'
}
}
Just compare this with the SwiftUI’s property binding functionality where we use the $ prefix to bind the properties between 2 views to keep them synchronized. It is the same functionality that we’ve implemented with our own custom property wrapper.
Conclusion:
I suppose this was a very simple example with a step-by-step explanation of how you can create your own property wrappers.
Property-wrapper is relatively new and has its own cons as said by matt but the way Apple engineers have adopted it and have used it widely in the SwiftUI framework is what’s making us all follow the lead, after all, they’re the pathmakers.
Also, these are some great resources related to this great topic - ‘Property Wrappers’:
I hope you enjoyed!
Comments