Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

Property Wrappers in Swift explained with code examples

Property Wrappers in Swift enable you to extract common logic into a separate wrapper object. Introduced at WWDC 2019 and available since Swift 5, this feature is a useful addition to the Swift library that helps eliminate much of the boilerplate code we often write in our projects.

You can find a background story on this feature on the Swift forums for SE-0258.  While the motivation is mainly talking about it being a solution for @NSCopying, there’s a typical pattern it solves that you might all recognize. In this article, we’ll dive into what they are, their relationship to Swift Macros, and how they work together with Swift Concurrency.

What is a Property Wrapper?

You can see a property wrapper as an additional layer that defines how a variable is stored or computed on reading. It’s beneficial for replacing repetitive code found in getters and setters of properties.

A typical example is a custom-defined user default property in which a custom getter and setter transform the value accordingly. An example implementation could look as follows, for which I’ll share implementation details later:

extension UserDefaults {

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool
}

The @UserDefault statement is a call into the property wrapper. As you can see, we can provide a few configuration parameters. There are several ways to interact with a property wrapper, like using the wrapped value and the projected value. You can also set up a property wrapper with injected properties, which we’ll all cover later. Let’s first dive into an example to work with User Defaults.

A UserDefaults example

The following code shows a pattern you all might recognize. It creates a wrapper around the UserDefaults object to make properties accessible without having to paste the string keys everywhere throughout your project.

extension UserDefaults {

    public enum Keys {
        static let hasSeenAppIntroduction = "has_seen_app_introduction"
    }

    /// Indicates whether or not the user has seen the onboarding.
    var hasSeenAppIntroduction: Bool {
        set {
            set(newValue, forKey: Keys.hasSeenAppIntroduction)
        }
        get {
            return bool(forKey: Keys.hasSeenAppIntroduction)
        }
    }
}

It allows you to set and get values from the user defaults from anywhere as follows:

UserDefaults.standard.hasSeenAppIntroduction = true

guard !UserDefaults.standard.hasSeenAppIntroduction else { return }
showAppIntroduction()

Now, as this seems to be a great solution, it could quickly end up being a large file with many defined keys and properties. The code is repetitive and could be made easier. A custom property wrapper using the @propertyWrapper keyword can help us solve this problem.

How to create a property wrapper

Taking the above example, we can rewrite the code and remove a lot of overhead. For that, we have to create a new property wrapper, which we will call UserDefault. Doing so eventually allows us to define a property as being a user default property.

Note: If you’re using SwiftUI, you should use the AppStorage property wrapper instead. Take this just as an example of replacing repetitive code.

You can create a Property Wrapper by defining a struct and marking it with the @propertyWrapper attribute. The attribute requires adding a wrappedValue property to provide a return value at the implementation level.

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }
}

As shown in the above example, the user defaults wrapper allows passing in a default value if there’s no registered value yet. We can pass in any value since the wrapper is using a generic value Value.

We can now change our previous code implementation and create the following extension on the UserDefaults type:

extension UserDefaults {

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool
}

As you can see, we can use the default generated struct initializer. We pass in the same key as before and set the default value to false. Using this new property is simple:

UserDefaults.hasSeenAppIntroduction = false
print(UserDefaults.hasSeenAppIntroduction) // Prints: false
UserDefaults.hasSeenAppIntroduction = true
print(UserDefaults.hasSeenAppIntroduction) // Prints: true

In some cases, you might want to define your custom user defaults. For example, in cases where you have an app group defined user defaults. Our illustrated wrapper defaults to the standard user defaults, but you can override this to use your container:

extension UserDefaults {
    static let groupUserDefaults = UserDefaults(suiteName: "group.com.swiftlee.app")!

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false, container: .groupUserDefaults)
    static var hasSeenAppIntroduction: Bool
}

Adding more variables using the same solution

Unlike the old solution, it’s effortless to add more properties when using a property wrapper. We can reuse the defined wrapper and instantiate as many variables as we need.

extension UserDefaults {

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool

    @UserDefault(key: "username", defaultValue: "Antoine van der Lee")
    static var username: String

    @UserDefault(key: "year_of_birth", defaultValue: 1990)
    static var yearOfBirth: Int
}

As you can see, the wrapper works with any type you define as long as the type is supported to be saved in the user defaults as well.

Storing optionals

