Advanced Property Wrappers in Swift

We love Swift as a lean and intuitive programming language and recently, SwiftUI made our hearts beat faster for providing us with great tools to create amazing user experiences with very little, simple code. But with the introduction of property wrappers, a lot of @-signs, $-signs and underscores suddenly popped up. Didn’t it just get a lot more complicated with that?

In good old Objective-C, those signs were over all the place, but in Swift, it seemed like they were gone for good for the sake of a simplified syntax – until Apple reintroduced them with property wrappers in Swift 5.1 (Proposal). So what’s the benefit of that? How do property wrappers work and what do you need to know about them?

In this article, we’ll take a quick look at how you can use existing property wrappers (for those of you who aren’t familiar with them, yet). We’ll continue with some very useful examples and then explain how you can write your own property wrappers. We will also have a look at some limitations of the current implementation and discuss when and how property wrappers are best used.

Fortunately, Apple already provides us with several property wrappers in iOS. So before we start creating our own property wrappers, let’s dive right in with how to use them!



How to Use Property Wrappers

We’ll start with a simple example of a very well-known problem in iOS development: UserDefaults. We frequently see properties with custom accessors like this:

 

var username: String {
    get {
        UserDefaults.standard.string(forKey: "user-name")
    }
    set {
        UserDefaults.standard.set(newValue, forKey: "user-name")
    }
}

These accessors for reading and writing to the UserDefaults are often scattered across the app, leading to a lot of code repetition. In a way, they expose an implementation detail to the class that’s using them, making the code more verbose.

From iOS 14 onwards, the@AppStorage property wrapper can be used to accomplish the same result – with a much simpler syntax. It’s especially useful in SwiftUI views.

Let’s say we have a property username in our view that we want to be synced with the UserDefaults:

struct MyView: View {
    @AppStorage("user-name")
    var username = ""
}

We can use the username property just like any other property. You can think of it as a computed property that reads and writes values, but instead of writing the code yourself, the AppStorage wrapper provides it for you. The specified String "user-name" corresponds to the key used for UserDefaults.

There are three ways to access different parts of a property wrapper: _username, $username and username:

  • With _username, you can access the AppStorage property wrapper itself.
  • With username, you access the wrappedValue property of the wrapper – in other words: It’s is equivalent to _username.wrappedValue.
  • With $username, you can access the projectedValue property of the wrapper which can be used for additional context provided by the property wrapper. We’ll discuss it in more detail later.
Direct Access Indirect Access Type
username
_username.wrappedValue
String
_username
AppStorage<String>
$username
_username.projectedValue
Binding<String>

But isn’t this just syntactic sugar, a simple convenience? In fact, everything you can do with property wrappers, you can do without them as well (at least outside the world of SwiftUI). However, when used the right way, property wrappers can make your code cleaner and less verbose.


Applications

Now that we’ve familiarized ourselves with the basics, let’s get a better feeling for what you can do with property wrappers with some real-world examples!

BetterCodable

BetterCodable makes use of property wrappers to ease the use of Codable for common problems with unusual encoding/decoding issues. For example, you can specify the date format for a given property or define lossy arrays which don’t fail when the decoding of any of their values fails. They simply skip values which can’t be read and continue decoding.

You could, of course, write all of this yourself, even hide it quite nicely in an extension, but with these property wrappers, you don’t need to write custom encoding/decoding at all and you can specify the encoding/decoding right at declaration.

struct Response: Codable {
    @LossyArray var integers: [Int]
    @DefaultEmptyArray var strings: [String]
    @LosslessValue var boolean: Bool
    @DateValue<ISO8601Strategy> var start: Date
    @DateValue<TimestampStrategy> var end: Date
}

In the example above, a JSON payload in the following form

{
    "integers": [3, 4, 5, null],
    "boolean": "true",
    "start": "1970-01-01T00:00:00+02:00",
    "end": 978307200.0
}

would still be decoded to the following object:

Response(
    integers: [3, 4, 5],
    strings: [], 
    boolean: true,
    start: Date(timeIntervalSince1970: 0), // 01.01.1970 at 00:00
    end: Date(timeIntervalSince1970: 978307200.0) // 01.01.2001 at 00:00
)

