Coder Orangu often found themselves writing the same repetitive boilerplate code in Swift just to manage variables. Tasks like persistently saving a value or ensuring thread safety when accessing a variable became tedious.
If you’ve faced the same challenge, property wrappers can be a game-changer, helping eliminate redundant code while improving reusability.
Property wrappers, as the name suggests, wrap around properties to encapsulate common logic. Apple first introduced them in WWDC 2019 session with Swift 5.1.
Did You Know
Swift property wrappers were originally called property delegates (SE-0258), inspired by Kotlin’s delegated properties.
Basics
A property wrapper is an object that provides an additional layer for reading and writing a variable. It is typically implemented as a class or struct annotated with the @propertyWrapper attribute and contains a property named wrappedValue.
A typical use-case of a property wrapper is saving & accessing the value from persistent. Let’s create a property wrapper to save a bool value in UserDefault.
1
2
3
4
5
@propertyWrapper
struct UserDefaultPersist {
var wrappedValue: Bool
// Your implementation to save a value in UserDefaults.
}
Now, we can use this property wrapper as follows.
1
@UserDefaultPersist var isCoachMarkShown: Bool = false
Here, the property isCoachMarkShown uses @UserDefaultPersist, which manages the access and storage of the underlying value
How Does the Compiler Handle Property Wrappers?
When you use a property wrapper, the Swift compiler automatically transforms your code. For example, the declaration:
1
@UserDefaultPersist var isCoachMarkShown: Bool
is internally transformed by the compiler into:
1
2
3
4
5
6
7
8
//Compiler Generated code
var $isCoachMarkShown: UserDefaultPersist = UserDefaultPersist() //Property 1
//Property 2
public var isCoachMarkShown: Bool {
get {$isCoachMarkShown.wrappedValue}
set {$isCoachMarkShown.wrappedValue = newValue}
}
This transformation allows controlled access to the wrapped property, reducing repetitive code.
Enhancing the Property Wrapper
Coder Orangu has implemented a complete property wrapper that saves a Bool value in UserDefaults, making data persistence simpler.
Step 1: Define a Key for Storage
To store a value in UserDefaults, we need a unique key. Since the key cannot be hardcoded inside the property wrapper, we pass it through an initializer:
1
2
3
4
5
6
7
8
9
@propertyWrapper
struct UserDefaultPersist {
private var defaultKey: String
var wrappedValue: Bool = false
init(defaultKey: String) {
self.defaultKey = defaultKey
}
}
Step 2: Implement Read/Write Logic
Now, let’s modify the wrappedValue property to read and write from UserDefaults.
1
2
3
4
5
6
7
var wrappedValue: Bool {
get {
UserDefaults.standard.bool(forKey: defaultKey)
} set {
UserDefaults.standard.setValue(newValue, forKey: defaultKey)
}
}
Step 3: Using the Property Wrapper
Since we now require a key, the property wrapper usage slightly changes:
1
UserDefaultPersist(defaultKey: "isCoachMarkShown") var isCoachMarkShown: Bool
We need to pass defaultKey as parameter in init.
UserDefaults Property Wrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@propertyWrapper
struct UserDefaultPersist {
private var defaultKey: String
var wrappedValue: Bool {
get {
UserDefaults.standard.bool(forKey: defaultKey)
} set {
UserDefaults.standard.setValue(newValue, forKey: defaultKey)
}
}
init(defaultKey: String) {
self.defaultKey = defaultKey
}
}
More Advance Examples
Generic UserDefaults Property Wrapper
Instead of restricting the property wrapper to Bool, Coder Orangu realized it could be extended to support any value type. By making it generic, the wrapper becomes even more flexible and reusable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@propertyWrapper
struct UserDefaultPersist<Value> {
private var defaultKey: String
var wrappedValue: Value? {
get {
UserDefaults.standard.value(forKey: defaultKey) as? Value
} set {
UserDefaults.standard.setValue(newValue, forKey: defaultKey)
}
}
init(defaultKey: String, defaultValue: Value) {
self.defaultKey = defaultKey
self.wrappedValue = defaultValue
}
}
Uses:
1
@UserDefaultPersist(defaultKey: "isCoachMarkShown", defaultValue: false) var isCoachMarkShown: Bool?
Atomic Property Wrapper (Thread-Safety)
One of the biggest challenges in multithreaded applications is ensuring thread-safe access to shared variables. To address this, Coder Orangu implemented a property wrapper using NSLock, ensuring atomic access and preventing race conditions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@propertyWrapper
struct AtomicPropertyWrapper<Value> {
private let lock: NSLock = .init()
private var value: Value
var wrappedValue: Value {
get { return getValueAtomicity() }
set { setValueAtomicity(newValue: newValue) }
}
init(wrappedValue value: Value) {
self.value = value
}
fileprivate func getValueAtomicity() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
fileprivate mutating func setValueAtomicity(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
Uses:
1
@AtomicPropertyWrapper var counter: Int = 1
Key Takeaways
Property wrappers help eliminate repetitive code and improve code reusability.
Swift compiler automatically transforms property wrapper declarations into computed properties.
UserDefaults property wrappers simplify persistent storage access.
Atomic property wrappers ensure thread-safe variable access.
Property wrappers are a powerful yet simple feature that makes Swift code cleaner and more maintainable. Apple encourages developers to use them for writing expressive and reusable APIs.
Thanks for reading! Let me know your thoughts or any improvements. 🚀
Comments powered by Disqus.