A common issue you can run into when using property wrappers is that the generic value either allows you to define all optionals or all unwrapped values. There’s a common technique found in the community to deal with this, which makes use of a custom-defined AnyOptional protocol:

/// Allows to match for optionals with generics that are defined as non-optional.
public protocol AnyOptional {
    /// Returns `true` if `nil`, otherwise `false`.
    var isNil: Bool { get }
}

extension Optional: AnyOptional {
    public var isNil: Bool { self == nil }
}

We can extend our UserDefault instance for cases where the Value conforms to the ExpressibleByNilLiteral. In other words, when the value is an optional:

extension UserDefault where Value: ExpressibleByNilLiteral {
    
    /// Creates a new User Defaults property wrapper for the given key.
    /// - Parameters:
    ///   - key: The key to use with the user defaults store.
    init(key: String, _ container: UserDefaults = .standard) {
        self.init(key: key, defaultValue: nil, container: container)
    }
}

This extension creates an additional initializer that removes the requirement of defining a default value and allows working with optionals.

Lastly, we need to adjust our wrapper value setter to allow removing objects from the user defaults when the value is nil:

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            // Check whether we're dealing with an optional and remove the object if the new value is nil.
            if let optional = newValue as? AnyOptional, optional.isNil {
                container.removeObject(forKey: key)
            } else {
                container.set(newValue, forKey: key)
            }
        }
    }

    var projectedValue: Bool {
        return true
    }
}

This now allows us to define optionals and set values to nil:

extension UserDefaults {

    @UserDefault(key: "year_of_birth")
    static var yearOfBirth: Int?
}

UserDefaults.yearOfBirth = 1990
print(UserDefaults.yearOfBirth) // Prints: 1990
UserDefaults.yearOfBirth = nil
print(UserDefaults.yearOfBirth) // Prints: nil

Great! We can now handle most scenarios with the user defaults wrapper. The last thing to add is a projected value, which we can convert to a Combine publisher, just like the @Published property wrapper.

How do you stay current as a Swift developer?

Let me do the hard work and join 27,293 developers that stay up to date using my weekly newsletter:

Projecting a Value From a Property Wrapper

Property wrappers can add another property besides the wrapped value, which is called the projected value. This allows us to project another value based on the wrapped value. A typical example is to define a Combine publisher so that we can observe changes when they occur.

To achieve this with our user defaults instance, we need to add a publisher that serves as a passthrough subject. It’s all in the name: it will simply pass through value changes. The implementation looks as follows:

 import Combine
 
 @propertyWrapper
 struct UserDefault<Value> {
     let key: String
     let defaultValue: Value
     var container: UserDefaults = .standard
     private let publisher = PassthroughSubject<Value, Never>()
     
     var wrappedValue: Value {
         get {
             return container.object(forKey: key) as? Value ?? defaultValue
         }
         set {
             // Check whether we're dealing with an optional and remove the object if the new value is nil.
             if let optional = newValue as? AnyOptional, optional.isNil {
                 container.removeObject(forKey: key)
             } else {
                 container.set(newValue, forKey: key)
             }
             publisher.send(newValue)
         }
     }

     var projectedValue: AnyPublisher<Value, Never> {
         return publisher.eraseToAnyPublisher()
     }
 } 

We can now start observing changes to our property as follows:

 let subscription = UserDefaults.$username.sink { username in
     print("New username: \(username)")
 }
 UserDefaults.username = "Test"
 // Prints: New username: Test 

This is great! It allows us to respond to any changes. As we defined our property statically before, this publisher will now work across our app. If you like to learn more about Combine, make sure to check out my article Getting started with the Combine framework in Swift.

Accessing private defined properties

Although it’s not recommended to work with property wrappers this way, it can be helpful in some cases to read the defined properties of a wrapper. I’ll demonstrate that this is possible, but you should reconsider your code implementation if you need to access private properties.

A private defined property can be accessed by using an underscore prefix. This allows us to access the private property key from our user defaults property wrapper:

extension UserDefaults {
    static func printKey() {
        print(_yearOfBirth.key) // Prints "year_of_birth"
    }
}

Take it with a grain of salt and see whether you can solve your needs by using a different instance type instead. One idea could be to access the property wrapper’s enclosing instance, which will have the added benefit of accessing private defined properties from external framework modules.

A better alternative for accessing all variables