Instead of failing the decoding process (in a possibly quite large JSON that you have no control over), the integers array can contain non-integer values that will be ignored, the strings array can be null or not exist at all without requiring to be optional in the struct as well. LosslessValue can convert between different representations of values. For example, if a boolean is encoded as a string in the JSON ("true") instead of a boolean (true), decoding will still succeed. Both dates using different encoding/decoding strategies, which would normally not be supported with Codable (without writing complicated custom decodings).

Fluent

Fluent is a framework to access local databases, e.g. SQLite or SQL databases. (You might know it from Vapor.) Despite being very useful for many applications, it had some downsides in version 3: For example, you had to provide a keypath for the property that you want to use as the identifier. Defining custom keys for individual properties was also pretty difficult.

That’s why they introduced property wrappers in the new version 4.0 to make these tasks a lot easier:

final class User: Model {

    static let schema = "users"

    @ID(custom: .id)
    var id: UUID?

    @Field(key: "email")
    var email: String

    @Field(key: "password")
    var passwordHash: String

    init() { }

    init(id: UUID? = nil, email: String, passwordHash: String) {
        self.id = id
        self.email = email
        self.passwordHash = passwordHash
    }

}

In this example, the property we want to use as the identifier is simply marked with the @ID(custom:) property wrapper. We specify which properties are stored in the database with their respective key using the @Field(key:) wrapper. We can, therefore, have our database configuration right where we define our properties, making it easy to add new properties.

SwiftUI

In case you are familiar with SwiftUI, you will most definitely also have stumbled over some property wrappers, such as @State, @Environment, @EnvironmentObject, @ObservedObject, @StateObject and many more. (If you haven’t, you may want to skip this section.) Let’s look at some of them and how they differ in their state management techniques.

struct HomeView: View {

    @Environment(\.presentationMode)
    var presentationMode

    @EnvironmentObject
    var store: Store

    @ObservedObject
    var service: DataService

    @State 
    private var query = ""

    var body: some View {
        ...
    }

}

In this example, we access a value presentationMode and an observable object store from a view’s environment (where the view configuration, such as accentColor or the current presentationMode is stored in). Furthermore, we expect a service in HomeView's initializer, which we want to observe (when it changes, the view gets updated). Additionally, a query is used as an internal state of the view containing the content of a search text field.

From this example, we can see that property wrappers can be used to extract values from different locations (e.g. the view’s environment) and/or add observability. If you want to learn more about how property wrappers are used in SwiftUI, please check out our article on SwiftUI Architectures.

Invariants & Mappings

Property Wrappers can also be used to ensure invariants and map values. For example, you might want to store a percentage value as a floating-point number between 0 and 1, but in the app, you want to access it as a value between 0 and 100. Or you might want to make sure a value is clamped inside a given range.

Example: You could build a property wrapper to clamp a value into a specific range (i.e. taking the max/min of that range, if the set value is outside of the range). In this case, we build a RGBColor struct containing red, green and blue values.

struct RGBColor {

    @Clamped(0...255)
    var red: Double

    @Clamped(0...255)
    var green: Double

    @Clamped(0...255)
    var blue: Double

}

When the user assigns a red value of 350, we want to stay in the valid range for RGB colors and assign 255 instead (since it is closest to the assigned value).


How to Build Your Own Property Wrappers

By now, you should have a pretty good feeling for what property wrappers are and how you can leverage them to simplify your code. So let’s see how to create our own!

🔑 A Keychain property wrapper

Apple only provides a property wrapper for UserDefaults (@AppStorage), there is none for Keychain access. So let’s change that and create a Keychain property wrapper! This is how we want it to be used:

@SecureAppStorage("user-name")
var userName: String?

What we use to build it

To make this a bit easier, we already wrote a KeychainItem struct that helps us connect to the Keychain with a simple interface.

struct KeychainItem {

    let service: String
    let account: String

    func get() -> String? { ... }
    func set(_ value: String?) { ... }

}

The full implementation is not central to this article, but if you’re curious you can find it here.

How we build it

Now let’s write our wrapper around that KeychainItem!

@propertyWrapper
struct SecureAppStorage {

    var item: KeychainItem

    init(_ account: String, service: String = Bundle.main.bundleIdentifier!) {
        self.item = .init(service: service, account: account)
    }

