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 theAppStorage
property wrapper itself. - With
username
, you access thewrappedValue
property of the wrapper – in other words: It’s is equivalent to_username.wrappedValue
. - With
$username
, you can access theprojectedValue
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.