By using the projected value of a property wrapper, we can define public access to all defined properties on a wrapper. We can do this by returning the wrapper instance itself as follows:

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            return container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }

    var projectedValue: UserDefault<Value> {
        self
    }
}

The projected value is now returning the user default structure with the generic type value. Its usage looks as follows:

extension UserDefaults {
    static func printPrivateProperties() {
        // Through underscore
        print(_hasSeenAppIntroduction.key)

        // Through projected value
        print($hasSeenAppIntroduction.key)
    }
}

Accessing a property wrapper’s enclosing instance

Using a custom static subscript, you’ll be able to access the property wrapper’s enclosing instance that defined the specific property wrapper. This can lead to exciting use cases in which you can share the same underlying instance properties for each defined wrapper.

For example, we could define a preferences class that represents a single user defaults container:

final class Preferences {
    let container = UserDefaults(suiteName: "group.com.swiftlee.app")!

    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    var hasSeenAppIntroduction: Bool
}

As you can see, we no longer defined the container in our user default property wrapper initializer. To still give access to the correct container, we can add a static subscript to our wrapper definition:

struct UserDefault<Value> {
    let key: String
    let defaultValue: Value

    @available(*, unavailable)
    var wrappedValue: Value {
        get { fatalError("This wrapper only works on instance properties of classes") }
        set { fatalError("This wrapper only works on instance properties of classes") }
    }

    static subscript(
        _enclosingInstance instance: Preferences,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
    ) -> Value {
        get {
            let propertyWrapper = instance[keyPath: storageKeyPath]
            let key = propertyWrapper.key
            let defaultValue = propertyWrapper.defaultValue
            return instance.container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            let propertyWrapper = instance[keyPath: storageKeyPath]
            let key = propertyWrapper.key
            instance.container.set(newValue, forKey: key)
        }
    }
}

This static subscript only works with instance properties of classes to have a reference type. By defining a fatal error inside the wrapper value, we ensure that usage on structures results in an exception. This technique is great for debugging purposes as it will directly provide feedback.

The static subscript allows us to access both the property wrapper container and the enclosing instance that defined the variable with the user defaults wrapper attribute. In other words, it gives us access to the UserDefault struct, as well as to the Preferences class.

By combining these, we can rewrite our user defaults wrapper to use the container as defined on the Preferences instance. This is a great way to reuse the same underlying user defaults container for each wrapper.

Attaching Property Wrappers to function and closure parameters

You can also use Property Wrappers with function or closure parameters, resulting in exciting use-cases that can help you remove more boilerplate code or improve debugging.

The following example demonstrates a wrapper for debugging purposes:

@propertyWrapper
struct Debuggable<Value> {
    private var value: Value
    private let description: String

    init(wrappedValue: Value, description: String = "") {
        print("Initialized '\(description)' with value \(wrappedValue)")
        self.value = wrappedValue
        self.description = description
    }

    var wrappedValue: Value {
        get {
            print("Accessing '\(description)', returning: \(value)")
            return value
        }
        set {
            print("Updating '\(description)', newValue: \(newValue)")
            value = newValue
        }
    }
}

A log will appear whenever the property is accessed or updated, while we can also add a breakpoint for enhanced debugging.

As an example, we could add this wrapper to the function argument duration in the following animation example:

func runAnimation(@Debuggable(description: "Duration") withDuration duration: Double) {
    UIView.animate(withDuration: duration) {
        // ..
    }
}

runAnimation(withDuration: 2.0)

// Prints:
// Initialized 'Duration' with value 2.0
// Accessing 'Duration', returning: 2.0

This can be a great tool during debugging in which you add the wrapper temporarily. Obviously, this is just an example. Other common use-cases allow transforming a string value to uppercase or lowercase within a function argument.

Lastly, you can use the same wrapper within closures:

struct Article {
    let title: String
}

let articleFactory: (String) -> Article = { (@Debuggable(description: "Closure debug") title) in
    return Article(title: title)
}

let article = articleFactory("Property Wrappers in Swift")

// Prints:
// Initialized 'Closure debug' with value Property Wrappers in Swift
// Accessing 'Closure debug', returning: Property Wrappers in Swift

The description inside the property wrapper initializer helps you identify which part of the code you’re running.

Other usage examples

Property wrappers are used throughout the default Swift APIs as well. Especially in SwiftUI, you’ll find property wrappers like @StateObject and @Binding. They all have something in common: making often-used patterns easier to access.