    public var wrappedValue: String? {
        get {
            item.get()
        }
        nonmutating set {
            item.set(newValue)
        }
    }

}

In the first line, we mark the following struct as a @propertyWrapper. In the initializer, a user can specify the keychain account and service to be used. (The account is the identifier with which the Keychain item is referenced within the service.) The wrappedValue implements a property with custom accessors (get/set) to read and write the respective value to and from the Keychain. When you call any property which uses a SecureAppStorage property wrapper, this is the value that is accessed implicitly.

If you were to further allow an initial/default value, you could add a first attribute wrappedValue in the initializer of your property wrapper. In our example, we could use the following initializer instead:

init(wrappedValue: String, 
     _ account: String, 
     service: String = Bundle.main.bundleIdentifier!) {
    ...
}

This would, for example, provide the following functionality:

@SecureAppStorage("user-name")
var username = "empty-user-name"

While the first attribute of the initializer is then filled with the "empty-user-name" string, the rest of the attributes are specified in the parentheses after the property wrapper name.

How we extend it

Now there might be some cases in which we want to access the KeychainItem directly, so we need a way to access it quickly. We could, of course, write a computed property on SecureAppStorage and access it with _username.<my property name> as explained above, but instead, we can also to use the shorter syntax here: $username. All we need to do is add a projectedValue property to our SecureAppStorage wrapper:

extension SecureAppStorage {
    var projectedValue: KeychainItem {
        item
    }
}

In other words: $username is just an alias for accessing the property wrapper’s projectedValue.


⚠️ Pitfalls & Limitations

In the previous sections, we’ve seen how powerful property wrappers are and how they can dramatically improve your code’s readability. But as with most tools, there are also some downsides and things you need to be aware of (referring to Swift 5.2 and 5.3).

Property wrappers are always private.

Accessing a property wrapper from outside of a given type is impossible or even in an extension of a type in a different file from its declaration is impossible without exposing it using a custom computed property.

In the following example, _username cannot be accessed from outside of KeyValueStore class:

final class KeyValueStore {
    @SecureAppStorage("user-name")
    var username: String?
}

Property wrappers can’t be aliased.

Let’s say, you would like to use the @Clamped property wrappers from above for different percentage values in your app and would therefore like to use a @Percentage wrapper, like this:

@Percentage
var opacity: Double = 0.5

Since property wrappers are structs (most of the time), inheritance cannot be used to create a subclass Percentage of Clamped. Assuming, we do not want to use inheritance, there is no intuitive way to create aliases for property wrappers.

Let’s assume that in the example above, we would like to write the following (no valid Swift code!):

typealias Percentage = Clamped(0...1)

There are ways to accomplish this, but they are far from ideal – see this article for more information.

Using property wrappers for non-property variables is not allowed.

The syntactic difference between defining properties or variables is quite small in Swift since the only difference is their context. However, property wrappers cannot be used for variables – only on properties of an enclosing type.

You cannot override properties with wrapped properties.

Swift does not allow you to override wrapped properties with differently wrapped properties without resorting back to using custom getters and setters. If we tried to override a wrapped property value in a subclass like this:

class SuperClass {

    @SuperClassWrapper
    var value: Value

}

class SubClass: SuperClass {

    @SubClassWrapper
    override var value: Value

}

Swift would throw two error messages:

Property 'value' with attached wrapper cannot override another property.

and

Cannot override with a stored property 'value'.

What works instead, is the following approach, but it goes against the idea of using property wrappers in the first place.

class SuperClass {

    @SuperClassWrapper
    var value: Value

}

class SubClass: SuperClass {

    @SubClassWrapper
    var _value: Value

    override var value: Value {
        get { _value }
        set { _value = newValue }
    }

}

Dependencies between properties and property wrappers

Let’s assume, you built a KeyValueStore type containing all UserDefaults and Keychain elements, as shown below:

class KeyValueStore {

    @SecureAppStorage("username") 
    var username

    @SecureAppStorage("password")
    var password

    @AppStorage("isFirstLaunch")
    var isFirstLaunch = false

    @AppStorage("isLoggedIn")
    var isLoggedIn: Bool

}

Now, since you want to use the store in different parts of your app (e.g. for different users), you want to use different service names and UserDefaults. You might try to add two properties serviceName and defaults to the store and then pass them to the respective property wrappers like this:

