Swift Property Wrappers
Years ago, weremarked that the “at sign” (@
) — along with square brackets and ridiculously-long method names — was a defining characteristic of Objective-C. Then came Swift, and with it an end to these curious little 🥨-shaped glyphs …or so we thought.
At first, the function of@
was limited to Objective-C interoperability:@IBAction
,@NSCopying
,@UIApplication
, and so on. But in time, Swift has continued to incorporate an ever-increasing number of@
-prefixedattributes.
We got our first glimpse of Swift 5.1 atWWDC 2019 by way of the SwiftUI announcement. And with each “mind-blowing” slide came a hitherto unknown attribute:@State
,@Binding
,@Environment
…
We saw the future of Swift, and it was full of@
s.
We’ll dive into SwiftUI once it’s had a bit longer to bake.
But this week, we wanted to take a closer look at a key language feature for SwiftUI — something that will have arguably the biggest impact on the«je ne sais quoi» of Swift in version 5.1 and beyond:property wrappers
DelegatesWrappers
About PropertyProperty wrappers were firstpitched to the Swift forums back in March of 2019 — months before the public announcement of SwiftUI.
In his original pitch, Swift Core Team member Douglas Gregor described the feature (then called“property delegates”) as a user-accessible generalization of functionality currently provided by language features like thelazy
keyword.
Laziness is a virtue in programming, and this kind of broadly useful functionality is characteristic of the thoughtful design decisions that make Swift such a nice language to work with. When a property is declared aslazy
, it defers initialization of its default value until first access. For example, you could implement equivalent functionality yourself using a private property whose access is wrapped by a computed property, but a singlelazy
keyword makes all of that unnecessary.
Expand to lazily evaluate this code expression.
structStructure{// Deferred property initialization with lazy keywordlazyvardeferred=…// Equivalent behavior without lazy keywordprivatevar_deferred:Type?vardeferred:Type{get{ifletvalue=_deferred{returnvalue}letinitial Value=…_deferred=initial Valuereturninitial Value}set{_deferred=new Value}}}
SE-0258: Property Wrappers is currently in its third review (scheduled to end yesterday, at the time of publication), and it promises to open up functionality likelazy
so that library authors can implement similar functionality themselves.
The proposal does an excellent job outlining its design and implementation. So rather than attempt to improve on this explanation, we thought it’d be interesting to look at some new patterns that property wrappers make possible — and, in the process, get a better handle on how we might use this feature in our projects.
So, for your consideration, here are four potential use cases for the new@property
attribute:
- Constraining Values
- Transforming Values on Property Assignment
- Changing Synthesized Equality and Comparison Semantics
- Auditing Property Access
Constraining Values
SE-0258 offers plenty of practical examples, including@Lazy
,@Atomic
,@Thread
, and@Box
. But the one we were most excited about was that of the@Constrained
property wrapper.
Swift’s standard library offercorrect, performant, floating-point number types, and you can have it in any size you want — so long as it’s32 or64(or80) bits long(to paraphrase Henry Ford).
If you wanted to implement a custom floating-point number type that enforced a valid range of values, this has been possible sinceSwift 3. However, doing so would require conformance to a labyrinth of protocol requirements.
Pulling this off is no small feat, and often far too much work to justify for most use cases.
Fortunately, property wrappers offer a way to parameterize standard number types with significantly less effort.
Implementing a value clamping property wrapper
Consider the followingClamping
structure. As a property wrapper (denoted by the@property
attribute), it automatically “clamps” out-of-bound values within the prescribed range.
@property WrapperstructClamping<Value:Comparable>{varvalue:Valueletrange:Closed Range<Value>init(initial Valuevalue:Value,_range:Closed Range<Value>){precondition(range.contains(value))self.value=valueself.range=range}varwrapped Value:Value{get{value}set{value=min(max(range.lower Bound,new Value),range.upper Bound)}}}
You could use@Clamping
to guarantee that a property modelingacidity in a chemical solution within the conventional range of 0 – 14.
structSolution{@Clamping(0...14)varp H:Double=7.0}letcarbonic Acid=Solution(p H:4.68)// at 1 m M under standard conditions
Attempting to set pH values outside that range results in the closest boundary value (minimum or maximum) to be used instead.
letsuper Duper Acid=Solution(p H:-1)super Duper Acid.p H// 0
You can use property wrappers in implementations of other property wrappers. For example, thisUnit
property wrapper delegates to@Clamping
for constraining values between 0 and 1, inclusive.
@property WrapperstructUnit Interval<Value:Floating Point>{@Clamping(0...1)varwrapped Value:Value=.zeroinit(initial Valuevalue:Value){self.wrapped Value=value}}
For example, you might use the@Unit
property wrapper to define anRGB
type that expresses red, green, blue intensities as percentages.
structRGB{@Unit Intervalvarred:Double@Unit Intervalvargreen:Double@Unit Intervalvarblue:Double}letcornflower Blue=RGB(red:0.392,green:0.584,blue:0.929)
Related Ideas
- A
@Positive
/@Non
property wrapper that provides the unsigned guarantees to signed integer types.Negative - A
@Non
property wrapper that ensures that a number value is either greater than or less thanZero 0
. @Validated
or@Whitelisted
/@Blacklisted
property wrappers that restrict which values can be assigned.
Transforming Values on Property Assignment
Accepting text input from users is a perennial headache among app developers. There are just so many things to keep track of, from the innocent banalities of string encoding to malicious attempts to inject code through a text field. But among the most subtle and frustrating problems that developers face when accepting user-generated content is dealing with leading and trailing whitespace.
A single leading space can invalidate URLs, confound date parsers, and sow chaos by way of off-by-one errors:
importFoundationURL(string:" https://nshipster.com")// nil (!)ISO8601Date Formatter().date(from:" 2019-06-24")// nil (!)letwords=" Hello, world!".components(separated By:.whitespaces)words.count// 3 (!)
When it comes to user input, clients most often plead ignorance and just send everythingas-is to the server.¯\_(ツ)_/¯
.
While I’m not advocating for client apps to take on more of this responsibility, the situation presents another compelling use case for Swift property wrappers.
Foundation bridges thetrimming
method to Swift strings,which provides, among other things,a convenient way to lop off whitespacefrom both the front or back of aString
value.Calling this method each time you want to ensure data sanity is, however,less convenient.And if you’ve ever had to do this yourself to any appreciable extent,you’ve certainly wondered if there might be a better approach.
In your search for a less ad-hoc approach, you may have sought redemption through thewill
property callback…only to be disappointed that you can’t use thisto change events already in motion.
structPost{vartitle:String{will Set{title=new Value.trimming Characters(in:.whitespaces And Newlines)/* ⚠️ Attempting to store to property 'title' within its own will Set, which is about to be overwritten by the new value */}}}
From there, you may have realized the potential ofdid
as an avenue for greatness…only to realize later thatdid
isn’t calledduring initial property assignment.
structPost{vartitle:String{// 😓 Not called during initializationdid Set{self.title=title.trimming Characters(in:.whitespaces And Newlines)}}}
Setting a property in its owndid
callback thankfully doesn’t cause the callback to fire again, so you don’t have to worry about accidental infinite self-recursion.
Undeterred, you may have tried any number of other approaches… ultimately finding none to yield an acceptable combination of ergonomics and performance characteristics.
If any of this rings true to your personal experience, you can rejoice in the knowledge that your search is over: property wrappers are the solution you’ve long been waiting for.
Implementing a Property Wrapper that Trims Whitespace from String Values
Consider the followingTrimmed
struct that trims whitespaces and newlines from incoming string values.
importFoundation@property WrapperstructTrimmed{private(set)varvalue:String=""varwrapped Value:String{get{value}set{value=new Value.trimming Characters(in:.whitespaces And Newlines)}}init(initial Value:String){self.wrapped Value=initial Value}}
By marking eachString
property in thePost
structure below with the@Trimmed
annotation, any string value assigned totitle
orbody
— whether during initialization or via property access afterward — automatically has its leading or trailing whitespace removed.
structPost{@Trimmedvartitle:String@Trimmedvarbody:String}letquine=Post(title:" Swift Property Wrappers ",body:"…")quine.title// "Swift Property Wrappers" (no leading or trailing spaces!)quine.title=" @property Wrapper "quine.title// "@property Wrapper" (still no leading or trailing spaces!)
Related Ideas
- A
@Transformed
property wrapper that appliesICU transforms to incoming string values. - A
@Normalized
property wrapper that allows aString
property to customize itsnormalization form. - A
@Quantized
/@Rounded
/@Truncated
property that quantizes values to a particular degree (e.g. “round up to nearest ½”), but internally tracks precise intermediate values to prevent cascading rounding errors.
Changing Synthesized Equality and Comparison Semantics
This behavior is contingent on an implementation detail of synthesized protocol conformance and may change before this feature is finalized (though we hope this continues to work as described below).
In Swift, twoString
values are considered equal if they arecanonically equivalent. By adopting these equality semantics, Swift strings behave more or less as you’d expect in most circumstances: if two strings comprise the same characters, it doesn’t matter whether any individual character is composed or precomposed — that is,“é” (U+00E9 LATIN SMALL LETTER E WITH ACUTE
) is equal to“e” (U+0065 LATIN SMALL LETTER E
) +“◌́” (U+0301 COMBINING ACUTE ACCENT
).
But what if your particular use case calls for different equality semantics? Say you wanted acase insensitive notion of string equality?
There are plenty of ways you might go about implementing this today using existing language features:
- You could take the
lowercased()
result anytime you do==
comparison, but as with any manual process, this approach is error-prone. - You could create a custom
Case
type that wraps aInsensitive String
value, but you’d have to do a lot of additional work to make it as ergonomic and functional as the standardString
type. - You could define a custom comparator function to wrap that comparison — heck, you could even define your owncustom operator for it — but nothing comes close to an unqualified
==
between two operands.
None of these options are especially compelling, but thanks to property wrappers in Swift 5.1, we’ll finally have a solution that gives us what we’re looking for.
As with numbers, Swift takes a protocol-oriented approach that delegates string responsibilities across a constellation of narrowly-defined types.
For the curious, here's a diagram showing the relationship between all of the string types in the Swift standard library:
While youcould create your ownString
-equivalent type, thedocumentation carries a strong directive to the contrary:
Do not declare new conformances to StringProtocol. Only the
String
andSubstring
types in the standard library are valid conforming types.
Implementing a case-insensitive property wrapper
TheCase
type below implements a property wrapper around aString
/Sub
value.The type conforms toComparable
(and by extension,Equatable
)by way of the bridgedNSString
APIcase
:
importFoundation@property WrapperstructCase Insensitive<Value:String Protocol>{varwrapped Value:Value}extensionCase Insensitive:Comparable{privatefunccompare(_other:Case Insensitive)->Comparison Result{wrapped Value.case Insensitive Compare(other.wrapped Value)}staticfunc==(lhs:Case Insensitive,rhs:Case Insensitive)->Bool{lhs.compare(rhs)==.ordered Same}staticfunc<(lhs:Case Insensitive,rhs:Case Insensitive)->Bool{lhs.compare(rhs)==.ordered Ascending}staticfunc>(lhs:Case Insensitive,rhs:Case Insensitive)->Bool{lhs.compare(rhs)==.ordered Descending}}
Although the greater-than operator (>
)can be derived automatically, we implement it here as a performance optimization to avoid unnecessary calls to the underlyingcase
method.
Construct two string values that differ only by case, and they’ll returnfalse
for a standard equality check, buttrue
when wrapped in aCase
object.
lethello:String="hello"letHELLO:String="HELLO"hello==HELLO// falseCase Insensitive(wrapped Value:hello)==Case Insensitive(wrapped Value:HELLO)// true
So far, this approach is indistinguishable from the custom “wrapper type” approach described above. And this is normally where we’d start the long slog of implementing conformance toExpressible
and all of the other protocolsto makeCase
start to feel enough likeString
to feel good about our approach.
Property wrappers allow us to forego all of this busywork entirely:
structAccount:Equatable{@Case Insensitivevarname:Stringinit(name:String){$name=Case Insensitive(wrapped Value:name)}}varjohnny=Account(name:"johnny")letJOHNNY=Account(name:"JOHNNY")letJane=Account(name:"Jane")johnny==JOHNNY// truejohnny==Jane// falsejohnny.name==JOHNNY.name// falsejohnny.name="Johnny"johnny.name// "Johnny"
Here,Account
objects are checked for equality by a case-insensitive comparison on theirname
property value. However, when we go to get or set thename
property, it’s abona fideString
value.
That’s neat, but what’s actually going on here?
Since Swift 4, the compiler automatically synthesizesEquatable
conformance to types that adopt it in their declaration and whose stored properties are all themselvesEquatable
. Because of how compiler synthesis is implemented (at least currently), wrapped properties are evaluated through their wrapper rather than their underlying value:
// Synthesized by Swift CompilerextensionAccount:Equatable{staticfunc==(lhs:Account,rhs:Account)->Bool{lhs.$name==rhs.$name}}
Related Ideas
- Defining
@Compatibility
such that wrappedEquivalence String
properties with the values"①"
and"1"
are considered equal. - A
@Approximate
property wrapper to refine equality semantics for floating-point types (See alsoSE-0259) - A
@Ranked
property wrapper that takes a function that defines strict ordering for, say, enumerated values; this could allow, for example, the playing card rank.ace
to be treated either low or high in different contexts.
Auditing Property Access
Business requirements may stipulate certain controls for who can access which records when or prescribe some form of accounting for changes over time.
Once again, this isn’t a task typically performed by,say, an iOS app; most business logic is defined on the server, and most client developers would like to keep it that way. But this is yet another use case too compelling to ignore as we start to look at the world through property-wrapped glasses.
Implementing a Property Value Versioning
The followingVersioned
structure functions as a property wrapper that intercepts incoming values and creates a timestamped record when each value is set.
importFoundation@property WrapperstructVersioned<Value>{privatevarvalue:Valueprivate(set)vartimestamped Values:[(Date,Value)]=[]varwrapped Value:Value{get{value}set{defer{timestamped Values.append((Date(),value))}value=new Value}}init(initial Valuevalue:Value){self.wrapped Value=value}}
A hypotheticalExpense
class couldwrap itsstate
property with the@Versioned
annotationto keep a paper trail for each action during processing.
classExpense Report{enumState{casesubmitted,received,approved,denied}@Versionedvarstate:State=.submitted}
Related Ideas
- An
@Audited
property wrapper that logs each time a property is read or written to. - A
@Decaying
property wrapper that divides a set number value each time the value is read.
However, this particular example highlights a major limitation in the current implementation of property wrappers that stems from a longstanding deficiency of Swift generally:Properties can’t be marked asthrows
.
Without the ability to participate in error handling, property wrappers don’t provide a reasonable way to enforce and communicate policies. For example, if we wanted to extend the@Versioned
property wrapper from before to preventstate
from being set to.approved
after previously being.denied
, our best option isfatal
,which isn’t really suitable for real applications:
classExpense Report{@Versionedvarstate:State=.submitted{will Set{ifnew Value==.approved,$state.timestamped Values.map{$0.1}.contains(.denied){fatal Error("J'Accuse!")}}}}vartrip Expenses=Expense Report()trip Expenses.state=.deniedtrip Expenses.state=.approved// Fatal error: "J'Accuse!"
This is just one of several limitations that we’ve encountered so far with property wrappers. In the interest of creating a balanced perspective on this new feature, we’ll use the remainder of this article to enumerate them.
Limitations
Some of the shortcomings described below may be more a limitation of my current understanding or imagination than that of the proposed language feature itself.
Pleasereach out with any corrections or suggestions you might have for reconciling them!
Properties Can’t Participate in Error Handling
Properties, unlike functions, can’t be marked asthrows
.
As it were, this is one of the few remaining distinctions between these two varieties of type members. Because properties have both a getter and a setter, it’s not entirely clear what the right design would be if we were to add error handling — especially when you consider how to play nice with syntax for other concerns like access control, custom getters / setters, and callbacks.
As described in the previous section, property wrappers have but two methods of recourse to deal with invalid values:
- Ignoring them (silently)
- Crashing with
fatal
Error()
Neither of these options is particularly great, so we’d be very interested by any proposal that addresses this issue.
Wrapped Properties Can’t Be Aliased
Another limitation of the current proposal is that you can’t use instances of property wrappers as property wrappers.
OurUnit
example from before,which constrains wrapped values between 0 and 1 (inclusive),could be succinctly expressed as:
typealiasUnit Interval=Clamping(0...1)// ❌
However, this isn’t possible. Nor can you use instances of property wrappers to wrap properties.
letUnit Interval=Clamping(0...1)structSolution{@Unit Intervalvarp H:Double}// ❌
All this actually means in practice is more code replication than would be ideal. But given that this problem arises out of a fundamental distinction between types and values in the language, we can forgive a little duplication if it means avoiding the wrong abstraction.
Property Wrappers Are Difficult To Compose
Composition of property wrappers is not a commutative operation; the order in which you declare them affects how they’ll behave.
Consider the interplay between an attribute that performsstring inflection and other string transforms. For example, a composition of property wrappers to automatically normalize the URL “slug” in a blog post will yield different results if spaces are replaced with dashes before or after whitespace is trimmed.
structPost{…@Dasherized@Trimmedvarslug:String}
But getting that to work in the first place is easier said than done! Attempting to compose two property wrappers that act onString
values fails, because the outermost wrapper is acting on a value of the innermost wrapper type.
@property WrapperstructDasherized{private(set)varvalue:String=""varwrapped Value:String{get{value}set{value=new Value.replacing Occurrences(of:" ",with:"-")}}init(initial Value:String){self.wrapped Value=initial Value}}structPost{…@Dasherized@Trimmedvarslug:String// ⚠️ An internal error occurred.}
There’s a way to get this to work, but it’s not entirely obvious or pleasant. Whether this is something that can be fixed in the implementation or merely redressed by documentation remains to be seen.
Property Wrappers Aren’t First-Class Dependent Types
Adependent type is a type defined by its value. For instance, “a pair of integers in which the latter is greater than the former” and “an array with a prime number of elements” are both dependent types because their type definition is contingent on its value.
Swift’s lack of support for dependent types in its type system means that any such guarantees must be enforced at run time.
The good news is that property wrappers get closer than any other language feature proposed thus far in filling this gap. However, they still aren’t a complete replacement for true value-dependent types.
You can’t use property wrappers to, for example, define a new type with a constraint on which values are possible.
typealiasp H=@Clamping(0...14)Double// ❌funcacidity(of:Chemical)->p H{}
Nor can you use property wrappers to annotate key or value types in collections.
enumHTTP{structRequest{varheaders:[@Case InsensitiveString:String]// ❌}}
These shortcomings are by no means deal-breakers; property wrappers are extremely useful and fill an important gap in the language.
It’ll be interesting to see whether the addition of property wrappers will create a renewed interest in bringing dependent types to Swift, or if they’ll be seen as “good enough”, obviating the need to formalize the concept further.
Property Wrappers Are Difficult to Document
Pop Quiz: Which property wrappers are made available by the SwiftUI framework?
Go ahead and visitthe official SwiftUI docs and try to answer.
😬
In fairness, this failure isn’t unique to property wrappers.
If you were tasked with determining which protocol was responsible for a particular API in the standard library or which operators were supported for a pair of types based only on what was documented ondeveloper.apple.com
, you’re likely to start considering a mid-career pivot away from computers.
This lack of comprehensibility is made all the more dire by Swift’s increasing complexity.
Property Wrappers Further Complicate Swift
Swift is a much,much more complex language than Objective-C. That’s been true since Swift 1.0 and has only become more so over time.
The profusion of@
-prefixed features in Swift — whether it’s@dynamic
and@dynamic
from Swift 4,or@differentiable
and@memberwise
fromSwift for Tensorflow —makes it increasingly difficultto come away with a reasonable understanding of Swift APIsbased on documentation alone.In this respect,the introduction of@property
will be a force multiplier.
How will we make sense of it all? (That’s a genuine question, not a rhetorical one.)
Alright, let’s try to wrap this thing up —
Swift property wrappers allow library authors access to the kind of higher-level behavior previously reserved for language features. Their potential for improving safety and reducing complexity of code is immense, and we’ve only begun to scratch the surface of what’s possible.
Yet, for all of their promise, property wrappers and its cohort of language features debuted alongside SwiftUI introduce tremendous upheaval to Swift.
Or, as Nataliya Patsovska put it ina tweet:
iOS API design, short history:
- Objective C - describe all semantics in the name, the types don’t mean much
- Swift 1 to 5 - name focuses on clarity and basic structs, enums, classes and protocols hold semantics
- Swift 5.1 - @wrapped $path @yolo
Perhaps we’ll only know looking back whether Swift 5.1 marked a tipping point or a turning point for our beloved language.