Property wrappers in Swift
Basics article available: PropertiesWhen dealing with properties that represent some form of state, it’s very common to have some kind of associated logic that gets triggered every time that a value is modified. For example, we might validate each new value according to a set of rules, we might transform our assigned values in some way, or we might be notifying a set of observers whenever a value was changed.
In those kinds of situations, Swift 5.1’s property wrappers feature can be incredibly useful, as it enables us to attach such behaviors and logic directly to our properties themselves — which often opens up new opportunities for code reuse and generalization. This week, let’s take a look at how property wrappers work, and explore a few examples of situations in which they could be used in practice.
Transparently wrapping a value
Like the name implies, a property wrapper is essentially a type that wraps a given value in order to attach additional logic to it — and can be implemented using either a struct or a class by annotating it with the @propertyWrapper
attribute. Besides that, the only real requirement is that each property wrapper type should contain a stored property called wrappedValue
, which tells Swift which underlying value that’s being wrapped.
For example, let’s say that we wanted to create a property wrapper that automatically capitalizes all String
values that were assigned to it. That might be implemented like this:
@propertyWrapper struct Capitalized {
var wrappedValue: String {
didSet { wrappedValue = wrappedValue.capitalized }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue.capitalized
}
}
Note how we need to explicitly capitalize any string that was passed into our initializer, since property observers are only triggered after a value or object was fully initialized.
To apply our new property wrapper to any of our String
properties, we simply have to annotate it with @Capitalized
— and Swift will automatically match that annotation to our above type. Here’s how we might do that to ensure that a User
type’s firstName
and lastName
properties are always capitalized:
struct User {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
The cool thing about property wrappers is that they act completely transparently, which means that we can still work with our above two properties as if they were normal strings — both when initializing our User
type, and when modifying its property values:
// John Appleseed
var user = User(firstName: "john", lastName: "appleseed")
// John Sundell
user.lastName = "sundell"
Similarly, as long as a property wrapper defines an init(wrappedValue:)
initializer (like our Capitalized
type does) — then we can even natively assign default values to our wrapped properties, like this:
struct Document {
@Capitalized var name = "Untitled document"
}
So property wrappers enable us to transparently wrap and modify any stored property — using a combination of an @propertyWrapper
-marked type, and annotations matching the name of that type. But that’s just the beginning.
A property’s properties
Property wrappers can also have properties of their own, which enables further customization, and even makes it possible to inject dependencies into our wrapper types.
As an example, let’s say that we’re working on a messaging app that uses Foundation’s UserDefaults
API to store various user settings and other pieces of lightweight data on disk. Doing so typically involves writing some form of mapping code for synchronizing each value with its underlying UserDefaults
storage — which often needs to be replicated for each piece of data that we’re looking to store.
However, by implementing that sort of logic within a generic property wrapper, we could make it easily reusable — as doing so would let us simply attach our wrapper to any property that we’d like to be backed by UserDefaults
. Here’s what such a wrapper might look like:
@propertyWrapper struct UserDefaultsBacked<Value> {
let key: String
var storage: UserDefaults = .standard
var wrappedValue: Value? {
get { storage.value(forKey: key) as? Value }
set { storage.setValue(newValue, forKey: key) }
}
}
Just like any other struct, our above UserDefaultsBacked
type will automatically get a memberwise initializer with default arguments for all properties that have a default value — which means that we’ll be able to initialize instances of it by simply specifying which UserDefaults
key that we want each property to be backed by:
struct SettingsViewModel {
@UserDefaultsBacked<Bool>(key: "mark-as-read")
var autoMarkMessagesAsRead
@UserDefaultsBacked<Int>(key: "search-page-size")
var numberOfSearchResultsPerPage
}
The compiler will automatically infer the type of each of our properties, based on which type we’ve specialized our generic UserDefaultsBacked
wrapper with.
The above setup makes our new property wrapper easy to use whenever we want a property to be backed by UserDefaults.standard
, but since we parameterized that dependency, we could also choose to use a custom instance if we’d like — for example to facilitate testing, or to be able to share values between multiple apps within the same app group:
extension UserDefaults {
static var shared: UserDefaults {
let combined = UserDefaults.standard
combined.addSuite(named: "group.johnsundell.app")
return combined
}
}
struct SettingsViewModel {
@UserDefaultsBacked<Bool>(key: "mark-as-read", storage: .shared)
var autoMarkMessagesAsRead
@UserDefaultsBacked<Int>(key: "search-page-size", storage: .shared)
var numberOfSearchResultsPerPage
}
For more information about using UserDefaults
to share data between multiple apps, check out “The power of UserDefaults in Swift”.
However, our above implementation has a quite significant flaw. Even though both of our above two properties are declared as non-optional, their actual values will still be optionals, since our UserDefaultsBacked
type specifies Value?
as its wrappedValue
property’s type.
Thankfully, that flaw can be quite easily fixed. All that we have to do is to add a defaultValue
property to our wrapper, which we’ll then use whenever our underlying UserDefaults
storage didn’t contain a value for our property’s key. To enable such default values to be defined the same way that property defaults are normally defined, we’ll also give our wrapper a custom initializer that uses wrappedValue
as the external parameter label for our new defaultValue
argument:
@propertyWrapper struct UserDefaultsBacked<Value> {
var wrappedValue: Value {
get {
let value = storage.value(forKey: key) as? Value
return value ?? defaultValue
}
set {
storage.setValue(newValue, forKey: key)
}
}
private let key: String
private let defaultValue: Value
private let storage: UserDefaults
init(wrappedValue defaultValue: Value,
key: String,
storage: UserDefaults = .standard) {
self.defaultValue = defaultValue
self.key = key
self.storage = storage
}
}
With the above in place, we’re now able to turn both of our properties into non-optionals, like this:
struct SettingsViewModel {
@UserDefaultsBacked(key: "mark-as-read")
var autoMarkMessagesAsRead = true
@UserDefaultsBacked(key: "search-page-size")
var numberOfSearchResultsPerPage = 20
}
That’s really nice. However, some of our UserDefaults
values are likely to actually be optionals, and it would be unfortunate if we had to constantly specify nil
as the default value for those properties — as that’s not something that we have to do when not using property wrappers.
To address that, let’s also add a convenience API to our wrapper for whenever its Value
type conforms to ExpressibleByNilLiteral
(which Optional
does) — in which we’ll automatically insert nil
as the default value:
extension UserDefaultsBacked where Value: ExpressibleByNilLiteral {
init(key: String, storage: UserDefaults = .standard) {
self.init(wrappedValue: nil, key: key, storage: storage)
}
}
With the above change in place, we can now freely use our UserDefaultsBacked
wrapper with both optional and non-optional values with ease:
struct SettingsViewModel {
@UserDefaultsBacked(key: "mark-as-read")
var autoMarkMessagesAsRead = true
@UserDefaultsBacked(key: "search-page-size")
var numberOfSearchResultsPerPage = 20
@UserDefaultsBacked(key: "signature")
var messageSignature: String?
}
However, there is one more thing that we’ll need to take into account, since we’ll now be able to assign nil
to a UserDefaultsBacked
property. To avoid crashes in such situations, we’ll have to update our property wrapper to first check if any assigned value is nil
before proceeding to store it within the current UserDefaults
instance, like this:
// Since our property wrapper's Value type isn't optional, but
// can still contain nil values, we'll have to introduce this
// protocol to enable us to cast any assigned value into a type
// that we can compare against nil:
private protocol AnyOptional {
var isNil: Bool { get }
}
extension Optional: AnyOptional {
var isNil: Bool { self == nil }
}
@propertyWrapper struct UserDefaultsBacked<Value> {
var wrappedValue: Value {
get { ... }
set {
if let optional = newValue as? AnyOptional, optional.isNil {
storage.removeObject(forKey: key)
} else {
storage.setValue(newValue, forKey: key)
}
}
}
...
}
The fact that property wrappers are implemented as actual types gives us a lot of power — as we can give them properties, initializers, and even extensions — which in turn enables us to both make our call sites really neat and clean, and to make full use of Swift’s robust type system.
Decoding and overriding
Although most property wrappers are likely going to be implemented as structs in order to utilize value semantics, sometimes we might want to opt for reference semantics by using a class instead.
For example, let’s say that we’re working on a project that uses feature flags to enable testing and gradual rollouts of new features and experiments, and that we want to build a property wrapper that’ll let us specify such flags in different ways. Since we’ll want to share those values across our code base, we’ll implement that wrapper as a class:
@propertyWrapper final class Flag<Value> {
var wrappedValue: Value
let name: String
fileprivate init(wrappedValue: Value, name: String) {
self.wrappedValue = wrappedValue
self.name = name
}
}
With our new wrapper type in place, we can now start defining our flags as properties within an encapsulating FeatureFlags
type — which’ll act as a single source of truth for all of our app’s feature flags:
struct FeatureFlags {
@Flag(name: "feature-search")
var isSearchEnabled = false
@Flag(name: "experiment-note-limit")
var maximumNumberOfNotes = 999
}
At this point, the above Flag
property wrapper might seem a bit redundant, given that it doesn’t actually do anything other than store its wrappedValue
— but that’s about to change.
A very common way to use feature flags is to download their values over the network, for example every time that the app launches, or according to a certain time interval. However, even when using Codable, there’s typically a fair amount of boilerplate involved in making that happen — given that we’ll most likely want to fall back to our app’s default values for flags that may not have been added to our backend yet (or those that have been removed after a test or rollout was completed).
So let’s use our Flag
property wrapper to implement that form of decoding. Since we want to use each flag’s name
as its coding key, the first thing we’ll do is to define a new CodingKey
type that’ll let us do just that:
private struct FlagCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init(name: String) {
stringValue = name
}
// These initializers are required by the CodingKey protocol:
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = String(intValue)
}
}
Next, we’re going to need a way to reference each of our flags without knowing their generic type — but rather than resorting to full type erasure, we’re going to add a protocol called DecodableFlag
, which’ll enable each flag to decode its own value according to its Value
type:
private protocol DecodableFlag {
typealias Container = KeyedDecodingContainer<FlagCodingKey>
func decodeValue(from container: Container) throws
}
With the above in place, we’ll now be able to write our decoding code by making our Flag
type conditionally conform to our new DecodableFlag
protocol whenever its generic Value
type is decodable:
extension Flag: DecodableFlag where Value: Decodable {
fileprivate func decodeValue(from container: Container) throws {
let key = FlagCodingKey(name: name)
// We only want to attempt to decode a value if it's present,
// to enable our app to fall back to its default value
// in case the flag is missing from our backend data:
if let value = try container.decodeIfPresent(Value.self, forKey: key) {
wrappedValue = value
}
}
}
Finally, let’s complete our decoding implementation by making FeatureFlags
conform to Decodable
. Here we’ll use reflection to dynamically iterate over each of our flag properties, and we’ll then ask each flag to attempt to decode its value using the current decoding container, like this:
extension FeatureFlags: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: FlagCodingKey.self)
for child in Mirror(reflecting: self).children {
guard let flag = child.value as? DecodableFlag else {
continue
}
try flag.decodeValue(from: container)
}
}
}
While we did have to implement a bit of underlying infrastructure, we now have a very flexible feature flag system in place — with the ability to specify flag values both on the server and client-side, and for new flags to be defined simply by adding a @Flag
-annotated property to our FeatureFlags
type.
Projected values
As we’ve explored so far in this article, one of the major benefits of property wrappers is that they enable us to add logic and behaviors to properties in a way that doesn’t impact our call sites at all — as values are read and written the exact same way regardless of whether a property is wrapped or not.
However, sometimes we might actually want to access a property wrapper itself, rather than the value that it’s wrapping. That’s especially common when building UIs using Apple’s new SwiftUI framework, which makes heavy use of property wrappers to implement its various data binding APIs.
For example, here we’re building a QuantityView
that enables some form of quantity to be specified using a Stepper
view. In order to bind that piece of state to our view, we’ve annotated it with @State
, and we’re then giving our stepper direct access to that wrapped state (rather than just its current Int
value) by passing it prefixed with $
— like this:
struct QuantityView: View {
...
@State private var quantity = 1
var body: some View {
// Passing a wrapped property prefixd with "$" passes
// the property wrapper itself, rather than its value:
Stepper("Quantity: \(quantity)",
value: $quantity,
in: 1...99
)
}
}
The above feature might seem like something that was tailor-made for SwiftUI, but it’s actually a capability that can be added to any property wrapper, for example our Flag
type from before. That “dollar-prefixed” version of our above property is known as its wrapper’s projected value, and is implemented by adding a projectedValue
property to any wrapper type:
@propertyWrapper final class Flag<Value> {
var projectedValue: Flag { self }
...
}
Just like that, any Flag
-annotated property can now also be passed as a projected value — that is, as a reference to its wrapper itself. Again, that’s not something that’s coupled with SwiftUI, in fact we could adopt the same sort of pattern when using UIKit as well — for example by having a UIViewController
accept an instance of Flag
when initialized.
Here’s an example of how we might do just that to implement a view controller that’ll let us toggle a given Bool
-based feature flag on or off when using a debug build of our app:
class FlagToggleViewController: UIViewController {
private let flag: Flag<Bool>
private lazy var label = UILabel()
private lazy var toggle = UISwitch()
init(flag: Flag<Bool>) {
self.flag = flag
super.init(nibName: nil, bundle: nil)
}
...
override func viewDidLoad() {
super.viewDidLoad()
label.text = flag.name
toggle.isOn = flag.wrappedValue
toggle.addTarget(self,
action: #selector(toggleFlag),
for: .valueChanged
)
...
}
@objc private func toggleFlag() {
flag.wrappedValue = toggle.isOn
}
}
To initialize the above view controller, we’ll use the same $
-prefix-based syntax as when passing an @State
reference when using SwiftUI:
let flags: FeatureFlags = ...
let searchToggleVC = FlagToggleViewController(
flag: flags.$isSearchEnabled
)
We’ll definitely explore the above use of property wrappers more in upcoming articles — as it could enable us to make our code more declarative, to implement property-based observation APIs, to perform quite sophisticated data binding, and much more.
Conclusion
Property wrappers is definitely one of the most exciting new features in Swift 5.1 — as it opens up a lot of doors for code reuse and customizability, and enables powerful new ways to implement property-level functionality. Even outside of declarative frameworks like SwiftUI, there’s a ton of potential use cases for property wrappers, many of which won’t require us to make any big changes to our overall code — as property wrappers mostly operate completely transparently.
However, that transparency can be both an advantage and a liability. On one hand, it enables us to access and assign wrapped properties the exact same way as unwrapped ones — but on the other hand, the risk is that we’ll end up hiding too much functionality behind what might be a quite non-obvious abstraction.
What do you think? Have you started adopting property wrappers, or do you have a use case that you think they’d be a great fit for? Let me know — along with your comments, questions and feedback — either via Twitter or email.
Thanks for reading! 🚀