class KeyValueStore {

    let serviceName: String
    let defaults: UserDefaults

    @SecureAppStorage("username", service: serviceName) 
    var username

    @SecureAppStorage("password", service: serviceName)
    var password

    @AppStorage("isFirstLaunch", defaults: defaults)
    var isFirstLaunch = false

    @AppStorage("isLoggedIn", defaults: defaults)
    var isLoggedIn: Bool

}

But property wrappers are not loaded lazily, meaning that you cannot use any reference to self or any of its properties to initialize them.

The only way around this limitation is to manually create the property wrappers inside an initializer:

class KeyValueStore {

    let serviceName: String
    let defaults: UserDefaults

    init(serviceName: String, defaults: UserDefaults) {
        self.serviceName = serviceName
        self.defaults = defaults
        self._username = SecureAppStorage("username", service: serviceName)
        self._password = SecureAppStorage("password", service: serviceName)
        self._isFirstLaunch = AppStorage(
            wrappedValue: false, 
            "isFirstLaunch", 
            defaults: defaults
        )
        self._isLoggedIn = AppStorage(
            wrappedValue: false, 
            "isLoggedIn", 
            defaults: defaults
        )
    }

    @SecureAppStorage
    var username

    @SecureAppStorage
    var password

    @AppStorage
    var isFirstLaunch: Bool

    @AppStorage
    var isLoggedIn: Bool

}

This doesn’t align with the lean approach of property wrappers. Adding new properties to this existing type will not be as easy as it would be, if property wrappers could be loaded lazily and their initialization could happen at the same place as the property declaration.

Property wrappers cannot be required in protocols.

In a protocol declaration, you cannot specify that a property should be wrapped by a specific property wrapper. Similarly, you cannot specify whether a property should be stored or computed. For example, we cannot write a protocol like the following to force an implementation to use a SecureAppStorage property wrapper for the username property.

protocol KeyValueStoreProtocol {
    @SecureAppStorage var username { get set }
}

There is an ongoing discussion about adding this feature in the Swift Evolution process, so it might eventually be possible in the future. We would highly appreciate this change, since it would allow us to make sure that data is stored securely at protocol level (as in the example above) instead of making sure to adhere to these constraints manually.

You cannot use different types for getting and setting the wrappedValue.

Let’s say you would like to build a property wrapper to use a default value when you assign nil to the property.

@Default([])
var array: [String]?

// Usage:
array = nil
print(array) // result is an Optional<[String]>
             // but we would like it to be [String]

Unfortunately, it is not possible to define a separate type for getting and setting the property. You could circumvent this problem by adding a set(_:) method to your property wrapper and instead of setting the property directly, you could then call this method using _array.set(nil). However, this doesn’t appear to be a very elegant solution as the syntax required for setting and getting the value would be inconsistent.

Setting a property wrapper’s wrappedValue cannot result in failure.

Since variable assignments in Swift cannot throw an error, property wrappers have no unified interface to handle failure events. That’s unfortunate since there are cases where we would really benefit from error handling: For example, a property wrapper might store changes to its property in a file. In these cases, we may want to handle the error when reading or writing to the file fails.

As a work-around for this problem, you might want to provide the failure events using Combine publishers or RxSwift observables, have a failure property, change the wrappedValue to Result<Value, Error> or again using a custom throwing set(_:) method on your property wrapper.

Property wrappers might hide heavy computations.

The syntax for property wrappers is quite minimalistic and powerful. If a property wrapper performs some heavy computations to fetch and/or store a value, it might lead to unforeseeable performance issues that are not directly visible where the wrapper is used, as it merely looks like a simple property assignment. To circumvent this issue, property wrappers might need to adhere to certain complexity criteria (such as constant access time) and/or cache values to get constant get/set times over a long period of time.

Let’s take a database, for example: When updating a property on a model object would automatically update the database value, you might run into performance issues quite fast – especially when setting such properties in code parts, where high performance is crucial, since it might be executed many times per second.

Protocol types cannot be used for property wrappers with type constraints.

Some property wrappers, such as the @ObservedObject wrapper in SwiftUI, require their wrapped value to conform to a certain protocol. If there is a type constraint on a generic type in Swift, you cannot specify a protocol as that type constraint but rather have to use a concrete type, even if that protocol has no associatedType/Self requirement.

