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!