Defining Sample Files using a property wrapper

The prominent example focuses on user defaults, but what if you want to define another wrapper? Let’s dive into another example that will hopefully spark some ideas.

Take the following property wrapper in which we define a sample file:

@propertyWrapper
struct SampleFile {

    let fileName: String

    var wrappedValue: URL {
        let file = fileName.split(separator: ".").first!
        let fileExtension = fileName.split(separator: ".").last!
        let url = Bundle.main.url(forResource: String(file), withExtension: String(fileExtension))!
        return url
    }

    var projectedValue: String {
        return fileName
    }
}

We can use this wrapper to define our sample files which we might want to use for debugging or while running tests:

struct SampleFiles {
    @SampleFile(fileName: "sample-image.png")
    static var image: URL
}

The projectedValue property allows us to read out the file name as used in the property wrapper:

print(SampleFiles.image) // Prints: "../resources/sample-image.png"
print(SampleFiles.$image) // Prints: "sample-image.png"

This can be useful in cases where you want to know which initial value(s) have been used by the wrapper to compute the final value. Note that we’re using the dollar sign here as a prefix to access the projected value.

Property Wrappers vs. Macros

If you’re familiar with Swift Macros, you might wonder how they compare to property wrappers and when you should use one over the other. While they can sometimes solve similar problems, they serve different purposes:

  • Property Wrappers are designed specifically to add behavior to properties. They encapsulate logic like persistence, validation, or transformation in a reusable, declarative way. Their scope is narrow: they only wrap individual properties, and their power comes from simplifying repetitive patterns related to stored values.
  • Macros, on the other hand, operate at the syntax level. They allow you to generate or transform Swift code before compilation. This makes them much more powerful and flexible: you can create boilerplate, generate conformances, or apply transformations beyond just properties. However, that power comes with increased complexity, and its use should be limited to situations where property wrappers or other abstractions are not expressive enough.

In short, property wrappers are specialized, and macros are general-purpose. If you can solve your problem cleanly with a property wrapper, prefer it for its simplicity and readability. If you need to affect broader parts of your code, macros are the tool for the job. However, there’s another factor that can influence your decision: Swift Concurrency.

Swift Concurrency and Property Wrappers

You might have been adopting Property Wrappers over the years and finding yourself now migrating existing code to Swift Concurrency. You’re trying to conform types to Sendable and you run into issues with property wrappers.

Property wrappers require properties to be defined as a variable. Variables are mutable, and Strict Concurrency will start to complain about sendability. As an example, let’s make our sample file property wrapper conform to Sendable:

@propertyWrapper
struct SampleFile: Sendable {

    let fileName: String

    var wrappedValue: URL {
        let file = fileName.split(separator: ".").first!
        let fileExtension = fileName.split(separator: ".").last!
        let url = Bundle.main.url(forResource: String(file), withExtension: String(fileExtension))!
        return url
    }

    var projectedValue: String {
        return fileName
    }
}

You might think we’re all good now. There are no mutable members on the property wrapper, so everything’s safe. However, we will run into the following compilation error when using the sample file wrapper:

Property Wrappers don't go well together with Strict Concurrency and Sendable support.
Property Wrappers don’t go well together with Strict Concurrency and Sendable support.

We would need to change the var into a let to solve this compilation error, but that’s not possible when using the property wrapper as we would get a compilation error: “Property wrapper can only be applied to a ‘var'”.

At this point, I gave up and decided to go for nonisolated(unsafe) as a rescue path, but even that is not possible:

‘nonisolated’ is not supported on properties with property wrappers

It looks like, at this point, there’s no way to properly use Property Wrappers with Sendable when strict concurrency and Swift 6 are enabled. I’ve posted my thoughts on the Swift forums and truly hope this is not the end for Property Wrappers.

Conclusion

Property wrappers are a great way to remove boilerplate in your code. The above example is only one of many scenarios in which it can be helpful. You can try it out yourself by finding repetitive code and replacing it with a custom wrapper.

If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.

Thanks!

 
Antoine van der Lee

Written by

Antoine van der Lee

iOS Developer since 2010, former Staff iOS Engineer at WeTransfer and currently full-time Indie Developer & Founder at SwiftLee. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

Are you ready to

Turn your side projects into independence?

Learn my proven steps to transform your passion into profit.