Example: If you were to create a property wrapper MyPropertyWrapper<ValueType: MyProtocol>, you cannot use it like this: @MyPropertyWrapper var value: MyProtocol.

Referencing the enclosing self is not possible – yet.

The Swift Evolution proposal for property wrappers also contained a section about referencing the enclosing self in a wrapper. Even though the features mentioned in the given section have already been implemented, they are not yet available to the public but only used internally in the Combine framework. Regular developers will have to wait until the feature goes public, possibly after passing another round of feedback in the Swift Evolution process.

It certainly would have its benefits if a property wrapper knew the context of how its used (e.g. the name of the property or the type that’s using it). For example, a database could use the property name as the column name in a database table without requiring the user to provide a custom one (but possibly still allowing it).

We would highly appreciate making this feature available to everyone. We can only imagine a handful of great use cases for now but are equally excited for all of the use cases to come which haven’t been imagined yet.

Composition is tricky.

Especially when using mapping or asserting property wrappers, you might want to use multiple property wrappers at once. In the following code example, we would like to provide a property that is the opposite value of what is stored in a given UserDefaults value.

@AppStorage("isDarkModeEnabled") @Negated
var usesLightInterface: Bool

This kind of composition of property wrappers is not that easy since the wrappedValue of the AppStorage property wrapper would now be a Negated object. When chaining property wrappers, you are actually not using multiple wrappers on the same property but instead one wrapper that’s wrapping the other one. The order matters.

In smaller use cases, you might want to use a property wrapper inside your implementation of another property wrapper. Let’s just do that for the example above:

@propertyWrapper
struct NegatedAppStorage {

    @AppStorage
    private var storedValue: Bool

    init(wrappedValue: Bool, _ name: String) {
        self._storedValue = AppStorage(wrappedValue: wrappedValue, name)
    }

    var wrappedValue: Bool {
        get {
            return !storedValue
        }
        set {
            storedValue = !newValue
        }
    }

}

As you can see, this new property wrapper would be highly specific to this one case and reusing it for similar use cases would require writing a new property wrapper. So it seems obvious that property wrappers are not really made for composition.


Alternatives to Property Wrappers

As we have seen, property wrappers can be quite useful to write reusable code, but also bring some limitations with them that you need to consider. What are your alternative options and when should you use them instead?

Property Observers: willSet/didSet

Property wrappers can be used to ensure invariants, whether it is by asserting these conditions or making sure they are kept (as in the Clamping example). Often, you would want to ensure these invariants in properties that you would also like to wrap with another property wrapper. Since composing property wrappers is not trivial and using property wrapper structs on its own (i.e. without wrapping a property in them, only using their behavior) is not easy either, one is better served with property observers (willSet/didSet) for these cases.

Custom Getters & Setters: get/set

While writing custom property wrappers might not take a lot of time, it still takes more time than writing custom getters and setters for a computed property, especially when you would like to create a somewhat generic property wrapper. With a computed property, you have more control and it is easier to debug, since property wrappers often cause segmentation faults in the compiler. When a property wrapper would only be used in one or two places throughout your app or when it depends on many other properties of the same type, custom getters and setters might be the better choice.


Conclusion

As we have seen in this article, property wrappers can increase code reusability and reduce code complexity immensely. This effect is especially drastic when paired with the extensive use of the projectedValue and generics, making property wrappers quite powerful.

As of Swift 5.3, there’s still room for improvement in the implementation of property wrappers though. The compilation of custom property wrappers might sometimes lead to weird error messages and there are still quite some limitations to this feature, which will hopefully be resolved in the further evolution of the concept and the implementation.

You should always consider using willSet/didSet or custom getters/setters before implementing custom property wrappers. Many use cases cannot easily be abstracted and property wrappers are most useful for generic, often-needed, instant property access.

We are especially excited about the addition of property wrappers for developers of frameworks since the use of property wrappers can greatly simplify a framework’s API.

As a closing note: if your company needs a developer team to implement an iOS or Android app, reach out to us at contact@quickbirdstudios.com.

Share via
Copy link
Powered by Social Snap

Get notified when our next article is born!

(no spam, just one app-development-related article
per month)