Swift’s property wrappers are a powerful tool that allows developers to change how properties are stored and manipulated while keeping their interfaces clean and consistent. This post will discuss some practical use cases for property wrappers.
Let’s get started!
1. UserDefault Wrapper
UserDefaults is a straightforward mechanism to store small amounts of data persistently. We can simplify UserDefaults interactions with a UserDefault
property wrapper:
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
You can now store and retrieve UserDefaults values effortlessly:
class MyAppSettings {
@UserDefault("has_seen_onboarding", defaultValue: false)
static var hasSeenOnboarding: Bool
}
2. Clamping Values
What if you want to keep a property’s value within a specific range? A Clamping
property wrapper is perfect for this:
@propertyWrapper
struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(wrappedValue))
self.value = wrappedValue
self.range = range
}
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
}
For instance, let’s make sure a percentile score remains within a 1-100 range:
struct MyStruct {
@Clamping(1...100) var percentileScore: Int = 50
}
3. Tracking Changes with DidChange
Wrapper
In certain scenarios, you might want to perform an action each time a property’s value changes. A DidChange
property wrapper can streamline this:
@propertyWrapper
struct DidChange<Value> {
private var value: Value
let action: (Value) -> Void
init(wrappedValue: Value, action: @escaping (Value) -> Void) {
self.value = wrappedValue
self.action = action
}
var wrappedValue: Value {
get {
value
}
set {
value = newValue
action(value)
}
}
}
You can then use it like so:
class MyClass {
@DidChange(action: { print("Value did change to: \($0)") })
var value = 0
}
Now every time value
changes, it will print the new value.
4. Injected Property Wrapper for Dependency Injection
A fundamental design principle in software development is dependency inversion. Rather than hard-coding dependencies, we can use protocols and property wrappers to provide flexible and testable code. Consider the following Injected
property wrapper:
@propertyWrapper
struct Injected<Service> {
var wrappedValue: Service {
return DependencyInjector.shared.resolve()
}
}
class DependencyInjector {
static let shared = DependencyInjector()
private var services: [String: Any] = [:]
func register<Service>(_ service: Service) {
let key = "\(Service.self)"
services[key] = service
}
func resolve<Service>() -> Service {
let key = "\(Service.self)"
guard let service = services[key] as? Service else {
fatalError("Service of type \(key) not found.")
}
return service
}
}
By using this pattern, you can automatically inject dependencies into your types:
protocol UserService {
func fetchUser() -> User
}
class RealUserService: UserService {
func fetchUser() -> User {
// Fetch the user from network or database
}
}
class ViewModel {
@Injected var userService: UserService
}
// In your app setup
DependencyInjector.shared.register(RealUserService() as UserService)
Conclusion
Swift property wrappers offer a variety of possibilities. They allow for cleaner code, more reusability, and the capability to maintain the encapsulation principle. This post provided some useful and practical use cases for property wrappers. There’s plenty of room for creativity and innovation when working with property wrappers. I hope this encourages you to explore further and use property wrappers in your Swift applications. Happy coding!