What are property wrappers?

Swift Docs-Property Wrappers

Property wrappers are a powerful feature of Swift that allow you to add extra functionality to your properties without changing their declaration. In this blog post, I will explain what property wrappers are, how to use them, and some common use cases.

Property wrappers are types that wrap a value and provide additional behavior or logic. They are declared with the @propertyWrapper attribute and have a wrappedValue property that holds the actual value. For example, here is a simple property wrapper that adds logging to any property:

@propertyWrapper
struct Logged {
    var wrappedValue: String {
        didSet {
            print("Value changed from \(oldValue) to \(wrappedValue)")
        }
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

To use a property wrapper, you need to apply it to a property with the @ syntax. For example, here is a struct that uses the Logged property wrapper:

struct User {
    @Logged var name: String
    @Logged var email: String
}

Now, whenever you change the name or email of a user, you will see a log message in the console:

var user = User(name: "Alice", email: "alice@example.com")
user.name = "Bob" // Prints "Value changed from Alice to Bob"
user.email = "bob@example.com" // Prints "Value changed from alice@example.com to bob@example.com"

How do property wrappers work?

Property wrappers are implemented using computed properties and property observers. When you apply a property wrapper to a property, Swift generates a computed property with the same name as the original property, and a private storage property with an underscore prefix. The computed property accesses the wrappedValue of the storage property, and the storage property holds an instance of the property wrapper type. For example, this is what Swift generates for the User struct:

struct User {
    private var _name = Logged(wrappedValue: "Alice")
    private var _email = Logged(wrappedValue: "alice@example.com")

    var name: String {
        get { _name.wrappedValue }
        set { _name.wrappedValue = newValue }
    }

    var email: String {
        get { _email.wrappedValue }
        set { _email.wrappedValue = newValue }
    }
}

You can also access the storage property directly using the $ syntax. For example, you can access the Logged instance for the name property using $name:

print(user.$name) // Prints "Logged(wrappedValue: "Bob")"

What are some common use cases for property wrappers?

Property wrappers can be used for various purposes, such as validating input, caching values, transforming data, or managing state. Here are some examples of built-in or custom property wrappers that you can use in your projects:

  • @State: This is a property wrapper provided by SwiftUI that allows you to store state in a view and update it automatically when it changes. For example, you can use @State to store a boolean value that controls whether an alert is shown or not:
struct ContentView: View {
    @State var showAlert = false

    var body: some View {
        Button("Show Alert") {
            showAlert = true
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("Hello"))
        }
    }
}
  • @UserDefault: This is a custom property wrapper that allows you to store and retrieve values from UserDefaults with ease. For example, you can use @UserDefault to store and access a user’s preferred theme:
@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 { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct Settings {
    @UserDefault("theme", defaultValue: "light")
    static var theme: String
}

Settings.theme = "dark" // Saves "dark" to UserDefaults
print(Settings.theme) // Prints "dark" from UserDefaults
  • @Clamped: This is a custom property wrapper that allows you to clamp a value within a range. For example, you can use @Clamped to ensure that a volume level is between 0 and 100:
@propertyWrapper
struct Clamped<Value: Comparable> {
    var wrappedValue: Value {
        didSet {
            if wrappedValue < range.lowerBound {
                wrappedValue = range.lowerBound
            } else if wrappedValue > range.upperBound {
                wrappedValue = range.upperBound
            }
        }
    }

    let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.wrappedValue = wrappedValue
        self.range = range
    }
}

struct AudioPlayer {
    @Clamped(wrappedValue: 50, 0...100)
    var volume: Int
}

var player = AudioPlayer()
player.volume = 120 // Sets volume to 100
player.volume = -10 // Sets volume to 0

Conclusion

Property wrappers are a great way to add extra functionality to your properties without changing their declaration. They can help you write cleaner and more reusable code, and make your properties more expressive and consistent. I hope this blog post helped you understand what property wrappers are, how to use them, and some common use cases.

Thanks for reading!