Expert Swift v1.0.0
Expert Swift v1.0.0
Expert Swift
Ehab Amer, Marin Benčević, Ray Fix & Shai Mishali
Notice of Rights
All rights reserved. No part of this book or corresponding materials (such as text,
images, or source code) may be reproduced or distributed by any means without prior
written permission of the copyright owner.
Notice of Liability
This book and all corresponding materials (such as source code) are provided on an
“as is” basis, without warranty of any kind, express of implied, including but not
limited to the warranties of merchantability, fitness for a particular purpose, and
noninfringement. In no event shall the authors or copyright holders be liable for any
claim, damages or other liability, whether in action of contract, tort or otherwise,
arising from, out of or in connection with the software or the use of other dealing in
the software.
Trademarks
All trademarks and registered trademarks appearing in this book are the property of
their own respective owners.
raywenderlich.com 2
Expert Swift
raywenderlich.com 3
Expert Swift
raywenderlich.com 4
Expert Swift
raywenderlich.com 5
Expert Swift
raywenderlich.com 6
Expert Swift
raywenderlich.com 7
Expert Swift
raywenderlich.com 8
Expert Swift
raywenderlich.com 9
L Book License
• You are allowed to use and/or modify the source code in Expert Swift in as many
apps as you want, with no attribution required.
• You are allowed to use and/or modify all art, images and designs that are included
in Expert Swift in as many apps as you want, but must include this attribution line
somewhere inside your app: “Artwork/images/designs: from Expert Swift, available
at www.raywenderlich.com”.
• The source code included in Expert Swift is for your personal use only. You are NOT
allowed to distribute or sell the source code in Expert Swift without prior
authorization.
• This book is for your personal use only. You are NOT allowed to sell this book
without prior authorization, or distribute it to friends, coworkers or students; they
would need to purchase their own copies.
All materials provided with this book are provided on an “as is” basis, without
warranty of any kind, express or implied, including but not limited to the warranties
of merchantability, fitness for a particular purpose and noninfringement. In no event
shall the authors or copyright holders be liable for any claim, damages or other
liability, whether in an action of contract, tort or otherwise, arising from, out of or in
connection with the software or the use or other dealings in the software.
All trademarks and registered trademarks appearing in this guide are the properties
of their respective owners.
raywenderlich.com 10
Before You Begin
This section tells you a few things you need to know before you get started, such as
what you’ll need for hardware and software, where to find the project files for this
book, and more.
raywenderlich.com 11
i What You Need
• A Mac running macOS Catalina 10.15 or later, with the latest point release and
security patches installed. This is so you can install the latest version of the
required development tool: Xcode.
• Xcode 12.5 or later. You can download the latest version of Xcode for free from the
Mac App Store, here: apple.co/1FLn51R
If you haven’t installed the latest version of Xcode, be sure to do that before
continuing with the book. The code covered in this book depends on Swift 5.4 and
Xcode 12.5 — you may get lost if you try to work with an older version or work
outside the playground environment that this book assumes.
raywenderlich.com 12
ii Book Source Code &
Forums
• https://github.com/raywenderlich/advs-materials/tree/editions/1.0
Forums
We’ve also set up an official forum for the book at https://forums.raywenderlich.com/
c/books/expert-swift/78. This is a great place to ask questions about the book or to
submit any errors you may find.
raywenderlich.com 13
Expert Swift Dedications
— Ehab Amer
— Marin Bencevic
— Ray Fix
— Shai Mishali
raywenderlich.com 14
Expert Swift Dedications
raywenderlich.com 15
Expert Swift Dedications
Eli Ganim is the final pass editor for this book. He is an iOS
engineer from Israel, who’s passionate about teaching, writing and
sharing knowledge with others.
raywenderlich.com 16
v Introduction
Welcome to Expert Swift! The book for intermediate Swift developers looking to
enhance their skills and understanding of the language.
Swift is a powerful and wonderful language, used by many developers around the
world to build iOS, macOS, tvOS and watchOS apps. If you’re reading this book, then
you’ve probably built a few apps yourself.
Most of the books from raywanderlich.com are “By Tutorials”, however this book is a
little different. Since this is an advanced book, we didn’t build a tutorial to
accompany each chapter. Instead, we go deep by covering low-level concepts as well
as high-level abstractions, by using sample projects to illustrate how the advanced
topic might be used in real life.
You should expect some longer chapters. Trying to explain advanced topics, like the
ones this book covers, is not an easy task. Acquiring this advanced knowledge is
worth it though, and as you read through the book, you’ll find yourself wanting to
jump to your existing apps and implement the techniques you learn.
raywenderlich.com 17
Expert Swift Introduction
The chapters are independent, meaning you can read them in any order you’d like.
The only exception to this are the Protocols (chapter 3) and Generics (chapter 4)
chapters, which share one sample project.
Some chapters provide a starter project, while in some you’ll use playgrounds to
write code from scratch.
The content of this section will expose the inner workings of the type system, as well
as get you intimately familiar with protocols and generics.
As you’d expect from an advanced book, we don’t only explain these topics, but also
investigate how they’re built, how they’re represented, and how to use them
effectively.
We’ll cover topics like Higher order functions, Functional reactive programming,
Objective-C interoperability, using Instrumentation, and API design.
raywenderlich.com 18
Section I: Core Concepts
The first section of this book covers the basic building blocks of the Swift language:
The type system (enums, structs and classes), Protocols and Generics. We’ll start with
a brief refresher of each topic and then jump right into the behind-the-scenes
implementations.
The content of this section will expose the inner workings of the type system, as well
as get you intimately familiar with protocols and generics.
raywenderlich.com 19
1 Chapter 1: Introduction
By Ray Fix
In 2010, Chris Lattner typed mkdir shiny on his laptop, and what would ultimately
become the Swift language was born. Shiny started as a personal project he worked
on during evenings and weekends.
The project had many ambitious goals. Lattner has mentioned some of them in
interviews and podcasts, including:
• Defining as much of the language as possible in the library instead of the compiler.
But perhaps most of all, he wanted this new language to be “real”. Using it should
feel like using a scripting language but be suitable for everything from app
development to system-level programming. Half-jokingly, he has called this, “world
domination”.
raywenderlich.com 20
Expert Swift Chapter 1: Introduction
Swift released
Shiny transitioned to Swift and officially became real in the summer of 2014 at
Apple’s Worldwide Developer Conference (WWDC). It was real in the sense that, out
of the box, it had full interoperability with Objective-C and could leverage battle-
tested frameworks such as UIKit for building apps. The iOS community embraced it
quickly and wholeheartedly, with little exception.
The following year’s WWDC came with another seismic announcement. Swift would
become an open-source project and expand beyond Apple platforms. Apple made
good on that promise by the end of the year, setting up https://swift.org/, a website
to find toolchain downloads for platforms such as Linux and ultimately Windows as
well as discussion and links to complete source code with history hosted at GitHub
(https://github.com/apple/swift). Here, Apple and the community would continue to
develop Swift in the open.
Easy onboarding
It was imperative to the design of Swift that this one-liner compiled and run as a
valid program:
print("Hello, world!")
Despite being a strongly typed language that can catch all kinds of errors at compile-
time, you can write programs like this. Robust type inference and lack of boilerplate
make it pleasant to use and feel almost like a scripting language.
raywenderlich.com 21
Expert Swift Chapter 1: Introduction
Multi-paradigm
Apple demonstrated Swift’s modern language features at that first WWDC. In the
first live demo, they functionally mapped strings to SpriteKit images. In addition to
handling the intricacies of giant, legacy object-oriented frameworks, such as UIKit,
CoreData and SpriteKit, Swift can also speak functionally with sequences of values
using map, reduce and filter. Chapter 10, “Higher-Order Functions”, covers these
in detail.
Consider the problem of a sequence of numbers you want to add so you can compare
using imperative and functional styles.
You can follow along with the starter playground Multi-paradigm.playground. This
includes an example() function that scopes the example so you can use the same
variable names repeatedly in the same playground.
example("imperative") {
var total = 0
for value in numbers {
total += value
}
print(total)
}
As the example’s title suggests, this adds the numbers and stores the result in the
mutable variable total in an imperative style.
raywenderlich.com 22
Expert Swift Chapter 1: Introduction
example("functional") {
let total = numbers.reduce(0, +)
print(total)
}
Suppose the task is to add a number sequence from left to right but to stop when a
negative number appears. One possible, functional version might look like this:
example("functional, early-exit") {
let total = numbers.reduce((accumulating: true, total: 0))
{ (state, value) in
if state.accumulating && value >= 0 {
return (accumulating: true, state.total + value)
}
else {
return (accumulating: false, state.total)
}
}.total
print(total)
}
This code is more complex but calls the same reduce function and uses a tuple to
control whether values accumulate and keep the running total. It works, although
the compiler needs to work hard to figure out that an early exit is possible.
raywenderlich.com 23
Expert Swift Chapter 1: Introduction
Although you can solve this problem functionally in other ways (such as finding the
sub-sequence and then summing), you can do it in a straightforward way
imperatively:
example("imperative, early-exit") {
var total = 0
for value in numbers {
guard value >= 0 else { break }
total += value
}
print(total)
}
This code is easy to follow because it more directly describes the problem statement.
It also maps directly to actual computer hardware so the optimizer doesn’t have to
work nearly as hard.
One downside of the above code is that total leaks out as a mutable variable.
However, thanks to Swift’s mutation model, you can fix that:
This code wraps the imperative code in a closure and calls it to assign the outer
immutable total. In this way, Swift gives you “mutation when you need it.” It lets
you express algorithms with local mutation when that’s the most natural solution.
raywenderlich.com 24
Expert Swift Chapter 1: Introduction
1. Parse: Swift source code is first parsed into tokens and put into an abstract
syntax tree or AST. You can think of this being a tree in which each expression is
a node. The nodes also hold source location information so, if an error is
detected, the node can tell you exactly where the problem occurred.
2. Semantic Analysis (Sema): In this step, the compiler uses the AST to analyze
your program’s meaning. This is where type checking occurs. It passes the type-
checked AST to the SILGen phase.
raywenderlich.com 25
Expert Swift Chapter 1: Introduction
3. SILGen: This phase departs from previous compiler pipelines such as Clang,
which didn’t have this step. The AST gets lowered into Swift Intermediate
Language (SIL). SIL contains basic blocks of computation and understands
Swift Types, reference counting and dispatch rules. There are two flavors of SIL:
raw and canonical. Canonical SIL results from raw SIL run through a minimum
set of optimization passes (even when all optimizations are turned off). SIL also
contains source location information so it can produce meaningful errors.
4. IRGen: This tool lowers SIL to LLVM’s intermediate representation. At this point,
the instructions are no longer Swift specific. (Every LLVM-based uses this
representation.) IR is still quite abstract. Like SIL, IR is in Static single
assignment (SSA) form. It models machines as having an unlimited number of
registers, making it easier to find optimizations. It doesn’t know anything about
Swift types.
5. LLVM: This final step optimizes the IR and lowers it to machine instructions for a
particular platform. Backends (which output machine instructions) include ARM,
x86, Wasm and more.
The diagram above shows how the Swift compiler generates object code. Other tools,
such as source code formatters, refactoring tools, documentation generators and
syntax highlighters can tap into the intermediate results, such as the AST, making
the final results more robust and consistent.
Historical note: Before Apple adopted LLVM and Clang for Xcode’s compiler
technology, the syntax highlighting, document generation, debugging and
compiling all used different parsers. Most of the time, this worked fine. But
things could also get weird if they got out of sync.
raywenderlich.com 26
Expert Swift Chapter 1: Introduction
Overflow detection
Check out the power of SIL in action. Consider the following playground error:
Definite initialization
Swift is a safe language that by default makes it hard to access uninitialized memory.
SILGen provides guarantee through a checking process called definite initialization.
Consider this example:
func printTest() {
var printer: Printer
if .random() {
printer = Printer(value: 1)
}
else {
printer = Printer(value: 2)
}
printer.print()
}
printTest()
This code compiles and runs fine. But if you comment out the else clause, the
compiler correctly flags an error (Variable ‘printer’ used before being initialized)
thanks to the SIL. This error is possible because SIL understands the semantics of
method calls to Printer.
class Magic {
func number() -> Int { return 0 }
raywenderlich.com 27
Expert Swift Chapter 1: Introduction
func magicTest() {
let specialMagic = SpecialMagic()
let magic: Magic = specialMagic
number = magic.number()
}
This code is probably the most contrived example you’ll ever see for setting a
number. In the function magicTest, you create a SpecialMagic type and then assign
it to a base class reference and call number() to set the global number. Conceptually,
it uses the class’s virtual table to look up the correct function, which returns the
value 42.
Raw SIL
From a terminal window, change to the source directory where magic.swift lives and
run this command:
This runs the Swift compiler with optimization and creates raw SIL, outputting it to
the file *magic.rawsil.
Take a deep breath, don’t panic and open magic.rawsil in your text editor. If you
scroll down to the bottom, you find this definition of the function magicTest():
// magicTest()
sil hidden [ossa] @$s5magic0A4TestyyF : $@convention(thin) () ->
() {
bb0:
%0 = global_addr @$s5magic6numberSivp : $*Int // user: %14
%1 = metatype $@thick SpecialMagic.Type // user: %3
// function_ref SpecialMagic.__allocating_init()
%2 = function_ref @$s5magic12SpecialMagicCACycfC :
$@convention(method) (@thick SpecialMagic.Type) -> @owned
SpecialMagic // user: %3
%3 = apply %2(%1) : $@convention(method) (@thick
SpecialMagic.Type) -> @owned SpecialMagic // users: %18, %5, %4
debug_value %3 : $SpecialMagic, let, name "specialMagic" //
id: %4
%5 = begin_borrow %3 : $SpecialMagic // users: %9,
raywenderlich.com 28
Expert Swift Chapter 1: Introduction
%6
%6 = copy_value %5 : $SpecialMagic // user: %7
%7 = upcast %6 : $SpecialMagic to $Magic // users: %17,
%10, %8
debug_value %7 : $Magic, let, name "magic" // id: %8
end_borrow %5 : $SpecialMagic // id: %9
%10 = begin_borrow %7 : $Magic // users: %13,
%12, %11
%11 = class_method %10 : $Magic, #Magic.number : (Magic) -> ()
-> Int, $@convention(method) (@guaranteed Magic) -> Int // user:
%12
%12 = apply %11(%10) : $@convention(method) (@guaranteed
Magic) -> Int // user: %15
end_borrow %10 : $Magic // id: %13
%14 = begin_access [modify] [dynamic] %0 : $*Int // users:
%16, %15
assign %12 to %14 : $*Int // id: %15
end_access %14 : $*Int // id: %16
destroy_value %7 : $Magic // id: %17
destroy_value %3 : $SpecialMagic // id: %18
%19 = tuple () // user: %20
return %19 : $() // id: %20
} // end sil function '$s5magic0A4TestyyF'
This excerpt is the SIL definition of the three-line function magicTest(). The label
bb0 stands for basic block 0 and is a unit of computation. (If you had an if/else
statement, there would be two basic blocks, bb1 and bb2 created for each possible
path.) The %1, %2, etc. values are virtual registers. SIL is in Single Static Assignment
form so registers are unlimited and never reused. There are many more small details
that aren’t important to the discussion here. Reading through it, you should roughly
see how it’s allocating, assigning, calling and deallocating the objects. This expresses
the full semantics of the Swift language.
Canonical SIL
Canonical SIL includes optimizations, including a minimum set of optimization
passes when optimization is turned off with -Onone. Run this Terminal command:
This command creates the file magic.sil, which contains canonical SIL. Scroll toward
the end of the file to find magicTest():
// magicTest()
sil hidden @$s5magic0A4TestyyF : $@convention(thin) () -> () {
bb0:
%0 = global_addr @$s5magic6numberSivp : $*Int // user: %3
%1 = integer_literal $Builtin.Int64, 42 // user: %2
raywenderlich.com 29
Expert Swift Chapter 1: Introduction
This excerpt is a lot more concise than the raw SIL, even though it represents the
same thing. The main work is to store the integer literal 42 into a global address
location store %2 to %3 : $*Int. No classes are being initialized or de-initialized,
nor are any virtual methods being called. When you hear that structures use the
stack and classes use the heap, keep in mind this is a generalization.
In Swift, everything starts off being initialized on the heap, and a SIL analysis can
move the allocation to the stack or even get rid of it altogether. Virtual function calls
can also be devirtualized through the optimization process and called directly or
even inlined.
A great way to learn about some of the more esoteric features of Swift or gain a
better appreciation of some of the basic ones is to do this yourself — to build a
language-like feature. You’ll do that now.
Building ifelse
For this coding experiment, you’ll implement an ifelse() statement like the
statistical programming language R uses. The function looks like this:
raywenderlich.com 30
Expert Swift Chapter 1: Introduction
It does the same thing as the Swift ternary operator condition ? valueTrue :
valueFalse, which some don’t like because of aesthetic objections.
What’s wrong with this solution? Maybe nothing. If it solves your problem and you’re
working with only Int, this might even be a good place to stop. But because you’re
trying to make a general-purpose language feature for everyone, you can make
several improvements. First, refine the interface a little:
For a language construct that’s going to be used often, removing the argument labels
makes sense. The wildcard label _ gives you the ability to remove them. For brevity,
implement the feature in terms of the less verbose ternary operator. (You might
wonder why you shouldn’t use the camel-case name ifElse. There’s precedent for
keywords being simple concatenations, such as typealias and associatedtype, so
stay with the original R language naming.)
The next obvious problem is that this works only for Int types. You could replace it
with a lot of overloads for the important types you want:
raywenderlich.com 31
Expert Swift Chapter 1: Introduction
It’s easy to see this doesn’t scale. As soon as you think you’re done, there’s another
type your users want support for. And each overload repeats the implementation,
which is not great.
As an alternative, you could use the type Any, a type-erased, stand-in for any Swift
type:
This code works for any type, but there’s an important caveat you have to cast back
to the original type you want. Using the Any type doesn’t protect you from a
situation like this where you mix types:
This statement might work in testing but crash in production if the random number
comes up true. Any is super versatile but also error-prone to use.
A better answer, as you might have guessed, is to use generics. Change the code to
this:
raywenderlich.com 32
Expert Swift Chapter 1: Introduction
anymore
let value = ifelse(.random(), 100, 0)
This design both preserves type information and constrains the arguments to be the
same type as the return type. Generics are such an essential part of the Swift
language that Chapter 4, “Generics” is dedicated to them. You’ll use generics
throughout the book.
Note: The Swift standard library uses generics extensively to eliminate code
duplication, as in the example above. In some cases where the generic system
is not yet strong enough, the library uses a python script, gyb (or generate-
your-boilerplate), to generate the code for a family of types.
Deferring execution
The feature is looking good, but it’s still not done. Consider this usage:
If you run this, you see both functions are always called. As a language feature, you
would hope that only the expression you use gets evaluated. You can fix this by
passing a closure that defers execution:
raywenderlich.com 33
Expert Swift Chapter 1: Introduction
This code defers the execution but changes how you need to call the function.Now,
you have to call it like this:
Only one function gets called, but having to wrap your arguments in a closure is
pretty annoying. Fortunately, Swift has a way to fix it:
raywenderlich.com 34
Expert Swift Chapter 1: Introduction
This fails to compile because the autoclosures aren’t expecting a throwing closure.
Without any special help from the compiler, you might think to solve it by creating
another function version like this:
This code works, but the situation is worse than initially described. Suppose only the
first expression throws or suppose only the second throws. Do you need to make four
versions of the same function to handle all the cases? Because the keyword throws
does not figure into the signature of a Swift function, you would need to have four
flavors of ifelse, all with slightly different names.
Fortunately, there’s a better way. You can write one version of the function that
handles all these cases:
The key is using rethrows. Rethrows propagates the error of any failing closure to
the caller. If none of the closure parameters throw, it deduces the function is non-
throwing and doesn’t need to be marked with try.
raywenderlich.com 35
Expert Swift Chapter 1: Introduction
try expensiveFailingValue1(),
expensiveValue2())
You’re getting close to finishing ifelse. You don’t want to pay the cost of an extra
layer of abstraction, and the implementation will never change, so it makes sense to
mark the function @inlinable. This added keyword hints to the compiler that the
body of the method should be directly included in the client code without the
overhead of calling a function.
@inlinable
func ifelse<V>(_ condition: Bool,
_ valueTrue: @autoclosure () throws -> V,
_ valueFalse: @autoclosure () throws -> V)
rethrows -> V {
condition ? try valueTrue() : try valueFalse()
}
Note: There are stronger forms of @inlinable available privately. You’ll see
these if you browse the Swift source. One such attribute is @_transparent,
which always “sees through” to the underlying implementation. It will inline
even with -Onone and not include a stack frame when debugging. Check out
the details here: https://github.com/apple/swift/blob/main/docs/
TransparentAttr.md
Performance
One of the cool things about writing programs with an optimizing compiler is that
the abstraction cost of making code clear and maintainable is often nothing or close
to nothing.
To look at how you did here, put this code into a text file called ifelse.swift:
@inlinable
func ifelse<V>(_ condition: Bool,
_ valueTrue: @autoclosure () throws -> V,
_ valueFalse: @autoclosure () throws -> V)
rethrows -> V {
condition ? try valueTrue() : try valueFalse()
}
raywenderlich.com 36
Expert Swift Chapter 1: Introduction
return 200
}
}
Take this code and run the compiler on it directly with this command:
Take another deep breath and open the assembly file. Keep in mind these assembly
files contain a ton of boilerplate ceremony around calling conventions and entry
points. Don’t let that discourage you from looking. Trimming off the unneeded stuff,
here are the good parts:
_$s6ifelse0A5Test1SiyF:
:
callq _swift_stdlib_random
testl $131072, -8(%rbp)
movl $100, %ecx
movl $200, %eax
cmoveq %rcx, %rax
:
_$s6ifelse0A5Test2SiyF:
:
callq _swift_stdlib_random
testl $131072, -8(%rbp)
movl $300, %ecx
movl $400, %eax
cmoveq %rcx, %rax
:
_$s6ifelse0A5Test3SiyF:
:
callq _swift_stdlib_random
testl $131072, -8(%rbp)
movl $500, %ecx
movl $600, %eax
cmoveq %rcx, %rax
:
raywenderlich.com 37
Expert Swift Chapter 1: Introduction
These are the assembly instructions for your three test functions. It might look like
gibberish to you. The important thing is that this is the same gibberish for
ifelseTest1(), ifelseTest2() and ifelseTest3(). In other words, there’s zero
abstraction penalty for the three ways of writing the code. Choose what looks most
beautiful to you.
Now, demystifying the above assembly, the callq instruction calls the function to
get a random number. Next, the testl instruction gets the random number return
value (located at the address pointed to by the 64-bit base pointer - 8). It checks this
against 131072, which is 0x20000 or the 17th bit. If you look at the Swift source for
Bool.random, you find this:
@inlinable
public static func random<T: RandomNumberGenerator>(
using generator: inout T
) -> Bool {
return (generator.next() >> 17) & 1 == 0
}
That explains the 131072 mystery: It’s shifting over the 17th bit, masking it and
testing it all in one instruction. Next, the two possible outcome values of the
function are moved (using the movl instruction) into the registers cx and ax. The
prefix “e” stands for extended 32-bit versions of the registers. The rest of the bits are
zero-extended to fill all 64-bits. Finally, the “conditional move if equal” or cmoveq
instruction uses the result of the earlier test instruction to move the cx register to
the ax register. The prefix r on rcx and rax indicates you use the full 64-bits of the
registers.
raywenderlich.com 38
Expert Swift Chapter 1: Introduction
That completes the discussion and implementation of ifelse. It would be best if you
asked the question posed by Swift core team member John McCall: “Is this an
abstraction that pays for itself?” In this case, probably not. The ternary operator
already exists, which does essentially the same thing. Nevertheless, going through
this example hopefully reminded you of some of the capabilities Swift offers when
building language-like features as part of a library.
Key points
This chapter talked about some of the motivations for building the Swift language
and how Swift’s libraries and compiler work together to make powerful abstractions.
Here are some key takeaways:
• Swift aims to pick reasonable defaults, making undefined behavior hard to trigger.
• Swift embraces the idea of progressive disclosure. You only need to learn about
more advanced language features when you need them.
• Much of Swift is defined in its expressive, standard library and not as part of the
compiler.
• The Swift compiler phases are parse, semantic analysis, SILGen, IRGen and LLVM.
• Source location information resides in AST and SIL, making better error reporting
possible.
• Any is the ultimate type-erasure in Swift, but it can be error-prone to use. Generics
are usually a better alternative.
raywenderlich.com 39
Expert Swift Chapter 1: Introduction
• rethrow is a way to propagate errors from closures that might or might not be
marked throws.
• @inlinable hints to the compiler that the instructions for a function should be
emitted into the call site.
• Compilers remove much if not all the abstraction costs of your source code. If
different source code has the same semantics, the compiler likely will emit
identical machine instructions.
• Abstractions should pay for themselves. Think hard before creating new language
features.
Members of the Swift compiler team, including Chris Lattner, Slava Pestov, Joseph
Groff, John McCall and Doug Gregor, appear in LLVM conference talks about compiler
implementation. A good one to start with is http://bit.ly/swift-sil.
Finally, check out the online tool https://godbolt.org, which lets you edit code in the
web browser for many different languages (including Swift) and see how they lower.
You might want to experiment around with compiler flags -O, -Onone, -Ounchecked.
Under Output settings in the web interface, you might want to uncheck “Intel asm
syntax” to get assembly output like in this chapter.
raywenderlich.com 40
2 Chapter 2: Types &
Mutation
By Ray Fix
What’s a type? It’s a logical grouping of data along with a set of operations you can
perform with it. You use both standard types and custom types to build your
programs in Swift. The Swift compiler often can guarantee correctness using type
information in a process known as type checking before your program even runs.
Beginners, who are dealing with unfamiliar types such as Optional for the first time,
might find type checking errors burdensome and slightly mysterious. But by type-
checking your program, the type system makes sure you use the software correctly
and enables many optimizations. Swift’s type system is the key to safe and efficient
code. As you become a more advanced practitioner, your programming perspective
likely will become more type-centric.
Swift emphasizes mutable value semantics for its types. In this chapter, you’ll
review the important nominal types that Swift offers. Value types (enumerations and
structures), reference types (classes) and the mutation rules all work together to
allow mutable value semantics, which you can adopt in your own Swift types.
raywenderlich.com 41
Expert Swift Chapter 2: Types & Mutation
As discussed in the previous chapter, it’s pretty incredible that all the standard
library types such as Bool, Int, Double, String, Optional, Array and Dictionary
are clever compositions of these fundamental types. It speaks to the power of what
you can do with them.
Note: Protocols and generics also are amazing. This book has whole chapters
for protocol types and generic types because they’re so powerful and
important. You’ll also briefly look at types of types, or metatypes, in Chapter 4,
“Generics”.
In this chapter, you’ll explore the properties of named concrete types created from
classes, structures and enumerations. Although some of this might be a review, it will
provide a platform to explore some more advanced topics.
struct StructPoint {
var x, y: Double
}
class ClassPoint {
var x, y: Double
init(x: Double, y: Double) { (self.x, self.y) = (x, y) }
}
Both of these types model a point in the x-y plane. But already there are five
essential differences you should be aware of.
raywenderlich.com 42
Expert Swift Chapter 2: Types & Mutation
Note: If you define an initializer for a structure, the compiler won’t define the
member-wise one for you. A common trick is to define your initializer in an
extension if you want both the compiler-generated one and a custom one.
With reference semantics, you can affect one object from the other. Check out an
example by adding this to the end of your playground and running it:
raywenderlich.com 43
Expert Swift Chapter 2: Types & Mutation
Notice that you can modify the x coordinate with the class version even though let
introduces classPointB. The mutation control applies to the reference itself, not the
underlying property data.
The beautiful thing about declaring the x and y properties with var in StructPoint
is that they can be mutable if you declare the instance with var and immutable with
let. That is why you usually want to declare properties with var for structures,
because you can control mutability for each instance at the point of use.
raywenderlich.com 44
Expert Swift Chapter 2: Types & Mutation
Each thread of execution has its own stack, and stacks only change by modifying the
top-most element. As a result, allocating and deallocating onto a stack doesn’t
require expensive concurrency locks or fancy allocation strategies. Allocation and
deallocation can be performed with a single add or subtract instruction in a single
clock tick.
Here’s a diagram of how the above code might look like allocated in memory:
raywenderlich.com 45
Expert Swift Chapter 2: Types & Mutation
Note: Heap for classes vs. stack for structures and enumerations is just a
general rule of thumb. As you saw in the previous chapter, the Swift compiler
starts by allocating everything on the heap and then reasons about the object’s
lifetime to determine whether it can be allocated on the stack. For example, an
escaping closure that closes over a local structure will need to put that object
on the heap to extend the structure’s lifetime beyond its scope. On the other
hand, a class that is created, that performs some action and then goes out of
scope might be optimized away entirely and just include the instructions
necessary to complete the operation.
Note: It’s possible to give a value type identity by specifying a unique property
attribute. The Identifiable protocol, which adds a Hashable (and
Equatable) id property, does this. The SwiftUI framework defines property
wrappers, such as @State, which among other things imbue lifetime into
simple value types.
More differences
There are other differences between classes and structures that this simple example
doesn’t illuminate. The most glaring one is inheritance, which classes use to realize
runtime polymorphism. Classes dispatch their methods dynamically, whereas this
doesn’t happen for structures unless you’re using a protocol. Dispatch happens
statically for structure methods not part of a protocol. You’ll learn more about
protocol dispatch in the next chapter.
raywenderlich.com 46
Expert Swift Chapter 2: Types & Mutation
Note: You can mark methods in classes as final, which can have a side effect
of devirtualizing them and making them run faster. The compiler can use hints
from access control and whole module optimization to prove that a method
can’t be overridden and optimize it.
Defining a Point
Given the differences above, having a lightweight value representing your Point is
likely a good choice. Go with that design. Add this to the playground:
This defines Point, Size and Rectangle with Equatable conformance. For value
types, the compiler will generate the required == method for you if the stored
properties are also Equatable types. Reference types (aka classes) require you to
write == for Equatable and hash(into:) for Hashable yourself.
Note: Code synthesis happens during the type-checking phase of the compiler.
When you adopt a protocol, the compiler checks to see whether the type
fulfills (witnesses) the protocol. If it doesn’t, it typically emits an error. In the
special cases of Equatable, if the type is a value type, it will attempt to
synthesize == if all the stored properties are also Equatable. A similar process
happens for Hashable, Codable and CaseIterable. Unlike the others,
Codable synthesizes code for both value types and reference types.
raywenderlich.com 47
Expert Swift Chapter 2: Types & Mutation
Here are two simple methods for exchanging the x and y coordinates of a point. The
names of the methods follow the “fluent” usage of mutating and non-mutating pairs
described by the Swift API Design Guidelines (https://swift.org/documentation/api-
design-guidelines/).
The function flipped() uses self, and the function flip both uses and modifies
self. For that reason, you need to declare it mutating. Both functions contain the
exchanging logic, which is repetitious.
extension Point {
func flipped() -> Self {
Point(x: y, y: x)
}
The unnecessary references to self are gone, and the exchanging logic is only in
flipped. In this case, the implementation is trivial, so the duplication wasn’t a big
deal. But when you have non-mutating and mutating function pairs that are more
complicated, you’ll appreciate this pattern.
raywenderlich.com 48
Expert Swift Chapter 2: Types & Mutation
Note: Methods on classes (i.e., reference types) don’t use inout. If you think
about what self: inout Self does, that makes sense. inout on a reference
type would only prevent the entire instance from being reassigned to another
instance.
extension Point {
static var zero: Point {
Point(x: 0, y: 0)
}
This code creates a static property zero, which is just a point at the origin. The static
method random creates a random point bounded by the specified radius. The x value
is first pinned down, and you use the Pythagorean theorem to determine the
maximum bounds of allowed y values so it stays in the circle.
raywenderlich.com 49
Expert Swift Chapter 2: Types & Mutation
Going deterministic
Swift’s default Double.random(in:) uses SystemRandomNumberGenerator(), which
is cryptographically secure. This choice is a great default because it prevents would-
be attackers from guessing your random numbers.
Sometimes, you want your random values to be deterministic and repeatable. This
importance is especially true for continuous integration tests. You want these types
of tests to fail in response to a code change (bad merge or refactoring), not because
of a new, untried input value. Fortunately, the Swift standard library supports your
own generators with the overloaded method Double.random(in:using:), where the
using parameter takes a pseudo-random number generator of your choice.
Although the standard library doesn’t include one of these seedable pseudo-random
sources, it’s easy to make one yourself. There’s a lot of research about making “good”
random generators on the web. Here is a decent one from Wikipedia. The Permuted
Congruential Generator (https://en.wikipedia.org/wiki/
Permuted_congruential_generator) can be translated to Swift from the listed C code.
Add this to your playground:
init(seed: UInt64) {
state = seed &+ increment
_ = next()
}
}
raywenderlich.com 50
Expert Swift Chapter 2: Types & Mutation
This code contains some math details that aren’t important for this book. (However,
you will see more about C-style unsafe binary arithmetic such as &>>, &* and &+ in
Chapter 5, “Numerics & Ranges”.) The critical thing to notice is how you can mark
the internal details and state as private. As a user of this type, you only need to know
that it’s seeded with a 64-bit integer and that it produces a deterministic stream of
pseudo-random 64-bit integers. This hiding is encapsulation in action; it tames
complexity and makes the type easy to use and reason about. You’ll see
encapsulation used throughout this book and discussed further in Chapter 14, “API
Design Tips & Tricks”.
extension Point {
static func random(inRadius radius: Double,
using randomSource:
inout PermutedCongruential) -> Point {
guard radius >= 0 else {
return .zero
}
It’s quite like the previous version that uses the system random number generator.
As a static method, random(in:using:) also doesn’t touch an instance of Point. But
notice how mutable state can flow through the function because randomSource is an
inout parameter. This way to handle a side-effect via parameters is a much better
design than, say, using a global variable to track the pseudo-random state. It
explicitly surfaces the side-effect to the user, allowing it to be controlled.
raywenderlich.com 51
Expert Swift Chapter 2: Types & Mutation
These look like random numbers but are reproducible. The tenth random point will
always be Point(x: 0.43091531644250813, y: 0.3236366519677818) given a
starting seed of 1234.
Enumerations
Swift enumerations are another powerful value type that lets you model a finite set
of states. Add this to your playground:
This code creates an abstraction for quadrants in the two-dimensional plane. The
CaseIterable conformance lets you access an array, allCases. Hashable means you
can use it as an element of a Set or key of as the key of a Dictionary. You can make
the initializer failable because points on the x- or y-axis aren’t defined to be in a
quadrant. An optional initializer lets you document this possibility naturally.
raywenderlich.com 52
Expert Swift Chapter 2: Types & Mutation
Types as documentation
Types can serve as documentation. For example, if you have a function that returns
an Int, you don’t need to worry if the function will return 3.14159 or “Giraffe”. It just
can’t happen. In a sense, the compiler rules out all those crazy possibilities.
Historical note: One of the more famous software engineering failures came
in 1999 with the Mars Climate Orbiter. Engineers at the Jet Propulsion Lab in
California wrote their functions with metric impulse values measured in
newton-seconds. In contrast, engineers at Lockheed Martin Astronautics in
Colorado wrote their functions with English units of pound-seconds. Imagine
if the two groups had made units explicit with a type. Doing so might have
prevented the costly ($125M+) error that caused the space probe to skip off (or
burn up in) the Mars atmosphere.
Foundation has an extensible set of types for dealing with common physical units,
such as length, temperature and angle. Consider angles, which can be expressed in a
variety of units. Add this to the playground:
a + b // 180 degrees
The variable a is a right angle expressed in radians, and b is a right angle expressed
in degrees. You can add them together to see that they’re 180 degrees. The + operator
converts them to a base unit before adding the values.
Of course, Swift lets you define overloads of standard math functions. You can make
a type-safe version of cos() and sin().
cos(a) // 0
cos(b) // 0
raywenderlich.com 53
Expert Swift Chapter 2: Types & Mutation
sin(a) // 1
sin(b) // 1
The function takes an angle and converts it explicitly to radians before passing it to
the standard transcendental cos() and sin() functions. With this new API, the
compiler can check to make sure you’re passing angle types instead of something
nonsensical.
Note: Several popular frameworks take care of angle types. In addition to the
Foundation’s Measurement type, SwiftUI also defines an Angle that explicitly
initializes with degrees or radians. A generic version that abstracts across all
the different floating-point types is proposed for the official Swift numerics
package.
extension Angle {
init(radians: Double) {
self = Angle(value: radians, unit: .radians)
}
init(degrees: Double) {
self = Angle(value: degrees, unit: .degrees)
}
var radians: Double {
converted(to: .radians).value
}
var degrees: Double {
converted(to: .degrees).value
}
}
typealias gives you a shorter, descriptive spelling for angles. You can now go back
and improve your sin and cos implementations like this:
raywenderlich.com 54
Expert Swift Chapter 2: Types & Mutation
You’ll probably agree that those look much nicer. Now, you can define a polar
coordinate type:
Because you’ll want to flip between xy coordinates and polar coordinates easily, you
can add type converting initializers for those:
Notice how your abstractions build on one another, making an even more powerful
environment to work with. Your types are letting you hide complexity layer-by-layer.
Now, you can easily go from xy coordinates to polar coordinates and vice-versa like
this:
Strong types mean you can’t accidentally mix up polar coordinates and xy
coordinates, but you can still easily switch between the two when that is what you
intend.
raywenderlich.com 55
Expert Swift Chapter 2: Types & Mutation
Associated values
Enumerations in Swift are quite powerful because they let you associate information
with a particular case. For example, you can create a fixed set of shapes:
enum Shape {
case point(Point)
case segment(start: Point, end: Point)
case circle(center: Point, radius: Double)
case rectangle(Rectangle)
}
As you can see, it’s easy to compose types. You can succinctly model the valid states
in your app and even prevent invalid states from being representable and thus
compiling.
Using RawRepresentable
There’s another essential tool for your tool chest. You have probably used
RawRepresentable for enumerations without realizing it. Open the starter
playground RawRepresentable and add the following:
enum Coin {
case penny, nickel, dime, quarter
}
When you back the enumeration with an integer, character or string, it becomes
RawRepresentable thanks to compiler magic. Replace the previous definition with
this:
Being RawRepresentable means you can create and get the raw value. It also means
that the type is Equatable, Hashable and Codable for free.
raywenderlich.com 56
Expert Swift Chapter 2: Types & Mutation
You can use RawRepresentable directly to create simple checked types. Consider
this example:
init?(rawValue: String) {
guard rawValue.contains("@") else {
return nil
}
self.rawValue = rawValue
}
}
This function’s easier to use because the parameter labels make it clear where
message and recipient go and hard to misuse because of the specific types that the
compiler can check. The type for Email means that it’s only possible to pass around
well-formed email addresses. (For this example, the check simply looks to make sure
there is an @ in the address, but you can make it arbitrarily strict.)
Rather than having a property like isValid, it’s better if you can make your custom
type’s initializer failable either by returning nil or throwing a more specific error
when a valid instance can’t be created. This explicit failure mode allows you to set up
your code so the compiler forces you to check for errors. The reward is this: When
you write a function that uses a type, you don’t have to worry about half-baked
instances that might not be valid. This pattern pushes data validation and error
handling to your software stack’s upper layers and lets the lower levels run efficiently
without extra checks.
raywenderlich.com 57
Expert Swift Chapter 2: Types & Mutation
Exercises
Here are a few quick exercises to check your understanding. You can use the starter
playground Exercises to get you started. It imports the types you’ve made so far. As
always, it’s best to give it an honest try before looking at the answers in the final
version.
1. Generate 100 random points in the unit circle. How many does the second
quadrant contain? Demonstrate the solution with code. Use
PermutedCongruential with the seed 4321 to make your answer repeatable.
2. How many cups are in 1.5 liters? Use Foundation’s Measurement types to figure it
out.
QuadTree
Now, you’ll get a little more experience with types by building a QuadTree type for
CGPoints. This example will show you how to implement mutable value semantics
when using class types for storage.
A QuadTree is an efficient tree data structure for finding points in a region. Instead
of requiring O(n) to find points in a matching region, it only takes O(log n). It does
this by putting the points in nodes (or buckets). When a node reaches maximum
capacity and overflows, it creates new child nodes that split the space into four equal
parts. When it comes time to find points, you can binary search these nodes
efficiently.
The final demo app will let you add some points and then find points in a region by
dragging your finger around.
raywenderlich.com 58
Expert Swift Chapter 2: Types & Mutation
When drawn as a traditional tree, the QuadTree data structure looks something like
this:
raywenderlich.com 59
Expert Swift Chapter 2: Types & Mutation
Each node has either zero or four children. The QuadTree type itself is a lightweight
value type. It has a private property root to a reference type Node at the top of the
tree. It’s cheap to copy a QuadTree because copies share the Node reference.
Graphically, sharing looks something like this:
Copy-on-write storage
In this case, you make a copy before adding the new point. The node overflows its
capacity and subdivides itself into four new sub-nodes.
raywenderlich.com 60
Expert Swift Chapter 2: Types & Mutation
Implementing QuadTree
Begin the implementation of QuadTree by opening the QuadTree Xcode starter
project. Use the file navigator to familiarize yourself with the files in the project.
Skim through these five files without worrying too much about the details.
• ContentView.swift: The top-level user interface. It defines the picker for adding
or finding points, a place to show the number of points found and a clear button. It
also contains the drag gesture for inserting and finding points.
• QuadTree.swift: Stubs out the definition of QuadTree. This is where you’ll do your
work.
• QuadTreeView.swift: The canvas where points and rectangles are drawn. It finds
the size of the view and reports it to the view-model so points can be stored in
normalized coordinates ranging from zero to one.
You can build and run the app at this point, but you won’t yet be able to insert and
find points. To make that happen, you need to fill out the QuadTree type.
Inside the QuadTree definition, add the private nested class Node:
struct Quad {
// more to come...
}
}
raywenderlich.com 61
Expert Swift Chapter 2: Types & Mutation
The nested class Node will do the heavy lifting for QuadTree. Each node instance
keeps a region and can hold up to four points (the bucket size) before it spills over
and subdivides itself into four more nodes contained in Quad’s structure.
Note: The bucket size is set to four so you can easily visualize what is
happening. An actual implementation would probably have a much higher
bucket size based on an analysis of insertion and find times.
init(region: CGRect) {
let halfWidth = region.size.width * 0.5
let halfHeight = region.size.height * 0.5
northWest =
Node(region: CGRect(x: region.origin.x,
y: region.origin.y,
width: halfWidth, height: halfHeight))
northEast =
Node(region: CGRect(x: region.origin.x + halfWidth,
y: region.origin.y,
width: halfWidth, height: halfHeight))
southWest =
Node(region: CGRect(x: region.origin.x, y:
region.origin.y + halfHeight,
width: halfWidth, height: halfHeight))
southEast =
Node(region: CGRect(x: region.origin.x + halfWidth,
y: region.origin.y + halfHeight,
width: halfWidth, height: halfHeight))
}
// more to come...
This code defines the four sub-nodes of a Quad. The initializer is verbose, but all it’s
doing is dividing the parent region into four equal sub-regions.
raywenderlich.com 62
Expert Swift Chapter 2: Types & Mutation
You need to be able to make a deep copy Node, so add this initializer and copy
method to Quad:
Copying
The code above expects a copy() method on Node, so add that now to the body of
Node:
This function will resolve the four compiler errors. Somewhat amazingly, adding this
allows Node and Quad to recursively copy themselves from a root node, all the way
down the tree.
Next, add a helper method to subdivide a Node by adding this to the Node definition:
func subdivide() {
precondition(quad == nil, "Can't subdivide a node already
subdivided")
quad = Quad(region: region)
}
All this does is assign quad to an instance. The initializer that you wrote above does
the real work of rectangle division. The precondition makes sure you aren’t
subdividing a node that has already been subdivided. It’s always a good idea to check
your assumptions if it’s computationally cheap.
raywenderlich.com 63
Expert Swift Chapter 2: Types & Mutation
@discardableResult
func insert(_ point: CGPoint) -> Bool {
// 1
if let quad = quad {
return quad.northWest.insert(point) ||
quad.northEast.insert(point) ||
quad.southWest.insert(point) ||
quad.southEast.insert(point)
}
else {
// 2
if points.count == maxItemCapacity {
subdivide()
return insert(point)
}
else {
// 3
guard region.contains(point) else {
return false
}
points.append(point)
return true
}
}
}
This function returns a Bool that is true if it inserts the point and false if it
doesn’t.
1. First, it checks whether the node has been subdivided. If it has, the function
attempts to insert the point into each one of the nodes in the quad. The logical or
|| short-circuits and stops inserting as soon as it does.
2. If the node is at maximum capacity, it subdivides the node and attempts the
insert again.
3. If the node is not at maximum capacity, it first checks whether the point falls
inside the region the node is responsible for. If it does, it inserts it into the array
of points and returns true.
The last method you’ll define for Node is for finding points in a region. Add this
method to the Node type:
raywenderlich.com 64
Expert Swift Chapter 2: Types & Mutation
return []
}
var found = points.filter { searchRegion.contains($0) }
if let quad = quad {
found += quad.all.flatMap { $0.find(in: searchRegion) }
}
return found
}
This code first checks whether the search region overlaps with the region the node is
responsible for. If it doesn’t, it returns no points. This return is the base case of the
recursion. Next, it filters the points in the region and adds them to the found list.
Finally, if the node has been subdivided, it goes through the quads and recursively
calls find(in:) to add points to the found list before returning it.
The initializer of QuadTree specifies a region of space it handles. Replace the stubbed
out implementation with this:
init(region: CGRect) {
root = Node(region: region)
}
find(in:) simply delegates to the root node. points() gathers all the points by
finding them from the root node down.
raywenderlich.com 65
Expert Swift Chapter 2: Types & Mutation
Next, replace the placeholder for regions() that returns the region each Node is
responsible for:
@discardableResult
mutating func insert(_ point: CGPoint) -> Bool {
if !isKnownUniquelyReferenced(&root) {
root = root.copy()
}
guard root.insert(point) else {
return false
}
count += 1
return true
}
This function is marked with @discardableResult because clients might not wish
to check if the insertion succeeded. The only way it can fail is if you try to insert a
point that is outside the region specified by the QuadTree initializer.
This code is where the copy-on-write magic happens. Swift has a special function,
isKnownUniquelyReferenced(), which returns true if there is only one instance. If
there is more than one instance, you need to make a copy of the underlying tree.
Then, you can add the point.
The property count exists to make it cheap O(1) to know QuadTree’s point count
without recursively traversing all the nodes. It only gets incremented if the insertion
succeeds. The success of insertion depends on the original size of the region
QuadTree was initialized with.
raywenderlich.com 66
Expert Swift Chapter 2: Types & Mutation
Note: To maintain value semantics on mutation, you must make a deep copy
of the underlying storage for every mutating method. It’s an optimization not
to copy with the instance if it’s not shared. This optimization can be prevented
if extra copies are lying around. The SwiftUI framework has special property
wrappers @State and @Published used to manage UI updates. Unfortunately,
these wrappers make an extra copy that interferes with the
isKnownUniquelyReferenced optimization. If you look closely at
QuadTreeViewModel.swift, you’ll see quadTree is not a @Published
property but instead calls objectWillChange.send() directly to handle UI
update. Doing this prevents extra copying from happening, which will slow the
user interface after a few hundred points are added.
Build and run the app. Drag your finger around to add some points. If you tap to add
points one by one, you’ll see that a region subdivides itself on the fifth tap because
the node overflows maximum capacity. But finding points in a region is where
QuadTree shines. Rather than traversing the entire point list, it can focus on a
particular region quickly. You can try it out by switching to find mode and dragging
the little find box around. Finding points in a region quickly is why QuadTree is used
in collision detection and compression applications.
raywenderlich.com 67
Expert Swift Chapter 2: Types & Mutation
Key points
Swift is a strongly typed language that allows the compiler to check your program’s
correctness before it runs. The better you get at working with types, the easier it will
be to write correct programs.
Here are some key points to keep in mind from this chapter:
• Structures, enumerations and classes are the fundamental named types that Swift
uses to make every other concrete type, including Bool, Int, Optional, String,
Array, etc.
• Structures and enumerations are value types. Classes are reference types.
• The most straightforward way to get value semantics is to use a value type
(structure or enumeration) that only contains other types with value semantics.
• All the standard types, including String, Array, Set and Dictionary, already
have value semantics, making them easy to compose into larger types with value
semantics.
• Making a class immutable is one way to give reference types value semantics.
• Value types are generally copied around on the stack, whereas reference types are
allocated on the heap and are reference counted.
raywenderlich.com 68
Expert Swift Chapter 2: Types & Mutation
• The foundation Measurement types make working with different units less error-
prone by defining them as concrete types.
• Swift lets you improve the usage ergonomics for types you don’t even own.
raywenderlich.com 69
3 Chapter 3: Protocols
By Marin Benčević
If you’ve used Swift before, you’ve probably used protocols. A lot. Protocols and
protocol-oriented programming are built into the DNA of Swift, and it’s hard to
imagine what Swift would look like without all the power its protocols hold. Because
they’re an integral part of Swift, this whole chapter focuses on explaining how
protocols work and how you can leverage them in your code to produce clean, long-
lasting and easy-to-refactor APIs.
As a Swift developer, you probably already know the basics of protocols. Still, it’s a
good idea to go through a brief refresher on the basics of protocols as well as some of
their more rarely used features. Once you remind yourself of everything protocols
can do, you’ll look at how they work behind the scenes. You’ll also learn about
common patterns that use protocols as well as some useful gotchas and edge cases to
keep in mind.
You’ll do this by building a tiny RESTful networking library, which you’ll then use to
make an app that displays raywenderlich.com articles. Before you get your hands
dirty, you’ll first go through a brief refresher on Swift’s protocols.
raywenderlich.com 70
Expert Swift Chapter 3: Protocols
counter.increment(by: 10)
Say that counter is an object of a class called Counter, and you’re calling an
instance method called increment(by:). This instance method may or may not exist
in the class — maybe you forgot to write it. In a more dynamic language such as
Objective-C, the compiler will happily run the code and nothing will happen. Some
dynamic languages, such as JavaScript, will run the code but show an error saying
that increment does not exist. Swift, being a statically typed language, will first
check whether increment(by:) exists in that class, and if it doesn’t, won’t even run
your code. Although sometimes it seems like the compiler is complaining, really it’s
saving you from making stupid mistakes.
The compiler knows whether the method exists because it knows the type of
counter is Counter, and it can then query the Counter class for a matching
increment method. But there are cases where neither the compiler nor you is exactly
sure which type you’d like to use. What if you want to define a single function that
can increment different kinds of counters — not just your own Counter but also a
DoubleCounter, UserCounter, etc.?
What should be the type of counters? It doesn’t make sense to limit it to just
[Counter], because you want other types to work as well. You could try using [Any]
but you’d get an error — Swift can’t know if an instance of Any will have the
increment(by:) method. What you need is a way to tell the compiler “I want any
type that has an increment(by:) method.” That’s where protocols come in.
protocol Incrementable {
func increment(by: Int)
}
raywenderlich.com 71
Expert Swift Chapter 3: Protocols
By defining a protocol with a method requirement, you can use the protocol as a
type, saying “This method receives anything that implements Incrementable.”
When you write a concrete implementation of the protocol, Swift will verify that you
declared the increment(by:) method. Knowing this, the Swift compiler can
guarantee that your function will work for all instances of Incrementable.
Hiding information
Protocols are a tool of the type system that allows you to use multiple types as the
same super-type by hiding information about a type. For instance, you know that for
a square with sides a and b, a == b is true. You also know that a square is a
rectangle. By loosening the requirement that a == b, you can treat a square as a
rectangle and define a single way to calculate the area. That way, you won’t need to
write two implementations.
In the same way, protocols hide all other members of a class or struct, except the
ones exposed by the protocol. This allows you to use multiple types in the same
place.
Another way to look at protocols is as interfaces (in fact, many languages call them
that). Kitchen appliances, computers and phone chargers are quite different, but they
all turn the same interface toward the wall socket: a plug. All a socket knows is that
the device is something that needs power and has a plug. In the same way, if you
were modeling different devices in Swift, you could make a Pluggable protocol that
exposes a plug(into:) method, allowing the Socket to work with all possible
devices.
Encoding semantics
So far you read about how a type can conform to a protocol if it implements the
required methods and protocols. In Swift, there is another important aspect to
protocols: semantics. In programming, there are things that you can’t encode in the
function signature. For instance, you might want to sort some arrays. To allow for
different sorting algorithms, you implement a Sorter protocol with methods to sort
an array.
raywenderlich.com 72
Expert Swift Chapter 3: Protocols
However, you are working on an app with lots of data, and your algorithms need to be
very fast, so you might want to limit Sorter implementations to those with Big O
notation of O(n) or faster. Maybe you also require that the sorting algorithm is
stable. A stable algorithm keeps repeated elements in the same order after sorting.
These are all examples of things you can’t let the compiler check for you. Instead,
you have to add these requirements in type names and documentation, so you
change your protocol name to StableEfficientSorter.
If protocols were only method requirements, it would make it hard to write truly
generic functions since implementation details could completely change the way
your code works. Protocols are also used to describe semantic requirements. This is
exactly the reason why Swift doesn’t conform types to protocols automatically. When
writing code, you must consider which semantic requirements are important for your
protocols and clearly document them.
Protocol syntax
Now that you understand the semantics of protocols, it’s time for a brief refresher on
protocol syntax.
Note: For this section of the chapter, you don’t need to type along. But should
you wish to do so, start by creating a new empty playground that imports
UIKit and SwiftUI. Alternatively, you can find the code used for this section
under final/Protocols.playground.
enum Language {
case english, german, croatian
}
protocol Localizable {
static var supportedLanguages: [Language] { get }
}
The protocol has one required member, supportedLanguages. In this case, this is a
variable (not a function) that needs to have a getter. A setter is not required by the
protocol, which means computed variables or variables declared with the
private(set) access modifier will also satisfy the requirement. It is also marked as
static, which means that the conforming variable needs to be static as well.
raywenderlich.com 73
Expert Swift Chapter 3: Protocols
This protocol inherits from the one mentioned earlier. Inheritance in protocols
works similarly to classes: All the protocol’s requirements are passed onto its child
protocols. This means the ImmutableLocalizable protocol has two required
members: supportedLanguages and changed(to:). Unlike supportedLanguages,
changed(to:) is a function and must be declared as returning Self, the current
type. For instance, if you declare this function inside a class called FeedView, Self
will have the value of FeedView.
This protocol is very similar to the last one, but you’ll notice a new keyword:
mutating. You’re probably familiar with this keyword because you have to use it in a
struct’s method when the method’s body changes a property of that struct. In
protocols, it means the same thing. Structs need to implement this method by
making it mutating, but classes don’t. Classes are already mutable by default, so
there’s no need for an additional keyword.
Implementing protocols
Now that you’ve declared three protocols, it’s time to implement them in a few of
your types. If you’re used to other programming languages, you might be surprised to
hear that, in Swift, every type can implement a protocol. This includes structs,
classes and even enums!
Start by declaring a struct called Text, which will implement one of your protocols:
raywenderlich.com 74
Expert Swift Chapter 3: Protocols
You’ll see extensions used to conform a type to a protocol more often than not.
Doing so allows you to conform types outside your control, like the ones in SwiftUI or
UIKit, to your protocols. It also provides a nice way to structure your files so
everything protocol-related is within an extension, giving you an easier overview of
the file.
Extending protocols
Not all members of a protocol need to be required. You can provide a default
implementation of a protocol’s member by extending the protocol itself:
extension Localizable {
static var supportedLanguages: [Language] {
return [.english]
}
}
raywenderlich.com 75
Expert Swift Chapter 3: Protocols
Note: There are a few caveats to consider with extending protocols like this.
Later in this chapter, you’ll learn exactly how this extension works and a few
edge cases to be wary of.
So far, you have worked with protocols that any type in your code can implement.
You can limit your protocols to be conformed to only by classes. You do this by
inheriting the AnyObject protocol — a protocol that each class implicitly conforms
to.
Speaking of limiting the conformance to a protocol, you can also limit a protocol to
only subclasses of a specific class.
Protocols are abstract. But that doesn’t mean you can’t get as specific as required to
limit conformance to exactly the types you need.
Now that you have a bunch of types and protocols, you can use them in their full
glory. For instance, you can define a single function that works with all
MutableLocalizable types.
raywenderlich.com 76
Expert Swift Chapter 3: Protocols
func localize(
_ localizables: inout [MutableLocalizable],
to language: Language
) {
for var localizable in localizables {
localizable.change(to: language)
}
}
Note that you use an array of MutableLocalizable, which can be filled with any
combination of types as long as they all implement MutableLocalizable.
Protocols have immense power. But it’s hard to see just how useful they are until you
work on a large codebase and for a long time. Everything protocols offer can be
approximated in other ways, using inheritance, generics, function overloading or
copying and pasting code. What all those ways lack is clear intentions and semantics,
as well as protocol-guaranteed safety and ease of refactoring.
Now that you have the basics down, it’s time to get into some nitty-gritty protocol
details.
There are two main mechanisms for storing and calling functions: static and
dynamic dispatch. Static dispatch is fairly straightforward: It happens when you
know for sure that a function will never change. Static dispatch is used, among other
reasons, for global functions and methods declared in structs as well as methods on
final classes.
raywenderlich.com 77
Expert Swift Chapter 3: Protocols
In those cases, there is no function overriding to worry about, so the compiler can, in
a sense, hard-code the function’s address and hop to that whenever the function is
referenced.
Note: Aside from method dispatch, there is also a technique called inlining
that Swift uses extensively. Inlining replaces a function call with the full body
of that function at compile time. This is the fastest way to call a function, but
it’s only available with static dispatch and under specific conditions.
When you add pesky inheritance and protocols, things get a bit more complicated. A
method called on a non-final class instance can be declared in multiple possible
places. It can be declared inside the class, any of its parent classes, an extension or
even a protocol extension. This means the compiler can’t know ahead of time what
the exact address of a function will be. Instead, it uses something called the witness
table (sometimes also called the v-table or the virtual table).
As the compiler goes through your code, it will create a table for each class. This
table will have two columns: one for an offset in the table and one for the function at
that offset. Each function in the class is stored in the table, which is stored in your
working memory. A subclass will get a copy of its parent’s table and then replace the
rows of the method it wants to override. Now that a witness table is built, Swift can
use the table at runtime. When it encounters a method call, Swift knows which offset
in the table corresponds to that method.
This allows dynamic changing of the implementation of a method with the same
name, allowing features like inheritance, polymorphism and even protocols. But
these features come at a cost. Calling functions from table rows adds a constant
overhead for each function call. It also prevents inlining and other compiler
optimizations, making dynamic dispatch slower than static dispatch.
raywenderlich.com 78
Expert Swift Chapter 3: Protocols
Dispatch in protocols
The whole dispatch story is interesting, but you’re probably wondering how this all
relates to protocols. I mentioned that inheritance complicates the compiler’s life by
requiring dynamic dispatch. Well, protocols support inheritance, too. Aside from
that, multiple classes and structs can conform to the same protocol and implement
the same method. And then they can be used the same way, like in the examples
earlier in this chapter. If you’re using a protocol as the type of your instance, Swift
can’t know ahead of time if the Localizable instance will be UILabel or Text. So it
needs to dispatch methods dynamically.
Dispatching protocol methods is similar to how classes work. Every type that
implements a protocol gets its own protocol witness table. The table has two
columns again, one with the function and the other with the offset of that function.
Each member of the protocol (the methods and variables declared as the protocol
requirements) has its own row in the table. This table is then stored together with
each instance that implements a protocol. Swift can then, at runtime, look up the
correct function in the protocol witness table and call it. If you’re using a class
instance, Swift can look the function up in both the class and the protocol witness
table, dynamically finding the correct implementation.
protocol Greetable {
func greet() -> String
}
extension Greetable {
func greet() -> String {
return "Hello"
}
}
raywenderlich.com 79
Expert Swift Chapter 3: Protocols
Then, make an instance of your new struct and call its protocol method:
As expected, because GermanGreeter doesn’t implement its own greet method, the
line above prints out “Hello”. The way this works is that the default greet
implementation is copied to every type that conforms to the protocol and added to
their protocol witness table. Note that the protocol itself doesn’t have a table. Only
concrete types do.
One letter is all it takes to make it German. If you run the code again, it’ll print
“Hallo”. This happens because your new implementation replaced the extension
method in the protocol witness table. The same thing happens when you override a
method in a class.
This function lives inside the protocol extension, but it’s not declared as a
requirement of the protocol. However, every type implementing the protocol still has
access to the method. Verify this by calling the new method:
print(greeter.leave())
As expected, this outputs “Goodbye”. Translate this again by adding a new method in
GermanGreeter:
raywenderlich.com 80
Expert Swift Chapter 3: Protocols
return "Tschüss"
}
If you run the code again, it works just like before: The new method gets called and
the output is “Tschüss”. However, if you change the declaration of greeter to use the
protocol:
greet still outputs “Hallo”, but leave is now in English and outputs Goodbye!?
Swift seems to entirely bypass the function you declared in the struct and calls the
function from the protocol extension. Although this is unexpected, there’s a clear
reason why it happens. Your first hint is that the called function depends on the
variable’s declared type. This means polymorphism doesn’t work. Earlier, I
mentioned that dynamic dispatch enables polymorphism, so leave must be called
using static dispatch.
Indeed, extension methods rely entirely on static dispatch. There is no table involved
in calling leave — Swift calls it statically on the variable’s type. greet works as
expected because by adding it to the protocol’s requirements, you forced Swift to
create a protocol witness table entry for the method, enabling dynamic dispatch.
Existentials
Earlier, you defined a variable as a protocol:
raywenderlich.com 81
Expert Swift Chapter 3: Protocols
This code probably isn’t very surprising: You define variables all the time. However,
there is a big difference between using Greetable here and using a concrete type like
Int. Greetable, though it might look and act as a regular type, is called an
existential type. Although the name sounds fancy, it’s not that complex of a
concept: You can think of existential types as placeholders for a real, concrete type.
The compiler can interpret them as saying “There exists some type that conforms to
this protocol.” Existential types allow you to use protocols as types of method
parameters, array elements, variables and other data structures without thinking
about it.
There also are many less-known but useful ways you can use protocols. For instance,
you can compose multiple types into a single type using the & operator:
The greeter parameter from above must be a type that conforms to both Greeter
and Localizable. You can compose a struct type with protocols (Date & Codable),
a class type with protocols (UITableViewCell & Selectable) or multiple protocols
like above. You can also string together as many protocols as you want in the
composition. It’s not limited to only two members.
However, you can only use these composed types, also called non-nominal types, as
the type of a variable. You can’t, for instance, define an extension on Greeter &
Localizable. You can do that in other ways, though.
For instance, you can define an extension of all subclasses of a class that also
conform to a protocol:
raywenderlich.com 82
Expert Swift Chapter 3: Protocols
You can also drill down to extend generic types where their generic parameter
conforms to a protocol:
You can take that a step further and add conformance to a protocol for a generic type
whose generic parameter conforms to a protocol:
The extension above makes all arrays Localizable as long as their Element type is
also Localizable. This is exactly the way Swift makes an array of Codable items
also conform to Codable, an array of Equatable items conform to Equatable, etc.
You’ll discover more about these sorts of extensions in the next chapter, when you
learn about generics in Swift.
There are limitations to this for each protocol. Usually, Swift can generate
conformance to a protocol only if all your properties also conform to that protocol. In
the case of Hashable, for instance, all your properties need to be hashable for Swift
to synthesize the required methods.
raywenderlich.com 83
Expert Swift Chapter 3: Protocols
If you add a property that isn’t itself Hashable, Swift will complain and you’ll need
to add your implementation.
Protocol-oriented programming
Enough theory! It’s time to get your hands dirty and work with protocols to build an
easy to use networking library similar to Alamofire. In this part of the chapter, you’ll
use protocols to make an API nice and clean. In the next chapter, you’ll keep working
on your library but introduce generics to make it even easier to use.
Start by opening the starter project provided in this chapter’s materials. If you build
and run the project, you’ll see a list of one hard-coded article. You’ll change this app
so it downloads articles from the raywenderlich.com API, building your own little
learning platform! The app already contains the necessary UI in the Views group. It
also includes an ArticlesViewModel.swift file, which you’ll modify to load actual
data from the API. The model you’ll be downloading is inside Article.swift: All the
decoding code is already written for you.
By the end of this section, you’ll have a screen that fetches a list of articles, as well as
their images, from the raywenderlich.com API. You’ll also use dependency inversion
and testing to verify that your code works.
raywenderlich.com 84
Expert Swift Chapter 3: Protocols
Now that you’re familiar with the project, you’ll start working on it by defining a
protocol that represents a network request. Create a new Swift file called
Request.swift. Add an enum to the file:
According to the REST protocol, when you make a request, you need to tell the server
which HTTP REST method you’d like to use for the request. Here, you define a handy
enum that will make it easier to select the proper method.
protocol Request {
var url: URL { get }
var method: HTTPMethod { get }
}
Now, you can create your first Request implementation. You’ll create a struct that
encompasses a request to fetch a list of articles. Create a new Swift file called
ArticleRequest.swift, and add the following struct to the file:
You conform to the Request protocol by implementing its two required members.
For the URL, you return the appropriate raywenderlich.com API endpoint. Because
you’re just fetching a list, the proper method to use is GET.
raywenderlich.com 85
Expert Swift Chapter 3: Protocols
Create another Swift file called Networker.swift. Add the following to the file:
import Combine
class Networker {
func fetch(_ request: Request) -> AnyPublisher<Data, URLError>
{
var urlRequest = URLRequest(url: request.url)
urlRequest.httpMethod = request.method.rawValue
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.compactMap { $0.data }
.eraseToAnyPublisher()
}
}
First, you import Combine because you’ll use it to more easily manage the
asynchronous nature of network requests. You define a class called Networker with a
single method: fetch(_:). This method receives an implementation of your
Request protocol. It then creates a URLRequest based on the request and uses
URLSession to fetch the request. Instead of returning the requested data, fetch(_:)
returns a publisher of that data in the form of AnyPublisher<Data, URLError>.
You now have all the pieces to fetch the articles. All that’s left is to assemble those
pieces. Open ArticlesViewModel.swift and add a new property to the top of the
class:
The view model’s fetchArticles() gets called by the view as soon as it appears and
is supposed to load articles from the API. Replace its contents with the following
code:
raywenderlich.com 86
Expert Swift Chapter 3: Protocols
First, you create your article request and a JSON decoder. Next, you call fetch(_:) to
initiate the request. You wrote a pretty big chain of methods here, but it’s fairly
readable. fetch(_:) gives you a Publisher of Data, which you decode into
Articles, a helper struct that’s there to match the JSON structure of the API. You
then convert your Articles instance into an Article array in the map. After you
replace any error you get with an empty array, you switch to the main thread. Finally,
you assign the received results to the articles property. Because the property is
marked as @Published, the view will update itself automatically.
Whew! That’s a lot of explaining. The good news is, you can now build and run your
project to see a list of articles fetched from raywenderlich.com! If you don’t see any
content, wait for a few seconds while the data is being downloaded.
You should see a bunch of articles presented on your screen. You probably noticed
the titles and descriptions are all there but all the images are missing. You don’t have
to be Sherlock to figure out why: You never wrote any code to download images. It’s
time to fix that.
raywenderlich.com 87
Expert Swift Chapter 3: Protocols
Downloading images
To fetch the articles, you created a new Request implementation. Naturally, to
download images, you’ll do the same. Create a new Swift file called
ImageRequest.swift. Add the following struct to the file:
Instead of hard-coding a URL like in the article request, this time you’ll receive a URL
in the initializer. This will make this struct capable of loading any image on the
internet.
Next, head back to ArticlesViewModel.swift to initiate your new request. Each row
in the list will call fetchImage(for:) as soon as it appears. Add the following code
inside fetchImage(for:):
Because this function is called every time a row appears on the screen, it’s important
to keep performance in mind, in terms of both scrolling speed and saving the user’s
bandwidth. You’ll do this by caching the results. Once you download an image, store
it inside the Article object. Before downloading an image, first check that your
Article doesn’t already have an image downloaded. This will save you from
downloading the same image over and over as the user scrolls.
Still inside fetchImage(for:), continue with the following code to download the
image:
raywenderlich.com 88
Expert Swift Chapter 3: Protocols
This code is similar to the code you wrote in fetchArticles. This time, instead of
decoding JSON, you’re converting the data into UIImage and replacing any errors
with a nil image. You then store the image inside the article object.
You have images! It’s time to pat yourself on the back and take a breath, but you’re
only getting started. There’s a lot more power to be gained from using protocols.
protocol URLSessionDecodable {
init(from output: Data) throws
}
This protocol encompasses all structs that can be initialized from URLSessions’s
output, such as your Article.
However, you never actually decode a single Article object. You decode an array of
articles. That’s why, instead of conforming Article to your new protocol, you’ll
conform Array<Article>.
raywenderlich.com 89
Expert Swift Chapter 3: Protocols
This is about as close as you can get to black magic with Swift extensions. Here’s how
you should read the first line: “Extend Array to conform to URLSessionDecodable
when its Element is Article.” Earlier, you saw a similar extension when Element
conforms to a protocol. When you’re talking not about conformance but a concrete
type, you need to use the == operator to signify that element is that type, rather than
any subtype of that type.
Inside the extension, you implement the protocol by decoding the data into
Articles, and then turning that into an array of Article by calling a different
Array initializer.
You can now leverage your decoding by shortening the big method chain sausage in
ArticlesViewModel.swift. Change the body of fetchArticles to the following:
This new method should raise fewer eyebrows when someone else reads your code.
Build and run the project to make sure everything still works.
raywenderlich.com 90
Expert Swift Chapter 3: Protocols
However, before you do so, you first need to make your view model testable. A
testable class means that the class is decoupled from any outside dependencies that
might affect the test results. Like a true scientist in a lab, you need to remove all
outside variables that could mess with your results. In your case, the outside variable
is the internet.
You don’t want to connect to a real server in your tests. Your tests verify that your
code works. If the server is down or your machine doesn’t have a stable internet
connection, the tests should still succeed because your code is not the problem.
To decouple your view model from the Internet, you need to add a way to inject a
different networker, one that returns hard-coded data. This is usually called a mock
object.
protocol Networking {
func fetch(_ request: Request) -> AnyPublisher<Data, URLError>
}
Instead of using the Networker directly, your code will use Networking, a protocol
that anyone can implement. By doing so, you allow other objects to take Networkers
place, like the mock object mentioned above.
init(networker: Networking) {
self.networker = networker
}
raywenderlich.com 91
Expert Swift Chapter 3: Protocols
Instead of creating a Networker itself, your view model now receives a Networking
instance in its initializer. This is called dependency injection: It allows the
consumers of a class to inject the dependencies into a class. Anyone using the view
model can now create their own specific Networking implementation and give it to
the view model, including your test suite.
Dependency injection and dependency inversion are key principles to make your
class more decoupled from its dependencies and to make it testable.
You’ll also need to modify ArticlesView.swift to pass a networker to its view model.
Change the declaration of viewModel to this:
You’ve now completely decoupled your view model from any outside dependencies.
The project should build without any errors at this point.
This mock networker will implement the Networking protocol but return hard-coded
values for each request. Continue implementing fetch(_:) by adding the following:
switch request {
case is ArticleRequest:
let article = Article(
name: "Article Name",
description: "Article Description",
image: URL(string: "https://image.com")!,
id: "Article ID",
downloadedImage: nil)
let articleData = ArticleData(article: article)
let articles = Articles(data: [articleData])
outputData = try! JSONEncoder().encode(articles)
default:
raywenderlich.com 92
Expert Swift Chapter 3: Protocols
outputData = Data()
}
return Just<Data>(outputData)
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
Here, you create a publisher of the encoded data and return it from the method.
With a mock networker in place, you can use it to create a view model instance that
you’ll test. Add this line to the end of setUpWithError:
Before your tests run, you’ll create a new view model instance and inject your stub
networker.
Now, you can get to writing your test. There’s already a method called
testArticlesAreFetchedCorrectly. As the name suggests, the method will verify
that your view model fetches articles and decodes them correctly. Add these lines to
the method:
XCTAssert(viewModel.articles.isEmpty)
let expectation = XCTestExpectation(description: "Article
received")
You’ll start your test with a sanity check that the articles array is initially empty.
You’ll then set up an XCTestExpectation. Expectations are used to test
asynchronous code. The expectation will keep the test going until the expectation is
fulfilled or a timer runs out.
viewModel.$articles.sink { articles in
guard !articles.isEmpty else {
return
}
XCTAssertEqual(articles[0].id, "Article ID")
expectation.fulfill()
}
.store(in: &cancellables)
raywenderlich.com 93
Expert Swift Chapter 3: Protocols
This is the main part of your test. Here, you subscribe to changes to the articles array
and wait until it’s not empty. You then grab the first element of the array and verify
that the ID matches the one from MockNetworker. If it does, you know that the view
model decoded the article correctly. You also fulfill your expectation, telling the test
it can stop waiting.
viewModel.fetchArticles()
wait(for: [expectation], timeout: 0.1)
You call the fetchArticles method to start the fetching and decoding process. You
also tell XCTest to wait for 0.1 seconds until your expectation is fulfilled.
In the Test navigator as well as next to the method you just wrote, you should see a
green checkmark, indicating that your test succeeded. Maybe this was the first test
you ever wrote! In that case, welcome to a whole new world of having confidence in
your code, enabled by clever use of Swift’s protocols!
Delegating work
If you’ve worked with Swift and iOS for a while, I’m sure you’ve implemented at least
one delegate property. Delegates are used across Apple’s APIs as well as a plethora of
community libraries for a variety of tasks:
When designing your APIs, especially if you have a complex object that performs lots
of complicated work, adding delegation is a good way to make sure your class
remains flexible enough to be used in a variety of contexts in the future.
raywenderlich.com 94
Expert Swift Chapter 3: Protocols
Open Networker.swift and add the following protocol to the top of the class:
func networking(
_ networking: Networking,
transformPublisher: AnyPublisher<Data, URLError>
) -> AnyPublisher<Data, URLError>
}
Delegates are plain old Swift protocols. But keep in mind a couple of additional
pieces of convention when making delegate properties. You defined the
NetworkingDelegate as inheriting from AnyObject. As mentioned earlier in this
chapter, this restricts the protocol so only classes can conform to it. Because
delegates typically are used to affect the behavior of class instances, it makes sense
that the delegate is a class as well.
You also might have noticed the slightly weird function signatures. By convention
led by Apple’s APIs, delegate methods have specific signatures that you should
follow. Methods that return a value are named after the value they return, and they
receive the source object (the Networking instance in this case) as their first
parameter. Following this convention, you define a method that returns the HTTP
headers that Networking will use for all of its requests.
Methods that perform side effects, on the other hand, are named after the delegate’s
source object (networking). The first parameter is usually that source object, while
the second parameter gives you a clearer idea of what the delegate does. In this case,
you define a function that can perform additional processing on the URLSession
publisher before the Networking instance returns it to its consumer.
extension NetworkingDelegate {
func headers(for networking: Networking) -> [String: String] {
[:]
}
func networking(
_ networking: Networking,
transformPublisher publisher: AnyPublisher<Data, URLError>
) -> AnyPublisher<Data, URLError> {
publisher
raywenderlich.com 95
Expert Swift Chapter 3: Protocols
}
}
There are two implementations of Networking in your code. The first is right below
the protocol you just defined, Networker. Add the delegate property to the class:
weak references
You’ll notice the variable is marked as weak. By default, Swift always keeps strong
references between objects. Strong references guarantee that the delegate won’t get
de-initialized while the Networking instance is using it. A weak reference has no
such guarantees: The delegate can get deallocated even if the Networking is
keeping a reference to it. In the case of delegates, you almost always want the latter
behavior.
The reason is to avoid reference cycles. Think of the following example: Your
ArticlesViewModel owns a Networker instance. It also sets itself as the
Networker’s delegate, which means that the Networker now has a reference to the
view model.
You now have a case where both classes have a strong reference to each other.
Networker won’t get deallocated unless ArticlesViewModel is deallocated, and vice
versa. This means that, even if you set the view model (or the networker) to nil, it
will still hang around in memory. This is why you have to make one of those
references weak. In that case, as far as Swift’s memory management is concerned,
there’s only one reference between the two objects. Therefore, deleting one object
will trigger the deletion of the other object, completely freeing up memory.
raywenderlich.com 96
Expert Swift Chapter 3: Protocols
You make sure to ask the delegate for the headers to add to the URLRequest.
Instead of returning the publisher directly, you store it in a variable. After creating
the publisher, you then call the delegate method to transform the publisher if there
is a delegate. If there isn’t, you directly return the publisher.
You make the view model the delegate. You also implement the headers(for:)
method, where you return a valid Content-Type header that the API expects.
func networking(
_ networking: Networking,
transformPublisher publisher: AnyPublisher<Data, URLError>
) -> AnyPublisher<Data, URLError> {
publisher.receive(on:
DispatchQueue.main).eraseToAnyPublisher()
}
raywenderlich.com 97
Expert Swift Chapter 3: Protocols
There’s one final step in making the view model the delegate. Add a line of code at
the bottom of init(networker:):
self.networker.delegate = self
This sets the view model as networker’s delegate. Now that you transform each
publisher inside the delegate, you can remove the two .receive(on:
DispatchQueue.main) lines in fetchArticles and fetchImage(for:).
Run the project one last time and make sure everything works as it worked before.
raywenderlich.com 98
Expert Swift Chapter 3: Protocols
Key points
• Protocols allow you to abstract away information from concrete types, letting
you use more types in the same place.
• You can extend existing types to conform to protocols using extensions and even
limit conformance to specific subtypes.
• Protocols use dynamic dispatch to achieve function calls, except for methods
defined in protocol extensions that are not declared as a protocol requirement.
• Using dependency inversion with dependency injection can make your code more
testable.
• Add delegates to complex classes to give them more flexibility. When doing so,
keep strong reference cycles in mind and use weak references to avoid them.
If you want to learn more about Swift’s underbelly and how things are arranged in
memory, the Understanding Swift Performance WWDC 2016 session (apple.co/
3n01j0w) will clear up a lot of questions you have. If you want to learn more about
testing your code, take a look at the Testing in iOS video course (bit.ly/38QLuUG).
Finally, if you’d like to know more about the raywenderlich.com API, check out its
documentation (bit.ly/3puWr58).
In the next chapter, you’ll continue to work on your networking library but add
generics into your toolbox to make the library even better, so keep reading!
raywenderlich.com 99
4 Chapter 4: Generics
By Marin Benčević
Almost everyone using Swift – from complete beginners to seasoned veterans – has
used generics, whether they know it or not. Generics power arrays and dictionaries,
JSON decoding, Combine publishers and many other parts of Swift and iOS. Because
you have already used many of these features, you know firsthand how powerful
generics are. In this chapter, you’ll learn how to harness that power to build
generics-powered features.
You’ll get intimately familiar with generics by continuing to work on the networking
library you started in the previous chapter. This time, you’ll modify it to use generics
to create a nicer API. You’ll learn how to write generic functions, classes and structs,
how to use protocols with associated types, what type erasure is and how to put all
that together to make a coherent API.
Before you do that, though, this chapter front-loads quite a bit of theory, giving you a
reference of what generics have to offer. Don’t worry — you’ll get your hands dirty
later in the chapter!
raywenderlich.com 100
Expert Swift Chapter 4: Generics
The one special bit of syntax that makes this function generic is <T> in the function’s
prototype. While parentheses (()) surround the function’s parameters, angle
brackets (‹›) surround the function’s type parameters. A generic function receives
type parameters as part of a function call, just like it receives regular function
parameters.
In this case, there is only one type parameter, called T. The name of this parameter
isn’t some magic, special constant — it’s user-defined. In the example above, you
could have used Element or anything else. Once you define the type parameter inside
the angle brackets, you can use it in the rest of the function’s declaration and even
inside the function’s body.
When you call this function, Swift replaces T with a concrete type that you’re calling
the function with.
In the function’s prototype, you defined that the function receives an array of
optional Ts as well as another T value. When you call the function with numbers,
Swift knows that numbers has a type of [Int?] and can figure out that it needs to
replace T with Int. Swift is smart like that.
This allows you to create a single function that works across all possible types,
saving you from having to copy and paste functions. In a sense, generics are the
opposite of protocols. Protocols allow you to call a function on multiple types where
each type can specify its implementation of the function. Generics allow you to call a
function on multiple types with the same implementation of that function.
raywenderlich.com 101
Expert Swift Chapter 4: Generics
Note: When your function is very generic, and the type can be any type, it’s
fine to use single letter type parameter names like T and U. But more often
than not, your type parameter will have some sort of semantic meaning. In
those cases, it’s best to use a more descriptive type name that hints at its
meaning to the reader. For example, instead of using single letters, you might
use Element, Value, Output, etc.
Of course, like regular parameters, you can have multiple comma-separated type
parameters:
However, sometimes instead of a function that works across all possible types, you
want one that works across only some of them. Swift allows you to add constraints to
your generic types:
In the example above, you need the ability to compare two values with the >
operator. Not every type in Swift can be compared (for example, is one View larger
than another?). So you need to specify that T must conform to Comparable. Swift
then knows there’s a valid implementation of > for T. You’re using Comparable as a
generic constraint: a way to tell Swift which types are accepted for the generic type
parameter.
Generic types
Generic functions can take you only so far. At some point, you run into cases where
you need a generic class or struct. You already use generic types all the time: Arrays,
dictionaries and Combine publishers are all generic types.
struct Preference<T> {
let key: String
raywenderlich.com 102
Expert Swift Chapter 4: Generics
var value: T? {
get {
UserDefaults.standard.value(forKey: key) as? T
} set {
UserDefaults.standard.setValue(newValue, forKey: key)
}
}
}
Like generic functions, generic types also have type parameters, declared right next
to the type name. The example above shows a generic struct that can store and
retrieve any type from UserDefaults.
You can provide a concrete type to a generic type by writing the type inside angle
brackets next to the type’s name:
Here, Swift replaces T with Float. The process of replacing a type parameter with a
concrete type value is called specialization. In this case, typing <Float> is
necessary because Swift doesn’t have a way to infer it. In other cases, when you use
the type parameter in the initializer, Swift can figure out what your concrete type is
without you having to write angle brackets.
Keep in mind, though, that Preference itself is not a type. If you try to use
Preference as the type of a variable, you get a compiler error. Swift recognizes only
specialized variants of the type, such as Preference<String>, as real types. Generic
types by themselves are more like a blueprint: a type of scaffolding for you but not of
much use for the compiler.
protocol Request {
associatedtype Model
func fetch() -> AnyPublisher<Model, Error>
}
raywenderlich.com 103
Expert Swift Chapter 4: Generics
In most cases, Swift can figure out the associated type, so you don’t need to add a
typealias as long as you use the type when implementing one of the protocol’s
methods:
In the example above, Swift sees that you use String in the place of Model, so it can
infer that String is the associated type.
Like generic types, PATs are not types! If you don’t believe me, try using a PAT as a
type:
The error tells you that Request can be used only as a generic constraint, which is
mentioned earlier in this section. Implied in that sentence is that it cannot be used as
a type. The reason has to do with how Swift handles generic types. Swift needs to
have a concrete type to work with at compile-time so it can save you from errors and
undefined behavior while your program is running. A generic type without all of its
type parameters is not a concrete type. Depending on the type parameters, method
and property implementations can change, and the object itself can be laid out
differently in memory. Because Swift always errs on the side of caution, it forces you
to always use concrete, known types in your code.
It’s good to be safe, but how the heck do you define an array of PATs, then? The
answer is type erasure, and you’ll see an example later in this chapter.
raywenderlich.com 104
Expert Swift Chapter 4: Generics
Extending generics
In the previous chapter, you’ve seen all the ways you can extend protocols and the
types that implement them. Generics are no different! First of all, you can extend
generics as any other type, with the added benefit that you get access to the type
parameters inside the extension:
extension Preference {
mutating func save(from untypedValue: Any) {
if let value = untypedValue as? T {
self.value = value
}
}
}
In the example above, you have access to Preferences type parameter T, which you
use to cast the received value.
Like protocol extensions, you can also constrain extensions of generic types. For
instance, you can constrain an extension to only generic types where the type
parameter implements a protocol:
In the code above, save will only exist on Preferences where the type parameter is
Decodable.
You don’t need to constrain the extension — you can also constrain a single method:
extension Preference {
mutating func save(from json: Data) throws where T: Decodable
{
let decoder = JSONDecoder()
self.value = try decoder.decode(T.self, from: json)
}
}
This code does the same thing as the code block you just saw. But it constrains the
method itself instead of the whole extension.
Extending PATs works the same way. You’ll see an example of that when you get to
the practical section of this chapter.
raywenderlich.com 105
Expert Swift Chapter 4: Generics
As you already know, self is usually a reference to the object whose scope you’re
currently in. If you use self inside an instance method of a User struct, self will be
that instance of that struct. So far, that’s pretty straightforward. However, when
you’re in a class method of a class, self can’t be a reference to an instance because
there is no instance: You’re in the class itself.
class Networker {
class func whoAmI() {
print(self)
}
}
Networker.whoAmI() // "Networker"
In class and static methods, self has the value of the current type, not an instance.
It makes sense when you think about it: Static and class methods exist on the type,
not an instance.
However, all values in Swift need to have a type, including the self above. After all,
you need to be able to store it in variables and return it from functions. What would
be the type that holds self in class and static methods, that you now know holds a
type? The answer is Networker.Type: a type encompassing all Networker subtypes!
Just like Int holds all integer values, Int.Type holds all Int type values. These types
that hold other types are called meta-types. It kind of makes your head spin, right?
raywenderlich.com 106
Expert Swift Chapter 4: Generics
In the example above, you declare a meta-type variable called type. The meta-type
can hold not only the Networker type itself but also all of its subclasses, such as
WebsocketNetworker. In the case of protocols, a meta-type of a protocol
(YourProtocol.Type) can hold the protocol type as well as all concrete types
conforming to that protocol.
You have to do this for practical reasons. Usually, type names are used to declare the
type of a variable or function parameter. When they’re not used for declaring types,
they’re used implicitly as initializers. Using .self makes it clearer that you need the
type as a value rather than as the type of something else and that you are not calling
an initializer.
Finally, there is Self with a capital “S”. Thankfully, this one is less convoluted than
all this meta-talk. Self is always an alias to the concrete type of the scope it appears
in. Concrete is emphasized because Self will always be a concrete type, even if it’s
used inside a protocol method.
extension Request {
func whoAmI() {
print(Self.self)
}
}
TextRequest().whoAmI() // "TextRequest"
Self is useful when you want to return the current concrete type from a protocol
method or use it as an initializer inside a static method when creating factory
methods.
That’s enough theory. It’s time to fire up Xcode and get into using generics.
raywenderlich.com 107
Expert Swift Chapter 4: Generics
In this chapter, you’ll expand that library to use generics to provide an even nicer API
for your users.
fetch(url:) is a generic function with one generic type parameter called T. You
declare that T is a type that must conform to Decodable. Once you declare T as a type
parameter, you can use it elsewhere in the type signature — like as the return value,
for instance.
raywenderlich.com 108
Expert Swift Chapter 4: Generics
Because you have access to the generic type parameter T, you can use it in the
function’s body to decode the received data into T. Swift will replace T with whatever
concrete type the function is called with. Because you declared that T conforms to
Decodable, the compiler is happy with your code. If someone tries to call this
function with a non-Decodable type like UIImage, the compiler will throw an error.
Using PATs
Using the generic function is good when you have quick one-off requests. But it
would help to be able to create reusable requests you can fire from anywhere in your
code. To do this, you’ll turn Request into a protocol with an associated type. Open
Request.swift and add the following two lines to the protocol:
associatedtype Output
func decode(_ data: Data) throws -> Output
The Output type tells the user what this request is supposed to fetch. It can be an
Article, [Articles], User, etc. The decode(_:) function is responsible for
converting the data received from URLSession into the output type.
raywenderlich.com 109
Expert Swift Chapter 4: Generics
You first decode the received data into Articles, a helper struct that matches the
API’s response. You then convert that to an array of Article and return it. Notice
you haven’t specified that Output is [Article]. Because you used it as the return
type of decode(_:), Swift can infer the type of Output without you saying so.
Next, you’ll also implement decode(_:) for images. Open ImageRequest.swift and
add an enum inside the struct:
You create a custom enum to represent different kinds of errors that can occur while
decoding the image. In this case, you’ll use only one error, but you can expand this in
your own code to be more descriptive. By conforming to Swift’s Error type, you get
the ability to use this enum as the error type of a Combine publisher or to use it with
the throw keyword.
You try to convert the data to UIImage. If it doesn’t work, you throw the error you
just declared.
Type constraints
Now that you’ve made changes to Request, it’s time to use those changes in
Networker.swift. You might have noticed a weird compiler error: “Protocol Request
can be used only as a generic constraint because it has Self or associated type
requirements”.
raywenderlich.com 110
Expert Swift Chapter 4: Generics
Earlier, I mentioned that protocols with associated types are not types themselves,
though they may act like types. Instead, they’re type constraints.
You convert fetch(_:) to a generic function over the type R, representing any
request. You know that it’s a request since you declare that the type must conform to
Request. You then return a publisher using the Request’s associated Output type.
Here, you’re no longer using Request as a concrete type. Instead, you’re using it as a
constraint on R, so the compiler error goes away.
return publisher.tryMap(request.decode).eraseToAnyPublisher()
}
The function stayed mostly the same except for a couple of key changes. First, you
changed the declaration to make it a generic function. Second, you added a line at
the end that tries to call the Request’s decode(_:) function to return the Output
associated type.
Now that you’ve made all these changes, you can finally use the networker as
intended. Open ArticlesViewModel.swift.
raywenderlich.com 111
Expert Swift Chapter 4: Generics
Next, inside fetchImage, replace the whole fetch(_:) call chain with the following:
You should see a list of raywenderlich.com articles together with their images.
Congratulations, you just made a working networking library using generics! Don’t
rest on your laurels, though. You can still improve the library further by adding
caching using generics.
raywenderlich.com 112
Expert Swift Chapter 4: Generics
Create a new Swift file called RequestCache.swift and add the following to the file:
class RequestCache<Value> {
private var store: [Request: Value] = [:]
}
You create a new generic class that stores request responses in an in-memory cache
inside a plain Swift dictionary. You’ll store the responses keyed by the request that
fetched them so you can always easily tie a request to its response.
You might notice a compiler error. The error is the same one you saw earlier:
“Protocol Request can only be used as a generic constraint because it has Self or
associated type requirements”. Earlier, you fixed this error by not using the Request
type directly instead of using it as a constraint.
But in this case, that’s not possible. You can’t constrain the keys of a dictionary –
they all need to be the same concrete type. In cases where you need to use a protocol
with an associated type as a concrete type, you need to employ type erasure. You
can think of PATs as generic protocols. And type erasure, as its name suggests, is a
way to convert that generic protocol into a concrete type by removing type
information.
Head to Request.swift and add a new struct to the bottom of the file:
This struct is what enables type erasure. You can convert any Request, regardless of
its associated type, to an instance of AnyRequest. AnyRequest is just a plain Swift
struct without any generics. So, naturally, you’ve lost type information along the
way. You can’t use AnyRequest to make your code more type-safe. But sometimes
you can get away with discarding type information, allowing you to write your code
more easily.
raywenderlich.com 113
Expert Swift Chapter 4: Generics
Now, you can go back to RequestCache.swift and use the type-erased struct instead
of Request in store’s declaration:
You’re not the only one using type erasure in this way. Plenty of Apple’s APIs use the
same pattern, such as Combine’s AnyCancellable or AnyPublisher. AnySequence,
AnyIterator and AnyCollection can help you create your own sequences and
collections more easily. SwiftUI’s AnyView allows you to store different types of views
in the same data structure. These are just a few examples to show you that type
erasure is a common pattern in Swift. Because the pattern is common, getting
familiar with it will help you understand existing APIs as well as create new APIs in
the future.
This function will get called when someone wants to retrieve an already stored
response for a given request. The function’s prototype might look a bit complicated,
so breaking it down will help. First, you declare a generic parameter R that must
conform to Request. You then receive a parameter of that same type and return an
instance of Value, the generic type parameter of the RequestCache class. Finally,
you specify that R’s associated Output type must be the same type as the class’s type
parameter.
The last bit of the function’s prototype verifies that no one will accidentally call the
function with the wrong request. If the cache stores images
(RequestCache<UIImage>), only Requests that fetch images can be retrieved
(R.Output == UIImage). If you try to call the method with a mismatched Request,
you’ll get a compiler error:
raywenderlich.com 114
Expert Swift Chapter 4: Generics
However, keep in mind that this is still a generic function, so multiple Request types
can be retrieved as long as their output type matches the type of the stored values.
For instance, you can use both AvatarThumbnailRequest and ImageRequest to call
this method on a RequestCache<UIImage> instance.
Inside the method, you use type erasure by constructing an AnyRequest from the
provided request, which you can use to retrieve a value from the dictionary.
Once again, you add a where clause to the function’s signature to verify that the
request type matches, disallowing accidental wrong entries. As you did in
response(for:), you use type erasure to store a new response in the dictionary.
Now that you have a generic cache, you can create a cache to store all your
downloaded images. Open Networking.swift and add a new property to Networker:
You create a new generic method that receives a request and returns a publisher just
like fetch(_:). But this method uses the cache to retrieve responses if they’re
already stored or store new responses if they aren’t. The function’s prototype
declares that this method can be used only by requests whose Output type is
UIImage because you currently only cache UIImage instances.
raywenderlich.com 115
Expert Swift Chapter 4: Generics
Inside the method, you first check if a response is already cached. If it is, you return a
publisher that emits the cached value using Just.
return fetch(request)
.handleEvents(receiveOutput: {
self.imageCache.saveResponse($0, for: request)
})
.eraseToAnyPublisher()
If there is no cached response, you use fetch(_:) to return a new publisher. You also
subscribe to the publisher’s output event so you can store the response in the cache.
Now that you have your new caching method in Networker, it’s time to use it from
the view model. Open ArticlesViewModel.swift and change fetchImage to the
following:
You no longer need to perform any checks here. Networker takes care of all caching,
letting the view model focus on preparing the view’s data and not worry about
storing requests.
Build and run the project. It should work just like before, except now you have a
much nicer API.
raywenderlich.com 116
Expert Swift Chapter 4: Generics
Key points
• Methods, structs, enums and classes all can become generic by adding type
parameters inside angle brackets!
• Protocols can be generic as well through use of protocols with associated types.
• self has the value of the current type in static methods and computed properties,
and the type of self in those cases is a meta-type.
• You can use extensions with generic constraints, using the where keyword to
extend generic types when their type parameters satisfy specific requirements.
• You can also specialize methods themselves by using the where keyword.
If you want even more introduction to generics, look at the Swift Generics
(Expanded) (https://apple.co/3cNKeW7) WWDC 2018 session.
If you want more behind the scenes information on how generics are implemented
and laid out in memory, check out the Understanding Swift Performance (https://
apple.co/2YTlQtT) WWDC 2016 session.
raywenderlich.com 117
Section II: Standard Library
This sections covers the base layer of writing Swift programs: Numerics, Ranges,
Strings, Sequences, Collections, Codable and the less obvious, but very important
topic - Unsafe.
As you’d expect from an advanced book, we don’t only explain these topics, but also
investigate how they’re built, how they’re represented, and how to use them
effectively.
raywenderlich.com 118
5 Chapter 5: Numerics &
Ranges
By Ray Fix
In this chapter, you’ll complete two iPad apps to investigate the properties of
integers and floating-point numbers. The first of these apps is BitViewer, which lets
you look at bit-level representations and operations. The second app is Mandelbrot,
which allows you to test Apple’s new Swift numerics package. The app lets you
visualize the precision of different floating-point types. Finally, you’ll use a
playground to explore how Swift implements ranges and strides. Throughout the
chapter, you’ll flex your generic programming muscles and write code that works
with a family of types.
This chapter might feel a little academic because it deals with the low-level machine
representation of numbers. A little knowledge in this area will give you extra
confidence and the ability to deal with low-level issues if they ever come up. For
example, if you deal with file formats directly, or find yourself worrying about
numerical range and accuracy, these topics will come in useful. Swift numerics is also
an excellent case study for using protocols and generics that you looked at in
previous chapters.
raywenderlich.com 119
Expert Swift Chapter 5: Numerics & Ranges
Representing numbers
Computers are number-crunching machines made of switching transistors. Consider
the base-10 number 123.75. You can represent it as 1, 2, 3, 7 and 5 if you multiply
each digit by an appropriate weight:
Computer transistors act like high-speed switches that can be either on or off. What
would it look like if you had only two states (0 and 1) to represent a number instead
of 10? 123.75 would look like this:
raywenderlich.com 120
Expert Swift Chapter 5: Numerics & Ranges
The first bit (in the 64 position) has a special name. It’s called the most significant
bit or MSB. That’s because it has the most significant effect on the overall value. The
last bit (in the 0.25 position) is called the least significant bit or LSB. It has the
smallest effect on the overall value.
You can see that number systems rely on exponents. If you need a refresher on those,
you might get a quick review over at the Khan Academy. https://bit.ly/3k0Tsin.
Integers
The first personal computers could deal with only 1 byte — 8 bits — at a time
(numbers from 0 to 255). You needed to juggle these small values around to produce
anything larger. Over the years, the size of the information computers could handle
repeatedly doubled — to 16 bits, 32 bits and now 64 bits on the latest Intel and Apple
processors.
Swift supports all the standard integer sizes, including Int8, UInt8, Int16, UInt16,
Int32, UInt32, Int64 and UInt64. These bit widths are necessary for systems-level
programming and often have native, specialized hardware support as well.
Here’s a table showing each type and the range of values it supports:
raywenderlich.com 121
Expert Swift Chapter 5: Numerics & Ranges
Integer Protocols
Int and UInt types adopt the FixedWidthInteger protocol and the SignedInteger
and UnsignedInteger, respectively. That gives them a ton of shared functionality
and codifies their two’s complement representation, which you’ll learn about
shortly.
But there’s more. Integers losslessly convert to and from String types thanks to
additional protocols and methods.
raywenderlich.com 122
Expert Swift Chapter 5: Numerics & Ranges
raywenderlich.com 123
Expert Swift Chapter 5: Numerics & Ranges
Select a numeric type. For this section, focus on the integer types. You can see a
binary representation of a number and tap on the bits to toggle them. Scroll
horizontally through all the bits or check the checkbox to stack them vertically into
bytes. In a moment, you’ll add code to operate generically on all the integer types.
Position seven is special: It’s the sign bit. You might guess that flipping this bit
would make the value -65. These days, all modern hardware uses two’s complement
representation, where the sign bit adds in the largest negative value. In this case,
negative two raised to the 7th power (-128) added to 65 results in -63. As a diagram,
it looks like this:
raywenderlich.com 124
Expert Swift Chapter 5: Numerics & Ranges
Exercises
• What are the minimum and maximum representable values of a make-believe
Int4 and Int10 type?
• What bit pattern represents -2 using Int4? (Add it to 2 to see if you get zero.)
• List all the protocols shown in this chapter (the above diagrams) that an Int32
supports.
• All the numerics get decomposed into bits and displayed by a BitsView contained
in IntegerView or FloatingPointView.
• Each bit has a “semantic” type, such as sign, exponent or significand, displayed
differently and defined in Model/BitSemantic.swift.
• Many of the abstractions are generic so they work with any integer or floating-
point type.
raywenderlich.com 125
Expert Swift Chapter 5: Numerics & Ranges
// 2
struct Section {
let title: String
let items: [Item]
}
// 3
struct Item {
let name: String
let operation: Operation
}
}
3. Item is a menu selection with a display name and the Operation that gets called
when selected.
Next, define a static property menu to hold sections of operations you’ll add to later.
extension IntegerOperation {
static var menu: [Section] {
[
// Add sections below
]
}
}
This menu can be rendered by the SwiftUI interface. To enable it, open Views/
NumericOperationsView.swift and uncomment the block of code around line 40:
raywenderlich.com 126
Expert Swift Chapter 5: Numerics & Ranges
At this point, you can build and run BitViewer. You won’t see any changes yet. But
when you’ve completed all of the sections below, it will look like this:
BitViewer Operations
You can build and run the app again. These are all generic methods on IntType,
which is constrained to FixedWidthInteger. So you can select any integer type and
run the operation on it to see how the bits change.
raywenderlich.com 127
Expert Swift Chapter 5: Numerics & Ranges
To set to all ones, use .zero from AdditiveArithmetic and the bitwise ~
complement operator from BinaryInteger to flip all the bits.
Endian operations
The term endian refers to two competing ideologies in “Gulliver’s Travels” by
Jonathan Swift that clash over whether you should crack the little end or big end of
an egg.
Endian
Add this to the menu property:
Build and run. Because ARM and Intel hardware are little-endian, tapping on the
littleEndian menu option will do nothing. Tapping on bigEndian will swap the
bytes. The opposite would be true if you were running on a big-endian machine. The
byteSwapped accessor always swaps the bytes no matter what platform you’re on.
raywenderlich.com 128
Expert Swift Chapter 5: Numerics & Ranges
Try out some multi-byte types and make sure they work as you expect.
Thanks to the BinaryInteger protocol, Swift integer types have all the basic
operations for flipping and masking bits. You already saw the complement operator ~
in the Set Value section. Here, it’s used to toggle bits.
The operators >> and << can shift bits left and right, respectively. A critical concept
here is sign extension. The operator >> works differently for unsigned and signed
types. If the type is unsigned, >> will always insert a zero into the most-significant-
bit. But if it’s signed, it will copy the sign bit. Try this with some numbers for
yourself.
Bit Shifting
The reverse operation currently just prints "do later". You’ll implement it in a
moment to reverse the order of all the bits.
raywenderlich.com 129
Expert Swift Chapter 5: Numerics & Ranges
Arithmetic operations
Add these arithmetic operations:
Note: You might think &+ are “fast” operations. They are, but they might also
cause an overall slowdown in your program. The reason is that, especially if
the operation computes array indices, the compiler can no longer reason about
memory safety. As a result, additional checks might end up in your inner loop,
causing a substantial performance hit.
SignedInteger gives you access to a negate method and unary - operator. However,
because IntType is constrained only to the FixedWidthInteger protocol, you need
to do it manually. You do it for two’s complement by flipping the bits and adding one
as you saw previously. Try your custom operation in BitViewer and see it work like
magic!
raywenderlich.com 130
Expert Swift Chapter 5: Numerics & Ranges
Note: Bit manipulations such as reverse can come in handy when you’re doing
signal processing or working with low-level hardware such as device drivers.
The bit-reverse operation is famously used by the Fast Fourier Transform (FFT)
algorithm, which has a wide range of applications.
This code uses the so-called divide and conquer approach, which breaks a big
problem into sub-problems until the sub-problems become trivial:
If you picture each bit as ABCDEFGH, here is how the first line to reverse nibbles
works:
Reverse Line 1
raywenderlich.com 131
Expert Swift Chapter 5: Numerics & Ranges
Reversing the upper and lower half of the nibbles and every other bit operates the
same way.
Now, inside the same file, add the following to make it work for all integer sizes:
extension FixedWidthInteger {
var bitReversed: Self {
var reversed = byteSwapped
withUnsafeMutableBytes(of: &reversed) { buffer in
buffer.indices.forEach { buffer[$0].reverseBits() }
}
return reversed
}
}
You use the endian operation byteSwapped to flip around all the bytes and then grab
a raw buffer to them. Then, you can call your reverseBits() private method to
mutate each of the individual bytes.
To hook it into the interface, change the definition of the reverse item in menu so it
becomes:
Improving bitReversed
The above code requires eight iterations to reverse the native 64-bit type. Can you do
better and use the full width of the processor? Yes, you can.
First, comment out (or rename) the current definition of bitReversed to make room
for a new one. Then type:
extension FixedWidthInteger {
var bitReversed: Self {
precondition(MemoryLayout<Self>.size <=
MemoryLayout<UInt64>.size)
raywenderlich.com 132
Expert Swift Chapter 5: Numerics & Ranges
It’s critical to use an unsigned type to prevent sign extension when you shift the bits.
A cryptic looking number such as 0xf0.... is just 0b11110000... from your first
version spelled out eight times in a compact format. The same is true for all the other
cryptic looking values.
Note: This version only supports a max of 64 bits and halts at runtime
otherwise. You might support larger (not yet standard) formats by looping on
words.reversed() and using the native size UInt instead of an explicit
UInt64.
With this code in place, test it in BitViewer and see that it works as you would
expect on all the sizes.
Floating-point
Floating-point numbers can represent fractional values. The standard floating-point
types include a 64-bit Double, a 32-bit Float and a relatively new 16-bit Float16.
There’s an Intel-only Float80 type dating back to when PCs had separate math co-
processor chips. Because ARM doesn’t support it, you’ll only encounter this type on
an Intel-based platform, such as an Intel Mac or the iPad simulator running on an
Intel Mac.
raywenderlich.com 133
Expert Swift Chapter 5: Numerics & Ranges
Floating-point Protocols
Some of these protocols, such as SignedNumeric, are the same ones used for
integers. The heavy lifting begins in FloatingPoint, which supports most of the
hardware-native, IEEE-754 floating-point standard. More functionality is added by
BinaryFloatingPoint, which handles the specific case when the radix is two.
Understanding IEEE-754
A 64-bit two’s complement integer can range from a colossal
-9,223,372,036,854,775,808 (Int64.min) to 9,223,372,036,854,775,807 (Int64.max).
But a 64-bit Double can range by an unfathomable ±1.8e+308 (as reported by
Double.greatestFiniteMagnitude via the FloatingPoint protocol) . Moreover,
this same Double can represent numbers as small as 4.9e-324 (as reported by
Double.leastNonzeroMagnitude). How is this even possible?
raywenderlich.com 134
Expert Swift Chapter 5: Numerics & Ranges
To explore this, open BitViewer again and select a floating-point type. Working with
Float16 is the most comfortable because the limits and sizes are relatively small.
With Float16, the largest representable finite magnitude is 65504.0 and the smallest
non-zero magnitude is 6e-08.
BitViewer Operations
There are three kinds of bits: one sign bit, five exponent bits and 10 significand bits
for a total of 16. The equation determines the value of finite numbers:
• For any BinaryFloatingPoint, the radix is two. A radix of two is a very efficient
machine representation but can’t exactly represent some common numbers such
as 0.1.
• The sign flips the number positive or negative. Any number raised to the 0th power
is defined to be one (positive). When raised to the first power, the term becomes
minus one (negative). Unlike in two’s complement, floating-point zero has two
representations: -0 and +0.
raywenderlich.com 135
Expert Swift Chapter 5: Numerics & Ranges
• The significand is derived from the raw significand bits in a particular way
described below. The FloatingPoint protocol gives you the cooked version of the
bits you can use in the above formula.
• The exponent is also derived from the bits, and its cooked value can be obtained
from the FloatingPoint protocol.
The tricky magic lies in how the significand and exponent are computed.
The significand bits determine the actual significand value. To get maximum range
in the fewest number of bits, IEEE-754 assumes a phantom, leading ones bit, even
though it is not stored in memory. This is known as the leading bit convention. It’s
why the significand bits are set to all zeros to represent the number one. If you turn
on bit 9 in the above example, it will add 0.5 (2^-1) and change the overall value to
1.5. Turning on bit 8 will add 0.25 (2^-2) to make 1.75 and so on. Try it out and see.
The exponent computation is equally subtle and clever to produce a maximum range
with the fewest bits. The exponent is computed by taking a bias value and
subtracting the magnitude of the exponent bits. The bias determined by the
IEEE-754 standard is:
While experimenting with changing bits, you might have noticed that certain bit
patterns are special case values and the computation rules above are ignored. For
example, if you turn all the exponent bits on, the number becomes a special NaN,
which stands for “not a number”.
struct Section {
let title: String
let items: [Item]
}
raywenderlich.com 136
Expert Swift Chapter 5: Numerics & Ranges
struct Item {
let name: String
let operation: Operation
}
This code should look pretty familiar: It’s just the floating-point version of what you
did with integers. The generic placeholder FloatType is constrained to
BinaryFloatingPoint, which gives you access to a lot of functionality across
concrete floating-point types.
To enable the operations in the UI so you can experiment with them, head over to
Views/NumericOperationsView.swift and uncomment the block of code that
begins with:
This code displays each operation in a list and calls it when you tap it.
Build and run. Select the Float16 type. This section of operations primarily uses the
protocols ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral to set
the value.
raywenderlich.com 137
Expert Swift Chapter 5: Numerics & Ranges
Notice the Attributes that describe the value using a set of binary computed
properties. 1.0 has isFinite, isCanonical and isNormal set to true. isFinite
means that it uses the formula you saw previously to compute the value.
isCanonical implies that the value is in its canonical form.
The same value represented in different ways is known as a cohort. Using the
expressible protocol will ensure you get the canonical representation.
Try some of the other values. In particular, look at 0.1. With only 16 bits and a radix
of two, it is impossible to represent it exactly. Even with a 64-bit Double, you can’t
get it exactly. It would require an infinite number of significand bits to do.
Note: If you’re writing apps that deal with currency, you’ll probably want to
use a numeric type that can represent 0.1 exactly and avoid accounting errors.
Although IEEE-754 specifies a radix 10 type that can handle this, it’s not yet
natively implemented for Swift. However, Swift provides Decimal, an overlay
(wrapper type) on Objective-C’s NSDecimalNumber.
Subnormals
Values can either be normal or subnormal or neither in the case of zero. A normal
number uses the leading bit convention you saw with 1.0. A subnormal (also
denormal) assumes the zero leading bit and supports really small numbers.
Subnormal numbers are created by keeping all exponent bits zero and setting one of
the significand bits. Try it and see!
raywenderlich.com 138
Expert Swift Chapter 5: Numerics & Ranges
FloatType.infinity
},
Item(name: "NaN") { _ in
FloatType.nan
},
Item(name: "Signaling NaN") { _ in
FloatType.signalingNaN
},
Item(name: "greatestFiniteMagnitude") { _ in
FloatType.greatestFiniteMagnitude
},
Item(name: "leastNormalMagnitude") { _ in
FloatType.leastNormalMagnitude
},
Item(name: "leastNonzeroMagnitude") { _ in
FloatType.leastNonzeroMagnitude
},
Item(name: "ulpOfOne") { _ in
FloatType.ulpOfOne
}
]),
As you’ve already seen, floating-point numbers can represent special values that
integers can’t. This section of operations sets them so you can look at the bit
patterns produced.
Setting all the exponent bits to one represents infinity, and you can make -infinity by
setting the sign bit. You can compare this to greatestFiniteMagnitude and
leastNormalMagnitude. For the absolute smallest representable number, you can
use the subnormal leastNonzeroMagnitude.
Not-a-number comes in two flavors. A signaling NaN may cause a hardware trap if
you operate on it. This behavior is good for stopping as soon as an issue occurs
instead of millions or billions of instructions later. Unfortunately, not all hardware
(including ARM) supports it, so you can’t depend on it. Many hardware platforms will
immediately convert a signaling NaN into a quiet one.
You can make a quiet NaN by setting all the exponent bits and the most significant
significand bit to one.
By setting other significand bits, you can send an error code along with your NaN.
This extra information could, in theory, be used to identify the operation that caused
the value to become a nan. But this is not done in practice.
raywenderlich.com 139
Expert Swift Chapter 5: Numerics & Ranges
Aside: Having such a vast number of error codes representing different NaN
codes is considered a significant weakness of IEEE-754. Emerging standards in
scientific computing avoid this, but hardware adoption is, as of this writing,
not available. Check out Type III Unum - Posit for recent developments in
floating-point representations https://en.wikipedia.org/wiki/
Unum_(number_format). Might you see support for this in a future version of
Apple Silicon? Although it’s not in the M1, Swift numerics seem to be mapping
a path for this type of evolution.
Many are surprised that the precision of a floating-point number changes depending
on its value. The bigger the value is, the less precise it is. Consider the following
condition:
if value == value + 1 {
fatalError("Can this happen?")
}
The fatalError will happen if the value is large enough. If value is 1e19, for
example, adding one doesn’t do anything.
raywenderlich.com 140
Expert Swift Chapter 5: Numerics & Ranges
The other stepping methods let you experiment with precision. For example, start
with zero on a Float16 and add 0.1 a dozen times. You’ll see you’re already off by
0.01, which might cause an accountant to go crazy and stay awake at night. Although
BinaryFloatingPoint types are great for overall range and precision, they’re ill-
suited to things like currency, for which you should use a Decimal type. Decimal can
represent 0.1 exactly.
raywenderlich.com 141
Expert Swift Chapter 5: Numerics & Ranges
Numeric Protocols
Notice that this improved hierarchy de-emphasizes the importance of radix-two-
only BinaryFloatingPoint. Instead, it creates a new empty protocol called Real
that combines all the interesting protocols so you can write generic numeric
algorithms like this:
RealType is a generic placeholder that can access all the transcendental functions
and algebraic operations it needs by conforming to Real. That makes it trivial to
switch between any floating-point types with relative ease.
raywenderlich.com 142
Expert Swift Chapter 5: Numerics & Ranges
The Swift Numerics package also introduces a Complex number type that consists of
two floating-point types conforming to Real. It is layout compatible with the
complex types found in C and C++, making it possible to play with popular signal
processing libraries.
You’ll now get some hands-on experience using the numerics package, the Real
protocol and the Complex number type by implementing the famous Mandelbrot set.
(Don’t worry if you’ve never heard of the Mandelbrot set. You’ll be in for a treat.)
Xcode lets you easily browse the source of Swift packages in your project. Take a
moment to explore the Numerics package, paying particular attention to files under
swift-numerics/Sources/RealModule. There, you’ll see implementations for all the
protocols diagrammed above.
The starter app is another SwiftUI app and looks like this in landscape orientation on
an iPad:
raywenderlich.com 143
Expert Swift Chapter 5: Numerics & Ranges
Again, you won’t focus on the specifics of SwiftUI, but you will see how to write
floating-point code generically.
For this application, you’ll compute millions of values continuously. The starter
project sets the debug scheme to release mode to maximize performance. This
setting will make specific loops execute faster by an order of magnitude. If you want
to enable more reliable debugging, change back to a debug build using the Xcode
scheme editor.
Note: A Complex number is a Swift struct that consists of two values. These
real and imaginary components are floating-point values of the same type.
Like plain-old numbers, complex numbers support common operations such as
sums, differences, products, etc. https://www.khanacademy.org is a great
resource for learning about complex numbers if you’re interested in the nitty-
gritty details beyond what is discussed here.
To find out if a number is contained in the Mandelbrot set, square it repeatedly, and
it either blows up (diverges) or does not. Values that don’t diverge are in the
Mandelbrot set.
Most numbers diverge. For example, take the number 5 and start squaring it. 5, 25,
125, 625, 3125, 15625, … It blows up, so is not in the Mandelbrot set. Take the
number 0.1. It equals 0.1, 0.01, 0.001, 0.0001, 0.00001. This number never diverges
and is in the set. You simply expand this idea to complex numbers that have two
components. Mathematicians have proved that if a complex number gets further
away than a radius of two from the origin, it will always diverge. You can use this fact
to great effect to determine if a number is in the set or not.
raywenderlich.com 144
Expert Swift Chapter 5: Numerics & Ranges
Note: Because sets are binary (either in the set or not in the set), you might
wonder why Mandelbrot set drawings appear in psychedelic colors instead of
just black and white. The colors come from how fast a given point diverges.
You’ll want to convert CGPoint to a Complex type with real and imaginary parts
and back again. The starter project defines a CGFloatConvertable protocol and
implements it for all floating-point types to make this easy. You can find the
implementation for this in CGFloatConvertable.swift.
// 2
var z = Complex<RealType>.zero
for _ in 0..<maxIterations {
z = z * z + start
defer {
results.append(z) // 3
}
raywenderlich.com 145
Expert Swift Chapter 5: Numerics & Ranges
// 4
if z.lengthSquared > 4 {
break
}
}
return results
}
This function is generic across any Real conforming type. It returns a list of points
that can be plotted by Core Graphics. These points are squares — the start point for
a maximum of maxIterations times. Here are some key observations:
1. You don’t know how many points will return, but you know it won’t be more than
maxIterations. Pre-allocating the results array will avoid repeated,
intermediate allocations.
2. The loop uses the fact that the Complex type is an AlgebraicField, which can be
squared and added. It handles the real and imaginary parts for you.
3. Using the defer block, you can guarantee that a point gets appended on every
iteration of the loop, even if there is an early exit via break.
4. A point has diverged from the Mandelbrot set if it goes outside radius-two. To
avoid calculating the expensive square root, you can use lengthSquared and 2^2
(four) as the limit.
Build and run the app. You can now explore specific points in the Mandelbrot set by
dragging the dot around. The MandelbrotView calls your function with all floating-
point types and renders them with different line thicknesses. In most cases, they line
up perfectly, but sometimes they don’t.
• One iteration: This squares the number once and ends up outside the circle for a
total of one iteration.
• Two iterations: This squares the number, lands inside the circle, then squares it
again and winds up outside the circle for a total of two iterations.
raywenderlich.com 146
Expert Swift Chapter 5: Numerics & Ranges
• All Sizes Different Paths: This point appears to converge. But at about 100
iterations, all the different floating-point types take off in different directions.
Float16 stays converged while the other 32-, 64-, and 80-bit floats diverge and go
their separate ways.
Unstable Paths
You might wonder what would happen if you tested every single point in the complex
plane and used a different color depending on the number of iterations it took to
diverge outside the radius-two circle. Would it look super cool? Yes, it would. You’ll
implement that now.
raywenderlich.com 147
Expert Swift Chapter 5: Numerics & Ranges
@inlinable static
func iterations<RealType: Real>(start: Complex<RealType>,
max: Int) -> Int {
var z = Complex<RealType>.zero
var iteration = 0
while z.lengthSquared <= 4 && iteration < max {
z = z * z + start
iteration += 1
}
return iteration
}
The @inlinable attribute means that you suggest that the compiler inject the body
of this function at the call site, so you don’t pay the cost of a function call.
This function computes the number of iterations required for a given start point
very efficiently without requiring any heap allocations as the previous point function
did for the array.
This method takes a specific RealType (e.g. Float, Double, Float16) and computes
an entire image with dimension imageSize, where each pixel is a test point.
raywenderlich.com 148
Expert Swift Chapter 5: Numerics & Ranges
The palette is a lookup table that goes from the number of iterations for a
particular point and maps to a 32-bit, red-green-blue-alpha pixel.
The starter project contains a pixel and bitmap abstraction to make image
generation easy. This abstraction can be found in Bitmap.swift and is generic across
pixel types.
// Continued below
return nil
}
This function truncates the width and height of the image and stores them as an
integer in width and height.
It then takes the displayToModel transform, which can transform the display into
math model points and grabs the scale stored in the “a” variable of the matrix. This
operation is valid because there isn’t rotation or skew and the x-scale and y-scale are
equivalent.
upperLeft takes the display point (0,0) and pushes it through the transform to find
a position in the complex plane.
raywenderlich.com 149
Expert Swift Chapter 5: Numerics & Ranges
This code uses the Bitmap abstraction to create a CGImage with the specified width
and height. A CGPoint initializes the Complex type to use as a starting point. Then,
it calls the inlined iterations function defined above to determine the number of
iterations for a particular test point. Finally, it pokes a color value looked up from the
palette into the pixel location. The cgImage accessor initializes an image from
these pixels.
With this code in place, rerun the app to explore the Mandelbrot set in detail. Tap the
image switch to show the image. You can pan and zoom the image to reveal this
fractal world’s infinite complexity.
raywenderlich.com 150
Expert Swift Chapter 5: Numerics & Ranges
Float16 renders well at low zoom factors, but you can see that it breaks down
quickly as you zoom in. You start seeing blocky artifacts like this:
Blocky Image
Float80 is also surprisingly slow on modern Intel machines. This lower performance
results from emulating the computations in microcode on the CPU and because it
takes time to marshal between the Core Graphics CGFloat size.
If you experiment with the other types, you’ll see that same blockiness eventually
with all the floating-point types the closer you zoom. The more precision the type
has, the deeper you can go without quantization errors.
raywenderlich.com 151
Expert Swift Chapter 5: Numerics & Ranges
For example, instead of doing 16 additions one by one, the processor can group one
set of 16 numbers with another set of 16 numbers and perform all 16 additions at
once, in parallel. One clock tick. This performance requires some tricky shuffling of
the data, and, in some cases, the compiler automatically does it for you. This
optimization is known as auto-vectorization and is an active area of compiler
research.
To help the compiler, Swift provides SIMD types for integers and floating-point
numbers. If you group numbers using a SIMD type, the compiler can perform auto-
vectorization much more reliably.
Swift supports SIMD2, SIMD4, SIMD8, SIMD16, SIMD32, SIMD64 types. Each of these
contains Scalar types that are an integer or floating-point number. SIMD8 is said to
contain eight scalar lanes.
Now, use SIMD8 to speed up the Mandelbrot image calculation by performing eight
test point calculations in parallel.
Once again, open the file MandelbrotMath.swift and find the function:
Replace it with:
This code defines some type aliases you can use to play around with different sizes.
ScalarFloat and ScalarInt must have the same width in bits because that is what
modern hardware requires. If you accidentally make them different sizes, the
program won’t type check.
raywenderlich.com 152
Expert Swift Chapter 5: Numerics & Ranges
This code looks similar to the previous non-SIMD version. But because you can’t use
Complex in a SIMD type, you need to perform the operations explicitly in the loop.
These constants appear in the inner loop. Each is eight lanes wide (as determined by
SIMDX, which aliases to SIMD8).
raywenderlich.com 153
Expert Swift Chapter 5: Numerics & Ranges
That code creates the bitmap and returns it as an image. Here are the details:
2. realZ and imaginaryZ are eight lanes of floating-point numbers to keep track of
how these eight test points evolve. counts are eight lanes of the number of
iterations for each test point.
4. ramp is used to determine the real value starting point of the complex number
efficiently below.
This code computes the starting imaginary component used for the whole row of
pixels. Then, you process each of the width pixels in chunks of eight. The answers
accumulate in realZ and imaginaryZ, while the iterations accumulate in counts.
// 1
for _ in 0..<maxIterations {
// 2
let realZ2 = realZ * realZ
let imaginaryZ2 = imaginaryZ * imaginaryZ
let realImaginaryTimesTwo = twos * realZ * imaginaryZ
realZ = realZ2 - imaginaryZ2 + real
imaginaryZ = realImaginaryTimesTwo + imaginary
raywenderlich.com 154
Expert Swift Chapter 5: Numerics & Ranges
// 3
let newMask = (realZ2 + imaginaryZ2) .>= fours
// 4
stopIncrementMask .|= newMask
// 5
let incrementer = ones.replacing(with: zeros,
where: stopIncrementMask)
if incrementer == SIMDX<ScalarInt>.zero {
break
}
// 6
counts &+= incrementer
}
// 7
let paletteSize = palette.values.count
for index in 0 ..< scalarCount {
buffer[x * scalarCount + index + y * width] =
palette.values[Int(counts[index]) % paletteSize]
}
1. For the eight values, you compute up to the maximum number of iterations.
2. This is the algebra used to compute the square of a complex number spelled out.
Recall (a+b)(a+b) = a^2+2ab+b^2. Because b is imaginary, squaring it makes it
a real number. The Complex type handled this for you before, and now you’re
doing it manually.
3. You check if any of the test points sits outside the radius-two circle. If it is, the
mask is true for that lane.
4. You accumulate this mask so that if none of the lanes are incrementing, the loop
can exit early.
5. You take a list of eight ones and replace them with zero if that lane has stopped
incrementing. This masking is how you avoid doing an if/else computation, which
kills parallel performance. If every count has stopped incrementing
(incrementer is all zeros), you cut out early.
6. The incrementer, which is eight lanes of ones and zeros, accumulate into the
counts.
7. Finally, you need to look up the color for each of the iteration counts in the
palette and write it into memory.
raywenderlich.com 155
Expert Swift Chapter 5: Numerics & Ranges
At this point, the algorithm is done. To make it work for any width (not just multiples
of eight), you can add this code to process the remainder:
The above code is the non-SIMD algorithm. If you had a display width that was not
divisible by eight, this code would handle a couple of leftover pixels.
Your SIMD implementation is now complete. You can run the app and now use the
8x64 float type. For fewer iterations, you won’t see much of a speedup. However, if
you boost the number of iterations to 255, you start seeing enormous performance
wins. For example, with max iterations set to 255 and a high zoom factor, Float64
takes 750 ms while the SIMD8<Float64> implementation is 332 ms.
Note: To go even faster than what the CPU can provide, you could take the
rendering algorithm and port it to the GPU. This involves writing the
algorithm as a shader in OpenGL or Metal.
raywenderlich.com 156
Expert Swift Chapter 5: Numerics & Ranges
Ranges
Now, turn your attention to another important aspect of Swift numeric types that
you’ve been using all along — ranges. Earlier, you saw that integers and floating-
point types conform to the Comparable protocol. This conformance is crucial for
supporting operations on ranges of numbers.
Like numeric types themselves, it would be reasonable to guess that ranges are a
concept built into the compiler. But, as with many core features in Swift, they’re just
part of the ever extensible, standard library.
It turns out that a Range is a generic struct with a lower and upper of type Bound
that conforms to Comparable. For example, in an empty playground (or the one
provided in the starter folder), type this:
let longForm =
Range<Number>(uncheckedBounds: (lower: .one, upper: .three))
The ..< operator makes it feel like a built-in language feature and is equivalent:
A key quality of a range is that it doesn’t include the upper bound. You can see that
by running this:
shortForm.contains(.zero) // false
shortForm.contains(.one) // true
shortForm.contains(.two) // true
shortForm.contains(.three) // false
If you need to include the upper bound, there is another range type called
ClosedRange. Try this:
let longFormClosed =
ClosedRange<Number>(uncheckedBounds: (lower: .one,
upper: .three))
raywenderlich.com 157
Expert Swift Chapter 5: Numerics & Ranges
shortFormClosed.contains(.zero) // false
shortFormClosed.contains(.one) // true
shortFormClosed.contains(.two) // true
shortFormClosed.contains(.three) // true
But those aren’t all the range types, of course. You can also create partial ranges
using prefix and postfix operators. Add this:
for i in 1 ..< 3 {
print(i)
}
To see this, make Number adopt Strideable. First, overwrite the definition to this:
raywenderlich.com 158
Expert Swift Chapter 5: Numerics & Ranges
Importantly, the Stride type is set to an Int, which is a SignedInteger. Using Int
makes your Number type a CountableRange which is a typealias defined by the
system:
A common way to do this is to treat the range like a collection and use the
reversed() algorithm like so:
However, when you conform to Strideable, you can use the standard library stride
functions even if your type is not a CountableRange. Try this:
You can also see the usage of stride with CGFloat in the Mandelbrot App. In the
file GraphingView.swift, strides of horizontal and vertical lines are created as part
of the GridLines shape to give the appearance of scaling graph paper.
raywenderlich.com 159
Expert Swift Chapter 5: Numerics & Ranges
Range expressions
If you’re writing a function that takes a range as an input, you might wonder which
of the five flavors to use. A good option is to use the RangeExpression protocol to
conform to all range types. Diagrammed, it looks like this:
Range Expression
You can take advantage of this protocol by making your function abstracted across
range type. For example, if you write this:
Making the function generic keeps your API much more flexible than if the user had
to remember to use a specific range operator such as ..< or .... Now, they all just
work.
raywenderlich.com 160
Expert Swift Chapter 5: Numerics & Ranges
Key points
You’ve seen how Swift builds numeric types and ranges from the ground up using
protocols and generics. Here are some key points to take away:
• As you move down the protocol hierarchy to FixedWidthInteger, you get more
and more functionality that you can use generically.
• To negate a number in two’s complement, you flip the bits and add one.
• Signed integers sign extend a value as it shifts right. Unsigned integers don’t.
• Swift traps your program if you overflow. But you can shut this safety feature off by
using operators that begin with & or special truncating initializers.
• Swift supports IEEE-754 binary floating-point types and uses protocols to describe
them.
• If you’re working with currencies, consider using the Decimal type, which uses a
radix equal to 10.
• Some floating-point types and features are not supported by hardware. (Intel
processors emulate Float16. Float80 is not supported on ARM.)
• The Swift Numerics package hasn’t been merged into the standard library yet.
However, it allows for full generic programming, using the Real protocol.
• The SIMD types let you group data so the compiler can vectorize them. Using
SIMD can significantly increase speed but also adds complexity.
• There are a wide variety of range types defined by the Swift standard library.
raywenderlich.com 161
Expert Swift Chapter 5: Numerics & Ranges
https://en.wikipedia.org/wiki/IEEE_754
The Swift Numerics package is one of the more exciting recent developments in
Swift. It will bring many powerful features, such as approximate equality, additional
number types, generalized tensors and more. For the latest information, check out
the forum at:
https://forums.swift.org/c/related-projects/swift-numerics/56
Finally, you’ve also looked at generic ranges, which can be used as sequences or
collections, or to slice existing collections. You’ll explore the details of these types in
the next chapter.
raywenderlich.com 162
6 Chapter 6: Sequences,
Collections & Algorithms
By Ray Fix
The concepts these protocols express are general enough that they appear where you
might not expect. For example, the ranges and strides you looked at in the last
chapter are sequences and collections, just like arrays are. Although a Range type
doesn’t need to allocate memory for elements like an Array does, it shares many of
the same capabilities and characteristics. In this chapter, you’ll learn about
Sequence, Collection and other related protocols and see how to use them to write
generic algorithms that operate across type families.
raywenderlich.com 163
Expert Swift Chapter 6: Sequences, Collections & Algorithms
A family of protocols
By defining primitive notions of a sequence of values, a collection of values and other
collection characteristics using protocols, you can write high-performance, generic
algorithms. This lets the compiler deal with the specifics of the memory layout that
concrete types use to adopt them.
In other languages, the number of implementations for data structures and their
algorithms can face what is known as “the M by N problem”. Without a language
feature like generics and protocols, the number of implementations for M data
structures and N algorithms is the simple product of the two.
M by N implementations
Imagine having to maintain all this code. The above graphic shows just four
collection types and four algorithms for a total of sixteen implementations. The
truth is that Swift has tons of concrete sequence and collection types such as
CollectionOfOne, JoinedSequence, DropWhileSequence and many more.
M plus N implementations
raywenderlich.com 164
Expert Swift Chapter 6: Sequences, Collections & Algorithms
In this world, any type that conforms to the required protocols gets all the algorithm
implementations generated on-demand for free. The compiler uses the protocol
witness table of protocol declaration to implement function definitions. It can also
create specializations for particular concrete types as an optimization. Although
there’s programmer complexity cost in knowing about these fundamental protocol
types, this knowledge pays for itself handily, as you’ll see.
• Sequence - This is the most primitive type in the hierarchy that lets you iterate
through a list of values. It makes no guarantee about being able to revisit an item.
Although a conforming type could be a collection like an array, it could also be a
stream of data from a network socket or a sequence of random numbers that never
repeat. A type adopting Sequence can be immutable but must vend an associated
mutable type that conforms to IteratorProtocol.
raywenderlich.com 165
Expert Swift Chapter 6: Sequences, Collections & Algorithms
• Collection - All collections are sequences, but Collection adds a guarantee that
you can revisit items using an index type. If you have an index, you can look up an
element in constant time O(1). When you implement your collections, it can be
tempting to break this guarantee. But doing so breaks the complexity guarantees
of the algorithms you inherit. Try not to do this. If you must break the complexity
guarantee, make it clear in the documentation of the API.
This list might feel pretty theoretical, so it’s time to get some hands-on practice with
some simple, concrete examples.
raywenderlich.com 166
Expert Swift Chapter 6: Sequences, Collections & Algorithms
As you can see, defining an iterator is easy. You need to implement a mutating
next() method that updates the state and returns the next element. This code:
1. Keeps going as long as the count state is greater than or equal to zero. Otherwise,
it terminates the iteration by returning nil.
2. Decrements the count after returning the current value. Changing a value after
returning it is a common use of the defer statement.
Now, create the actual Countdown sequence type that vends CountdownIterator:
All this type does is return the iterator above. Now, try it out by adding:
Running the playground counts down from five to zero. Under the hood, the compiler
instantiates an iterator for Countdown and calls next() repeatedly until it returns
nil. The behind-the-scenes iterator instance is what keeps track of the loop’s state.
Note: In case you missed it, there are a lot of type inference and generic
constraints in action here. Sequence types have associated types for the
iterators (Iterator) they create and the elements (Element) they return. A
generic constraint guarantees that Sequence.Element is the same type as
Sequence.Iterator.Element. It’s also possible to hide the iterator’s
implementation from clients by returning some IteratorProtocol from
makeIterator() instead of a specific type.
Admittedly, the code above is contrived and academic. Still, it’s good to get
experience building sequences from the ground up like this so you can appreciate the
other tools at your disposal in the Swift standard library. This exercise also reveals
how the state and its mutation are kept in iterator instances while the sequence
remains immutable.
raywenderlich.com 167
Expert Swift Chapter 6: Sequences, Collections & Algorithms
print("---")
for value in stride(from: 5, through: 0, by: -1) {
print(value)
}
print("---")
for value in stride(from: 5, to: -1, by: -1) {
print(value)
}
Running the playground, you once again see numbers counting down from five to
zero in the console. The function sequence(first:next:) returns the type
UnfoldFirstSequence. You need an initial value and a closure taking the current
value and returning the element or nil when done. Notice this sequence can never
be empty because you specify the first element.
raywenderlich.com 168
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Running the playground once again counts down from five to zero. This overload of
the sequence() function takes an initial state and a closure that lets you mutate that
state with an inout variable. The value returned from the closure is the
Optional<Element> type of the sequence. This sequence is represented by
UnfoldSequence and is more flexible than the first overload because it treats the
state and the elements returned independently.
extension Sequence {
func eraseToAnySequence() -> AnySequence<Element> {
AnySequence(self)
}
}
raywenderlich.com 169
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Run it to see that the type of seq is AnySequence<Int> instead of the underlying
countDownFrom5State, an UnfoldSequence<Int, Int>. It’s helpful to type-erase
method parameters and return AnySequence so you’re not locked into a specific kind
of sequence.
When you run the playground, you see another countdown from five. This
AnySequence takes a closure that makes an iterator. You could make that type
explicitly or use AnyIterator which type erases iterators. This version of the
initializer lets you define the next() method inline.
raywenderlich.com 170
Expert Swift Chapter 6: Sequences, Collections & Algorithms
The examples above demonstrate the many ways the Swift standard library lets you
create a sequence. In the next section, you’ll graduate from counting down from five.
First, though, try a few exercises to see if you’ve got the concepts down.
Exercises
Answers to exercises, as always, are in the final download materials. For best results,
don’t peek — try it yourself first.
1. Turn the array ["a", "tale", "of", "two", "cities"] into a type-erased
sequence.
Collections
Collections build on top of sequences and feature an additional guarantee that you
can revisit elements. To visit an element, all you need is an index that can access an
element in constant time O(1). This complexity guarantee is important because
many other algorithms rely on this base level of performance to guarantee their own
performance.
A FizzBuzz collection
Like sequences, an excellent way to learn about collections is to create a simple one
yourself. Looking at all the protocol requirements of Collection makes creating one
seem a daunting task. However, because most of the API has good default protocol
implementations, it’s pretty straightforward. In some ways, it’s easier to create a
collection than to create a sequence from scratch. What’s more, because Collection
is-a Sequence, you get all the sequence functionality for free. You’ll use FizzBuzz to
see this in action.
raywenderlich.com 171
Expert Swift Chapter 6: Sequences, Collections & Algorithms
FizzBuzz is a classic exercise in which you print out numbers from 1 to 100. However,
if the number is evenly divisible by three, you print “Fizz,” and if the number is
evenly divisible by five, you print “Buzz”. If the number is evenly divisible by both
three and five, you print “FizzBuzz”. The twist is that instead of just printing the
numbers, you’ll create a custom collection type of numbers, fizzes, buzzes and
fizzbuzzes.
This code defines the FizzBuzz collection. You first decide what the associated type
for Index will be and define the start and end index. It isn’t necessary to define
Index here with typealias, but doing so clarifies the code. The endIndex is defined
to be one past the valid range. The function index(after:) defines how to advance
your index. In this case, the implementation is trivial and just adds one.
This code uses Swift pattern matching to implement the actual FizzBuzz logic. The
switch tuple expression generates the appropriate element for a given index.
raywenderlich.com 172
Expert Swift Chapter 6: Sequences, Collections & Algorithms
That’s it. The default protocol implementations do the rest of the work of making a
full-blown collection type. You can test your new collection with:
Run the playground and watch it go. Again, under the hood, the compiler is creating
a FizzBuzz iterator and calling next() on it repeatedly until the loop terminates.
But there’s more. You can use all the collection algorithms Swift has to offer. For
example, you can print the position of all the “FizzBuzz” occurrences using
enumerated() and reduce(into:) by adding this to your playground:
let fizzBuzzPositions =
fizzBuzz.enumerated().reduce(into: []) { list, item in
if item.element == "FizzBuzz" {
list.append(item.offset + fizzBuzz.startIndex)
}
}
print(fizzBuzzPositions)
Running the playground outputs [15, 30, 45, 60, 75, 90]. The enumerated()
method produces a tuple of offsets and elements. You need to make sure you add the
startIndex to the offset to get a valid position.
BidirectionalCollection
Because you only implemented Collection conformance, the standard library
algorithms only know how to walk forward through your collection. To see this, add
some debug printing to your previous implementation of index(after:):
Comment out your previous test code and add the following:
print(fizzBuzz.dropLast(40).count)
As you might have expected, this drops the last 40 elements and prints the number
60, the remaining count of elements. You might be surprised to see after(index:)
being called 220 times.
raywenderlich.com 173
Expert Swift Chapter 6: Sequences, Collections & Algorithms
The first 100 calls are finding the last element in the collection from the beginning.
The subsequent 60 calls are to find the first index of the range to be dropped. The
final 60 calls are to count the remaining 60 elements.
This code lets you go to an index before the current one with the trivial
implementation i - 1.
When you run the playground, you get the same answer as before: 60. But you’ll see
that index(before:) gets called only 40 times as it scans backward to find the first
item to drop. And then index(after:) gets called 60 times to count the remaining
elements. The algorithm adapted to take advantage of the bidirectional traversal
capability.
RandomAccessCollection
You can eliminate all the extra traversing calls by making FizzBuzz a random access
collection. Add this to your playground:
Next, it’s time to explore making collections modifiable with a slightly beefier
example.
raywenderlich.com 174
Expert Swift Chapter 6: Sequences, Collections & Algorithms
MutableCollection
Because FizzBuzz is not mutable by definition, change gears with another example.
Mutable collections allow you to change elements with a subscript setter.
MutableCollection implies that items can be swapped and reordered. This
operation doesn’t imply a change in the size of the collection.
This example features not only mutability but also a custom, non-integer index type.
You’ll implement Conway’s Life, a two-dimensional, cellular automata simulation,
using a custom collection.
raywenderlich.com 175
Expert Swift Chapter 6: Sequences, Collections & Algorithms
You can draw cells and then start and stop the simulation, watching the cells evolve
until they reach a stable point and the simulation automatically stops.
Open the starter project, ConwaysLife, and take a moment to familiarize yourself
with the files.
• AppMain.swift: This is the standard app definition and @main entry point.
• Bitmap.swift: You might remember this type from the Mandelbrot project. It
represents a 2-D set of pixels. This version generalizes it more by removing the
PixelProtocol requirement from the Pixel placeholder type, letting you use it
more generally as a 2-D grid of anything. You still have the power of the
PixelProtocol and the ability to generate a CGImage using the conditional
conformance defined in this file.
• LifeSimulation.swift: This is where you’ll define the business logic of the game.
• LifeView.swift: This is the SwiftUI definition of your user interface that uses your
LifeSimulation model.
• RingMemory.swift: This is a utility class that remembers the last n items. This
memory can identify previous cell patterns and stop the simulation automatically
when it sees one.
// More to come...
}
raywenderlich.com 176
Expert Swift Chapter 6: Sequences, Collections & Algorithms
You need to define what it means for your Bitmap index to be Comparable. A
reasonable choice is to make traversal happen in raster-scan order. Using a tuple
comparison implements a multi-value comparison. Raster-scan is row-major,
meaning the row is the most significant value. Only if the rows of the left-hand side
(lhs) and right-hand side (rhs) are equal does the column break the tie to determine
which is greater.
// More to come...
Minus the subscript operator that you’ll define in a moment, this code provides the
basic definition for a Collection type. Of note is the definition for index(after:).
To know when to jump down to the next row, you need to know the width of the
collection. Needing to know information outside of the index is why advancing the
index is the responsibility of the collection and not the index itself. (You might
imagine a more complex data structure such as a tree needing to know the
collection’s internal details to advance.)
raywenderlich.com 177
Expert Swift Chapter 6: Sequences, Collections & Algorithms
// More to come...
@inlinable
func index(_ i: Index, offsetBy distance: Int) -> Index {
Index(row: i.row + distance / width,
column: i.column + distance % width)
}
@inlinable
func distance(from start: Index, to end: Index) -> Int {
(end.row * width + end.column)
- (start.row * width + start.column)
}
// More to come...
Next, add:
@inlinable
func index(of i: Index, rowOffset: Int, columnOffset: Int) ->
Index {
Index(row: i.row + rowOffset, column: i.column + columnOffset)
}
// More to come...
raywenderlich.com 178
Expert Swift Chapter 6: Sequences, Collections & Algorithms
This indexing method isn’t part of the collection protocol. It’s just a convenient
method you can use to look at neighboring pixels.
Finally, add:
get {
precondition(contains(index: position),
"out of bounds index \(position)")
return pixels[position.row * width + position.column]
}
set {
precondition(contains(index: position),
"out of bounds index \(position)")
pixels[position.row * width + position.column] = newValue
}
}
Defining contains(index:) isn’t required, but it’s a good utility and safety measure
for this type.
raywenderlich.com 179
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Before implementing evolve(), create a helper method that counts the number of
neighbors around a given cell. Add the following to LifeSimulation:
This function uses an imperative style. It uses the index-creating method and the
contains helper method you defined earlier. If an index position goes outside the
bounds of the Bitmap collection, it counts as zero. (This choice is a little bit arbitrary.
You could have made it wrap around.)
func evolve() {
guard isRunning else {
return
}
generation += 1
let neighbors = cells.indices.map(neighborCount(around:))
raywenderlich.com 180
Expert Swift Chapter 6: Sequences, Collections & Algorithms
isRunning = false
}
previous.add(cells)
}
The guard immediately exits if the simulation is not running. If it is, the generation
increments and finds the neighbor counts for all the cell positions.
cells.indices.map(neighborCount(around:)) produces a sequence of all cell
positions and maps it into neighborCount(around:). Next, the core rules of the
game are applied. The zip algorithm creates a sequence of tuples of indices with
neighbor counts, and the switch statement mutates the collection according to the
rules of Life. Finally, previous is used to check if the pattern is a repeat and stops
the simulation if it is.
Next, implement the cellImage property getter that creates an image. It should look
like this:
This code maps a bitmap of booleans to a bitmap of color pixels it can display.
Because you guarantee a valid pixel type, creating the bitmap won’t fail and you can
just fatalError if it does.
Finally, implement the method that lets you draw cells on the board. Replace the
method setLive(row:column:) with the following:
The code here is straightforward. Get the position and set it to true. You don’t want
to look for previous cell patterns that would stop the simulation, so it’s an excellent
place to reset the history of patterns seen.
raywenderlich.com 181
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Build and run. Draw some cells in the gray rectangle and see how they simulate.
Note: Conway’s Game of Life is Turing complete. That means that any
computation you can make in Swift (or any other Turing complete language)
can be made by drawing cells and simulating on a sufficiently large grid in Life.
If you’re interested in learning more about Life and its amazing creator, John
Conway, check out this video: https://www.youtube.com/watch?
v=Kk2MH9O4pXY and prepare to have your mind blown.
raywenderlich.com 182
Expert Swift Chapter 6: Sequences, Collections & Algorithms
3. The subsequence (a collection), in turn, has a subsequence that’s the same as the
original subsequence. The definition is recursive, so it’s the same collection type
for subsequences all the way down.
To see this in action, go back to your FizzBuzz playground and comment out the
debugging print statements so the console isn’t too noisy.
raywenderlich.com 183
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Take a moment to appreciate that, without any extra code, you can create a
subsequence in the FizzBuzz using a range of indices. The sliced collection, instead
of starting from 1 as in the original collection, starts from 20. The end index is 31, for
a total of 11 elements. You call enumerated() to loop through the elements in the
slice.
Again, the start index matches the numbering in the original collection. Also, just as
the generic constraint said, slice and sliceOfSlice are both of type
Slice<FizzBuzz>.
Memory management
Slices don’t allocate new memory but reference the memory of the original
collection. This reference means they’re cheap O(1) to create because they don’t
copy elements and can be used to construct efficient, generic algorithms.
But because a Slice references the original collection, even a tiny slice will extend
the original collection’s lifetime. If you want to disconnect from the original
collection so it can deallocate when it goes out of scope, you can explicitly make a
copy with the appropriate initializer. To see this in action, add this to your
playground:
raywenderlich.com 184
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Consider the following problem. Find the first three, even non-Fizz, Buzz, FizzBuzz
numbers in the FizzBuzz collection. Solve it by adding this code:
print(firstThree)
This code creates a FizzBuzz collection by iterating through all 100 strings and
compacts to an array of 53 integers. It then filters that array by creating a new array
of 27 even integers. Finally, it picks off the first three values of [2, 4, 8].
Because you want only the first three numbers, it’s much more efficient to evaluate
this chain of computation lazily. You can stop after you find three instead of finding
everything and throwing away all but the first three. You can do that by accessing the
lazy property on sequences like so:
raywenderlich.com 185
Expert Swift Chapter 6: Sequences, Collections & Algorithms
.compactMap(Int.init)
.filter { $0.isMultiple(of: 2) }
.prefix(3)
print(Array(firstThreeLazy))
Generic algorithms
The Swift standard library contains a bevy of algorithms that automatically apply to
sequences and collections that meet the appropriate requirements. For example,
first, forEach, map, reduce, sort and zip are standard library algorithms.
It’s time for you to get some practice creating your own custom algorithm. Chunk the
elements in a sequence in the FizzBuzz playground, and add the following:
extension Array {
func chunks(ofCount chunkCount: Int) -> [[Element]] {
var result: [[Element]] = []
for index in stride(from: 0, to: count, by: chunkCount) {
let lastIndex = Swift.min(count, index + chunkCount)
result.append(Array(self[index ..< lastIndex]))
}
return result
}
}
values.chunks(ofCount: 3)
raywenderlich.com 186
Expert Swift Chapter 6: Sequences, Collections & Algorithms
This extension on Array breaks elements into chunks of a given count. The last
chunk might be smaller than the requested count depending on how many items are
in the array. It uses a stride sequence starting from zero up to count to initialize
smaller arrays repeatedly.
Although this code works, it’s not particularly generic or efficient. If, for example,
you sliced the array and tried to get the chunks of that, it wouldn’t compile because
ArraySlice isn’t an Array. It certainly won’t work with a collection like FizzBuzz.
Also, each chunk requires a separate heap allocation and may require reallocations,
depending on the size of the array being split into chunks. You can do better.
Comment out the previous version and its call site and add this:
extension Collection {
func chunks(ofCount chunkCount: Int) -> [SubSequence] {
var result: [SubSequence] = []
result.reserveCapacity(count / chunkCount
+ (count % chunkCount).signum())
var idx = startIndex
while idx < endIndex {
let lastIndex = index(idx, offsetBy: chunkCount,
limitedBy: endIndex) ?? endIndex
result.append(self[idx ..< lastIndex])
idx = lastIndex
}
return result
}
}
Because you’re extending Collection, it can be used with many more types than
just arrays. It returns an array of SubSequence, which might be Slices or
ArraySlices or SubStrings, depending on the type. You can’t assume a zero-based
index like before, so you need to use startIndex. Finally, using reserveCapacity(),
you ensure that there’s exactly one allocation instead of many.
values.chunks(ofCount: 3)
Array(FizzBuzz().chunks(ofCount: 5).last!)
"Hello world".chunks(ofCount: 2)
Generic algorithms are powerful and reusable and promote clarity. Using algorithms
like this instead of raw loops generally will make your code easier to read and
maintain.
raywenderlich.com 187
Expert Swift Chapter 6: Sequences, Collections & Algorithms
Key points
The Swift standard library’s sequence and collection protocols fully leverage the
generic system to make a consistent and predictable (and incredible) programming
model. Here are some key points to take away:
• Sequences, the most primitive type in the sequence hierarchy, guarantee only that
you can visit a list of elements once.
• Sequences can be immutable but vend mutable iterators that keep track of your
iteration state.
• Iterators can be used directly, but the Swift compiler usually generates and
maintains them for you. The compiler makes one every time you write a for loop.
• Collections are sequences that can visit elements using an index any number of
times.
• There are many ways to create custom sequences that range from hand-coding the
iterator and sequence type to using standard library utility methods to do the job
in much less code.
• AnySequence and AnyIterator have initializers that take closures that compose
together to create custom sequences succinctly.
raywenderlich.com 188
Expert Swift Chapter 6: Sequences, Collections & Algorithms
• SubSequence is the name of the associated type in Sequence and Collection that
gets a subset of elements.
• Using a Slice doesn’t copy the underlying collection but references into it and
extends its lifetime.
• Generally, avoid using subsequences for high-level APIs because even a tiny slice
can keep alive a huge underlying collection.
• String uses Substring and Array uses ArraySlice instead of the vanilla Slice
to make working with these subsequences more like the original collection.
• There’s a whole set of LazySequence types that prevent eager evaluation and can
prevent unnecessary computation, speeding your code.
• The Swift standard library uses protocols and generics to define generic
algorithms. It’s easy to define your own.
raywenderlich.com 189
Expert Swift Chapter 6: Sequences, Collections & Algorithms
raywenderlich.com 190
7 Chapter 7: Strings
By Ehab Amer
The proper implementation of a string type in Swift has been a controversial topic
for quite some time. The design is a delicate balance between Unicode correctness,
encoding agnosticism, ease-of-use and high-performance. Almost every major
release of Swift has refined the String type to the awesome design we have today. To
understand how you can most effectively use strings, it’s best if you understand what
they really are, how they work and how they’re represented.
• The binary representation of characters, and how it developed over the years
• How Swift works with UTF encodings, and how low-level details of UTF affect
String’s performance
• What string folding is and how you can best search in strings
• Custom String interpolation and how you can use it to initialize a custom object
from a string or convert it to a string
raywenderlich.com 191
Expert Swift Chapter 7: Strings
Binary representations
Character representation has changed so much over the years, starting from ASCII
(American Standard Code for Information Interchange), which represents English
numbers and characters using up to seven bits.
Then, Extended ASCII came along, which used the remaining 128 values
representable by a single byte.
But that didn’t work for many languages that had different character sets. So another
standard came out, called ANSI. Which is also the name of the entity that created
this standard. American National Standards Institute.
Unlike ASCII, ANSI’s not a single character set. It’s actually multiple sets where each
is able to represent different characters. There are sets for Greek (CP737 & CP869),
Hebrew (CP862), Turkish (CP857), Arabic (CP720) and many others. Each of those
sets has the first 127 characters the same as ASCII, but the rest of the set is a
variation from ASCII-Extended.
raywenderlich.com 192
Expert Swift Chapter 7: Strings
For example, the character of byte hex value 0x9C, when read with character set
CP-852, aka Latin-2, will show the character ť (Lower case t with caron). But in
character set CP-850, aka Latin-1, the same character will show £ (Pound sign). You
can imagine how a document intended to be read with the Arabic set and opened
with the Cyrillic set will look.
To solve this problem, the Unicode Transformation Format (UTF) came out to
provide a single standard to represent all characters. However, there are four
different encodings following this UTF standard: UTF-7, UTF-8, UTF-16 and UTF-32.
Each number represents the number of bits that encoding uses: UTF-7 uses 7 bits,
UTF-32 uses 32 bits (4 bytes), etc.
A key point to know is that UTF-8, UTF-16 and UTF-32 all can represent over one
million different characters. It is clear that the latter of the group has a large range.
As for the first, it’s not limited to 8 bits only — it can expand over 4 bytes. To cover all
possible values in the UTF standard requires 21 bits.
A byte with its most significant bit having 0 value is, on its own, a character. The
character is 1 byte.
A byte with its three most significant bits having 110 value, along with the following
byte, represent a character. The character is 2 bytes.
raywenderlich.com 193
Expert Swift Chapter 7: Strings
A byte with its four most significant bits having 1110 value, along with the two
following bytes, represent a character. The character is 3 bytes.
A byte with its five most significant bits having 11110 value, along with the three
following bytes, represent a character. The character is 4 bytes.
Any byte with its two most significant bits having 10 value is a byte that is part of a
character (following byte). It doesn’t provide enough information on its own without
the leading byte.
The number of bits available to store the value for UTF-8 is calculated as follows:
If the 2 bytes start with 0xD8 (110110 in binary), those two bytes complete a
character. This character is 4 bytes in size.
The following 2 bytes will start with 0xDC (110111 in binary). This makes for 4-byte
values, with 12 bits reserved and 20 bits to define the value.
With those reserved values, characters can’t be represented with values in the range
between 0xD800 to 0xDFFF, because doing so would confuse their values with length
extensions.
raywenderlich.com 194
Expert Swift Chapter 7: Strings
It’s worth noting that UTF-16 and UTF-32 aren’t backward compatible with ASCII,
but UTF-8 is. That means a file saved with ASCII encoding can still be read with
UTF-8 encoding, but can’t in the other two encodings.
Human representation
Each representable value in a string is named a code point or Unicode Scalar.
Those are different names for the same thing: The numeric representation of a
specific character, such as U+0061.
For example, the Unicode scalar U+0061 represents the letter a (Latin lowercase
letter “a”), and U+00E9 represents é (Latin lowercase letter “e” with acute).
Fonts are a pallet of glyphs that have a different drawing. Each glyph/letter is drawn
in a different style, but in the end, they all have maps to a binary representation. A
font affects only the rendering; it changes nothing in the stored information.
Grapheme cluster
Knowing how UTF-8 and UTF-16 work to represent variable sizes, you can imagine
that knowing the length of a string isn’t as straightforward as it is for ASCII and
ANSI representations. For the latter, an array of 100 bytes is simply 100 characters.
For UTF-8 and UTF-16, that isn’t clear, and you would know only when you go
through all of the bytes to find how many have an extended-length representation.
For UTF-32, this isn’t an issue. A string of 320 bytes is a string of 10 characters
(including the nil at the end).
raywenderlich.com 195
Expert Swift Chapter 7: Strings
To make it a little more complicated, say you have 4 bytes for a UTF-16 string, and
there are no extended lengths. You would think that this means you have a string of
length two. The answer is: not necessarily!
Take the character U+00E9 é (Latin lowercase letter “e” with acute) as an example. It
can be represented like that or by two Unicode scalar values of the standard letter e
U+0065 (Latin lowercase letter “e”) followed by U+0301 (combining acute accent).
import Foundation
eAcute.count // 1
combinedEAcute.count // 1
In Swift, both of those strings have a length of 1, although they have different binary
sizes. Also, those strings are equal:
When different Unicode sequences form the same result, those results are said to
have canonical equivalence. Swift equality checks the canonical equivalence of the
content and not the absolute equality of the content.
eAcute_objC.length // 1
combinedEAcute_objC.length // 2
Objective-C String didn’t read any of that. It simply compared the contents of the
bytes. It didn’t check its contents and didn’t figure out that both represent the same
thing.
raywenderlich.com 196
Expert Swift Chapter 7: Strings
If you keep two characters separate that normally would form a grapheme cluster if
merged together, they’ll both be treated as normal characters. It’s only when you
merge them that they become a different character:
acute.count // 1
smallE.count // 1
combinedEAcute2.count // 1
Now that you understand how characters are represented in zeros and ones, let’s see
how Swift works with them and how all those “under the hood” details affect how
you can work with strings.
UTF in Swift
Until Swift 4.2, Swift used UTF-16 as the preferred encoding. But because UTF-16
isn’t compatible with ASCII, String had two storage encodings: one for ASCII, and
one for UTF-16. Swift 5 and later versions use only UTF-8 storage encoding.
UTF-8 is the most common server-side encoding: Over 95% of the internet uses it.
You might think for a moment that the internet isn’t only English and UTF-16 is the
more logical choice because less extended byte values will be used. But most of a
webpage is HTML, and HTML can be completely represented in ASCII. This makes
the usage of UTF-8 for internet content a better choice for size and transfer speed.
That said, the change to UTF-8 storage encoding made any communication between
Swift and a server straightforward, because they use the same encoding and
therefore require no conversion.
sampleString.last
̗a͌ t̕ȉs ̉
// t̕em ͙ ͞d ̗
ro͞ lo musp͇ȉ ̗ ̉o͞ L
mer
let reversedString = String(sampleString.reversed())
raywenderlich.com 197
Expert Swift Chapter 7: Strings
You can traverse a Swift String in either direction, and you can also replace a range of
values. But it doesn’t conform to RandomAccessCollection.
You could extend String with subscript(_:) so you can easily access characters by
their index:
extension String {
subscript(position: Int) -> Self.Element {
get {
let characters = Array(self)
return characters[position]
}
set(newValue) {
let startIndex = self.index(self.startIndex,
offsetBy: position)
let endIndex = self.index(self.startIndex,
offsetBy: position + 1)
let range = startIndex..<endIndex
replaceSubrange(range, with: [newValue])
}
}
}
sampleString[2] // r
sampleString[2] = "R"
r sȉt ̕
͙ ̉͞
͇ ̗ do͞ lo
sampleString // LoRem ȉpsum a͌m̗et ̕
In the code above, there doesn’t seem to be a problem. Try the following code:
for i in 0..<sampleString.count {
sampleString[i].uppercased()
}
raywenderlich.com 198
Expert Swift Chapter 7: Strings
With a quick look, you would think this code has a complexity of O(n), but that is
incorrect. In the subscript(_:) implementation, you converted the string to an
array to get the index you want. That itself is an O(n) operation, giving the loop you
added a complexity of O(n^2).
You can’t reach the nth character directly without passing by the n-1 characters first.
A character — aka grapheme cluster — can be a long sequence of Unicode scalars,
making the operation of reaching the nth character one of O(n), not O(1), thus not
meeting the requirement of RandomAccessCollection.
Although the extension you created simplifies and shortens your code, it also affects
the performance:
This code is the same. It didn’t use the subscript approach and traverses the
collection once. Using the subscript approach will often seem appealing, but that
approach causes you to do many more operations than you think. Thus,
understanding how the String class works, as well as what Character is and how
Swift treats it, can make a huge difference in how you approach challenges and
implement solutions.
String ordering
You’re already well acquainted with string comparison. The default sorting in a string
ignores localization preference.
String comparison is always consistent, as it should be. However, for different locales,
it should be different.
For example, the ordering of Ö is different than Z between German and Swedish:
// German "
OwithDiaersis.compare(
zee,
locale: Locale(identifier: "DE")) == .orderedAscending // true
// Sweden #
raywenderlich.com 199
Expert Swift Chapter 7: Strings
OwithDiaersis.compare(
zee,
locale: Locale(identifier: "SE")) == .orderedAscending //
false
When you’re ordering text for internal use in the system, the locale must not affect
it. But if you’re ordering it to show it to the user, you must be aware of the
differences.
Also, there is a notorious problem that arises when strings have numbers. A string
with value "11" should be higher than a string of value "2". But this isn’t the case
unless it is a comparison that is considering the locale:
String folding
The more you work with different languages, the more challenges you’ll face with
string searching. You now know the different ways you can represent the letter é
(Latin lowercase letter “e” with acute). But the word "Café" doesn’t match "Cafe":
And checking if it contains the letter e (Latin lowercase letter “e”) will return false:
"Café".contains("e") // false
When you want to compare strings and ignore casing, you convert the original string
and keyword to the same casing, upper or lower. This is called String Folding, where
you remove distinctions on the strings to make them suitable for comparison.
In the case of diacritics, you want to remove all of the marks and return all of the
characters to their original letter to simplify comparison. To continue with our
example, this would return Café, or any other diacritic variation of it, to Cafe.
raywenderlich.com 200
Expert Swift Chapter 7: Strings
̾e͜ ͘
let originalString = "H l͘lò W͛òr
̠͘ ͐
ld!"
originalString.contains("Hello") // false
originalString contains a combining character for each letter in the string Hello
World!. That makes it very hard to search for any words. Luckily, String provides a
mechanism for folding so you can specify what distinctions you want to remove.
Cases, diacritics, or both:
originalString.localizedStandardContains("hello") // true
This method does the same. It performs a case- and diacritic-insensitive, locale-
aware comparison. Without folding the string to remove the diacritics, you’ll have a
very hard time searching for text, or you’ll give the user a very unpleasant
experience.
As you can see from its name, a substring is a part of a string. And it is a very fast and
optimized datatype when you’re breaking down a large string. However, there is a key
point that you should be aware of, especially when working with large strings:
raywenderlich.com 201
Expert Swift Chapter 7: Strings
What you expect with a quick look is that you worked with the large string, finished
using it, and returned only the small part of the string you need:
You still have the large string loaded in memory. Substring shares memory with the
original string. If you’re working with a large string and need a lot of smaller strings
from it, while still using the large string, there will be no additional memory cost. But
if you want to just break it and remove the large string from memory, then you need
to create a new string object from your substring right away:
If you don’t, the original string will stay in memory for much longer without your
awareness.
That was a lot of info about String. The next part will cover a very interesting perk
from Swift that you’ve been using frequently. You’ll know how it works under the
hood and builds on top of it.
struct Book {
var name: String
raywenderlich.com 202
Expert Swift Chapter 7: Strings
Wouldn’t it be super cool if you could define a new instance from Book with a string
like "Expert Swift by: Ehab Amer,Marin Bencevic,Ray Fix,Shai Mishali"?
Swift allows you to define any type by a string literal by conforming to the protocol
ExpressibleByStringLiteral, and implementing init(stringLiteral value:
String).
You break down the string into two parts with the " by: " separator: The first part
is the book name, and the second part is the author names, comma-separated. Ignore
the “fpe” (final pass editor) for now, but you’ll use this property later.
This is a very human-friendly way to construct your object, but if anything changed
in this format, unexpected data will be saved in the object!
raywenderlich.com 203
Expert Swift Chapter 7: Strings
Now, the name contains invalid information, and the last author is actually two of
them together. You can fix this by improving the implementation of
init(stringLiteral value: String), but will you ever be able to expect all
possible inputs to make sure that the string will be parsed properly?
There is another way you can construct Book: using string interpolation. To do this,
you define a string that has clear, explicit mention of the book name and the array of
authors:
init(stringInterpolation: StringInterpolation) { // 8
self.authors = stringInterpolation.authors
self.name = stringInterpolation.name
self.fpe = stringInterpolation.fpe
}
}
raywenderlich.com 204
Expert Swift Chapter 7: Strings
3. The new struct must carry properties to store the values that will be provided in
the string. For this example, name and authors will do. You can also have any
properties you may need. Ignore fpe for now.
4. The string will contain literal strings and interpolations. This initializer is the
first that gets called. It provides the counts of every character in the literal and
the number of interpolations present.
5. This gets called for literals in the string. For this example, do nothing with them.
This method declaration identified the generic type for the literal as String.
6. This added an interpolation with a string that defines the name of the book.
Interpolation should look like "\(String)"
The book was defined with a lot more description. Even the list of authors came
before the name of the book. But because each interpolation has its form, either
through a label and/or data-type, there was no mixup.
raywenderlich.com 205
Expert Swift Chapter 7: Strings
stringInterpolation.appendInterpolation(
authors: ["Ehab Amer",
"Marin Bencevic",
"Ray Fix",
"Shai Mishali"])
stringInterpolation
.appendLiteral(" wrote this great book. Titled ")
stringInterpolation
.appendInterpolation("Expert Swift")
Book(stringInterpolation: stringInterpolation)
Then, for each literal sequence, appendLiteral(_:) is called. After that, for each
interpolation, its appropriate method is called. Finally, the initializer is called with
the interpolation object.
Notice that each interpolation was translated to a method. \(_:) was translated to
appendLiteral(_:), and \(authors:) was translated to
appendLiteral(authors:).
Remember the fpe that you didn’t use? So far, you focused only on the title and
authors of the book. But at the point of creating the interpolation object, you had no
use for this property and left it empty.
extension Book.StringInterpolation {
mutating func appendInterpolation(fpe name: String) {
fpe = name
}
}
raywenderlich.com 206
Expert Swift Chapter 7: Strings
This created a new instance of a book and used the interpolation you identified in
the extension to set fpe. You can define as many additional interpolation formats as
you wish:
extension Book.StringInterpolation {
mutating func appendInterpolation(bookName name: String) {
self.name = name
}
This added an alternative way to define the name of the book and a way to add
authors one by one:
The type String is no different from Book. You have already been using its
StringInterpolation subtype for some time with your standard interpolations,
such as including a number in a string:
Just as you added a new interpolation for fpe on Book, you can do the same on
String to interpolate Book.
The string doesn’t have a friendly representation of the book. But you can control
that. Add an extension to StringInterpolation inside String:
extension String.StringInterpolation {
mutating func appendInterpolation(_ book: Book) {
appendLiteral("The Book \"")
appendLiteral(book.name)
appendLiteral("\"")
if !book.authors.isEmpty {
raywenderlich.com 207
Expert Swift Chapter 7: Strings
if !book.fpe.isEmpty {
appendLiteral(" Final Pass Edited by: ")
appendLiteral(book.fpe)
}
}
}
Add the fpe to interpolatedBook object you defined earlier, and convert it to a
string:
In the extension, you had full control over how the fields were printed, their order
and what user-friendly text to precede and/or follow each property.
The reason appendLiteral(_:) was heavily used here is that you don’t know the
internal implementation of String.StringInterpolation, and you don’t know
what temporary fields it has to store the information. But it’s not like
Book.StringInterpolation. The literals are stored just like interpolations and in
order, so you can safely convert an interpolation to a series of literals. In the end, it is
only one string. Not multiple fields like in Book.
raywenderlich.com 208
Expert Swift Chapter 7: Strings
Key points
• ASCII was the first standard for storing characters, and it evolved to UTF to
represent all the possible characters in one single standard.
• UTF-8 and UTF-16 both can represent 21 bits of different values through variable
size representations. A UTF-8 character can take up to 4 bytes.
• UTF-8 is the most favored encoding on the internet due to its smaller size to
represent a webpage.
• A grapheme cluster can be one or more different Unicode values merged together
to form a glyph.
• A character in Swift is a grapheme cluster, not a Unicode value. And the same
cluster can be represented in different ways. This is called canonical equivalence.
• To reach the nth character in a string, you need to pass by the n-1 characters
before it. It is not an O(1) operation.
• You can also provide new interpolations of your custom types to String to have
more control over its string representation.
raywenderlich.com 209
8 Chapter 8: Codable
By Shai Mishali
When developing your app, you’ll often deal with a myriad of data models and
various external pieces of data that you’ll want to represent as data models in your
app.
To solve this problem, you’ll often use a technique called serialization, where you
create an external representation of your data models in one of many consumable
formats supported by multiple platforms. The most popular of the bunch by far is
JSON (Javascript Object Notation).
The need for data serialization in Apple’s platforms is so common that they solved it
all the way back in Xcode 3.0, with the introduction of NSCoding, which lets you
describe how to encode and decode your data.
raywenderlich.com 210
Expert Swift Chapter 8: Codable
What is Codable?
Codable is a type alias combining two protocols: Encodable and Decodable. These
let you define how objects are encoded and decoded to and from an external data
representation, such as JSON.
The great thing about Codable is that it’s mostly agnostic toward the format it
encodes to and decodes from. It uses an additional set of abstractions called Encoder
and Decoder to achieve this separation.
These abstractions own the specific intimate knowledge of how to encode and
decode in and out of their specific data formats. For example, a JSONEncoder would
know how to encode a given data model into a JSON response, while a
PropertyListDecoder would know exactly how to take a plist file and decode it
into a given data model.
This abstraction means that your objects only have to conform to Codable or either
of its parts once, and can be encoded to and decoded from many different formats
using various encoders and decoders.
raywenderlich.com 211
Expert Swift Chapter 8: Codable
Since JSON represents the vast majority of Codable use cases, you’ll focus on using
JSONEncoder and JSONDecoder, with the majority of this chapter focusing on the
Decodable portion of the equation.
Specifically, you’ll work on three real-life API responses and write the appropriate
Decodable and Encodable conformances for them, teaching you everything you
need to tackle the weirdest of responses.
{
"name": "Shai Mishali",
"twitter": "@freak4pc",
"github": "https://github.com/freak4pc",
"birthday": "October 4th, 1987"
}
struct Person {
let name: String
let twitter: String
let github: URL
let birthday: Date
}
All you’d have to do to make Person compatible with its JSON structure is simply
conform it to Codable, like so:
raywenderlich.com 212
Expert Swift Chapter 8: Codable
// Decode
let decoder = JSONDecoder()
let person = try decoder.decode(Person.self, from: jsonData)
// Encode
let encoder = JSONEncoder()
let jsonData = try encoder.encode(person)
Swift will know to match the keys in your JSON to the ones in your Person struct,
and will even know how to automatically convert the String date and URL to Swift’s
Date and URL!
You’ll learn more about how Swift knows to do this automatically for you later in this
chapter.
With this quick refresher behind you, it’s time to work on some APIs!
raywenderlich.com 213
Expert Swift Chapter 8: Codable
• The Sources folder includes an API helper you’ll use throughout this chapter.
• The Resources folder includes JSON files containing the responses you’ll interact
with in this chapter.
[
{
"id": "comb",
"name": "Combine: Asynchronous Programming with Swift",
"store_link": "https://store.raywenderlich.com/...",
"authors": [
"Scott Gardner",
"Shai Mishali",
"Forent Pillet",
"Marin Todorov"
],
"image_blob": "..."
}
]
This file provides an array of books from the raywenderlich.com book catalog.
Basic decoding
Open the page 1. Ray’s Books. The easiest path to decoding the response is to start
with properties you get “for free” from Swift.
Above your new struct, add the following piece of code to try it out:
do {
let books = try decoder.decode([Book].self, from: data)
print("—— Example of: Books ——")
raywenderlich.com 214
Expert Swift Chapter 8: Codable
print(books)
} catch {
print("Something went wrong: \(error)")
}
Run your playground and you’ll see a response similar to the following:
Nice! You’ve got the basics down. You’ll now learn how to decode the other fields,
starting with store_link.
As the error outlines, no key called storeLink exists in the JSON response. However,
there is one called store_link!
The snake-case option is so common that it’s already built into JSONDecoder.
raywenderlich.com 215
Expert Swift Chapter 8: Codable
decoder.keyDecodingStrategy = .convertFromSnakeCase
This tells the JSON decoder to automatically convert snake-cased keys as needed.
Decoding data using Decodable is not as hard as it sounds. It uses a concept similar
to a key decoding strategy — a data decoding strategy.
With a data decoding strategy, whenever JSONDecoder sees a property of type Data,
it checks with the data decoding strategy to determine how it should translate the
JSON data into Swift’s Data type.
Below the keyDecodingStrategy you added earlier, add the following line:
decoder.dataDecodingStrategy = .base64
The code above defines Base 64 as the data decoding strategy (which is also the
default), an imageBlob property to correlate with the image_blob key in your JSON
response and, finally, wraps everything up with an image computed property you can
use to see the image itself.
raywenderlich.com 216
Expert Swift Chapter 8: Codable
Run the playground and you’ll see output similar to the following:
In the gutter area, in the line that says _ = book.image, press the Show Result
button:
Then, right-click the resulting image area and choose Value History:
Now you can scroll and see all the loaded Base 64 decoded images from your JSON
response. Excellent!
You’re still not done with the Ray Books API. Before wrapping up, you’ll take a small
detour to learn a bit more about Coding keys.
raywenderlich.com 217
Expert Swift Chapter 8: Codable
init?(stringValue: String)
init?(intValue: Int)
}
It can be decoded with either a numeric key or a string key, but not both.
If you think back to the decoding error you saw earlier, a CodingKey with a
stringValue of storeLink wasn’t found in the original response until you added
the appropriate key decoding strategy.
When your properties match perfectly with those of the JSON response, you don’t
have to manually create any coding keys. As soon as a single one of them requires a
custom key, however, you’ll need to define your own coding keys.
A common way to do this is to use an enum with a raw value of String. There’s no
need to copy this into your playground.
When you explicitly specify the coding keys as shown above, you don’t need a key
decoding strategy.
raywenderlich.com 218
Expert Swift Chapter 8: Codable
This uses a kebab-case version of the Ray’s Books API, meaning that store_link is
now store-link, and image_blob is now image-blob.
Run your playground and you’ll find a new error waiting for you:
As the error mentions, the decoder tries to convert storeLink to store_link but
can’t find a key that matches it in the JSON response.
One of the key decoding strategies you can use, the aptly named .custom, lets you
take full control of the key decoding and conversion.
extension JSONDecoder.KeyDecodingStrategy {
static var convertFromKebabCase:
JSONDecoder.KeyDecodingStrategy = .custom({ keys in
})
}
This new static property returns a custom key decoding strategy that takes in an
array of CodingKeys and returns a single, transformed coding key. Don’t worry about
the compilation errors for now.
// 1
let codingKey = keys.last!
let key = codingKey.stringValue
// 2
guard key.contains("-") else { return codingKey }
// 3
let words = key.components(separatedBy: "-")
let camelCased = words[0] +
words[1...].map(\.capitalized).joined()
raywenderlich.com 219
Expert Swift Chapter 8: Codable
return ???
1. Get the last coding key in the array. The array of coding keys represents the entire
path from the root of the JSON response to the specific key you’re working on, so
you’re only interested in the last one, in this case.
2. If the key doesn’t contain a dash, it’s definitely not kebab-case, so you return the
coding key as-is.
3. If it is kebab-case, you split the key by the dashes and capitalize every word but
the first one.
camelCased now contains the kebab-case-turned-camel-case key you need. But what
should you actually return at the end? A CodingKey!
Unfortunately, you can’t just instantiate one because it’s a protocol. But you can
easily write your own concrete helper type to aid in this task. Add the following piece
of code to your playground:
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
This AnyCodingKey type simply lets you instantiate a CodingKey with either an
integer or a string. This exact implementation is actually part of Apple’s
documentation, but unfortunately, it’s not currently part of the standard library.
With your new AnyCodingKey ready, you can finalize your custom key decoding
implementation.
raywenderlich.com 220
Expert Swift Chapter 8: Codable
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.keyDecodingStrategy = .convertFromKebabCase
This will tell JSONDecoder to use your new custom key decoding strategy.
Run your playground page for the final time and everything should work as it did
before. Hooray!
Getting this API to parse beautifully and elegantly with Swift’s type system took a bit
of work, but look how much you’ve learned so far. And you’re just getting started.
Time for the next challenge!
To get started, switch over to the playground page, 2. Magic the Gathering and look
around. The playground already includes some boilerplate code to save time, which
gets the API response and decodes it into a basic struct where all keys are
automatically synthesized.
Run the playground and you’ll see that the basics are already up and running:
$ Archangel of Thune #8
$ Thoughtseize #110
$ Batterskull #130
$ Force of Will #28
$ Krenko, Mob Boss #15
$ Rishkar’s Expertise #123
raywenderlich.com 221
Expert Swift Chapter 8: Codable
Open magic.json in the Resources folder to get a glimpse of the somewhat complex
data structure you’ll work on in this section. When you’re ready, move over to the
next section.
The response uses a string for this, but it could have a much nicer, typed
representation in Swift.
To do this, you’ll add two new data types: Card.Mana, which will represent the mana
cost, and include an array of Card.Mana.Color — an enum of the various mana color
options.
Add the following piece of code to the end of the playground page:
extension Card {
/// Card's Mana
struct Mana: CustomStringConvertible {
// 1
let colors: [Color]
// 2
var description: String { colors.map(\.symbol).joined() }
}
}
extension Card.Mana {
/// Card's Mana Color
enum Color {
// 3
case colorless(Int)
case extra
case white
case blue
case black
case red
case green
// 4
var symbol: String {
switch self {
case .white: return "W"
case .blue: return "U"
case .black: return "B"
case .red: return "R"
raywenderlich.com 222
Expert Swift Chapter 8: Codable
3. The Color enum contains cases for all individual mana colors, as well
as .colorless, which has an associated value of the number of colorless mana.
4. Finally, the mana color has a symbol computed property to print out the single-
character symbol of the mana color.
So far, so good. But how do you actually make Mana conform to Decodable in a way
that processes the source string 5{W}{W}? Codable obviously can’t know how to
translate this information automatically.
First, you’ll add an initializer to Card.Mana.Color that accepts the mana symbol.
You’ll use this in the Decodable conformance of Mana itself.
init?(symbol: String) {
if let value = Int(symbol) {
self = .colorless(value)
return
}
switch symbol.lowercased() {
case "w":
self = .white
case "u":
self = .blue
case "b":
self = .black
case "r":
self = .red
case "g":
raywenderlich.com 223
Expert Swift Chapter 8: Codable
self = .green
case "x":
self = .extra
default:
print("UNKNOWN \(symbol)")
return nil
}
}
This initializer simply accepts the symbol, such as W or 3, and returns the appropriate
enum case. If the value is numeric, it returns the .colorless case with the associated
numeric value.
Before you move on to the next section, you’ll want to learn more about one of the
foundations of how encoding and decoding are structured in Codable — containers.
Understanding containers
If you’ve ever written a custom Decodable or Encodable initializer, it’s quite likely
you’ve worked with a decoding or encoding container, accordingly:
• Keyed Container: The most common kind of container. In the example above, you
decode a dictionary keyed by a set of CodingKeys, hence the name.
• Unkeyed Container: As its name suggests, you use this to decode structures that
don’t have string-based coding keys — arrays, for example.
• Single Value Container: Use this when decoding a single value into some
concrete type. In the example above, you could have a Status enum with a custom
decoder that uses a single value container to resolve the decoded value.
raywenderlich.com 224
Expert Swift Chapter 8: Codable
Looking at the example above, the top keyed container lets you work in the context
of the keys id, status, name, family and location, while the inner-most
coordinate nested container lets you decode in the context of the longitude and
latitude keys.
You’ll use both keyed and single-value containers during this chapter.
With:
Swift automatically synthesizes this initializer for you when it can determine how to
properly decode your response. For example, if the coding keys accurately match the
response. More often then not, though, you’ll want more granular control, as in this
case.
Remember that decoding mana works on a simple String and not a dictionary or
array. As mentioned above, the solution is to use a single value container.
raywenderlich.com 225
Expert Swift Chapter 8: Codable
You first ask the decoder for a single-value container. You’re basically telling the
decoder: “Hey there, this type is only dealing with a single value type, not a
dictionary or other complex structure.”
Because this container works on a single value, it doesn’t need coding keys, and you
can simply attempt to decode it as the String you’re expecting.
Now that you have the raw string, for example "{5}{W}{W}", you can perform the
necessary processing to break it down into an array of Colors. Finish your initializer
with the following block of code:
// 3
guard !symbol.isEmpty,
let color = Color(symbol: symbol) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unknown mana symbol \(symbol)")
}
// 4
return color
}
1. Separate the string by the } sign, getting an array such as ["{5", "{W", "{W",
""]. You then use dropLast() to get rid of the empty string at the end.
2. Strip the first character for each of the array parts, which leaves you with ["5",
"W", "W"].
3. Attempt to create a new Mana.Color instance by passing the clean symbol to the
initializer you added in the previous section. If the initializer returns nil or the
symbol is empty, you throw a DecodingError.dataCorruptedError to notify
the consumer of the unexpected result.
You’ve worked so hard, so how about actually using your Mana object?
raywenderlich.com 226
Expert Swift Chapter 8: Codable
Finally, in the for loop at the top of the file, modify print to look as follows:
Run the playground, then check the output produced by Mana’s conformance of
CustomStringConvertible:
Although the produced output is a simple String, your data model is now an actual
concrete enum you can work with inside Swift’s type system.
extension Card {
enum Rarity: String, CustomStringConvertible, Decodable {
case common = "Common"
case uncommon = "Uncommon"
case rare = "Rare"
case mythicRare = "Mythic Rare"
case special = "Special"
case land = "Basic Land"
raywenderlich.com 227
Expert Swift Chapter 8: Codable
}
}
In the code above, you create a simple Rarity enum with the appropriate cases,
which also provides a custom String value for each case. This is similar to a single-
value container, except that Swift takes care of that boilerplate for you behind the
scenes.
Also, add , \(card.rarity) to the end of the print statement at the top to see the
result of your new property:
You have three more properties to take care of as part of this section. Unfortunately,
they all require Card to provide a custom Decodable initializer.
To save some time, add the following initializer and CodingKeys enum to Card:
raywenderlich.com 228
Expert Swift Chapter 8: Codable
The code above is basically the manual implementation of what Swift automatically
synthesized for you so far. But as mentioned earlier, you can easily get to cases where
your models don’t directly correlate to your response. In this case, Codable gives you
the ability to take matters into your own hands.
Unfortunately, set, setName, power and toughness are scattered in the JSON
response. Wouldn’t it be much more aesthetic and “Swifty” to put them in their own
structures? Why, of course!
Start by adding the following two structs to the end of your playground:
extension Card {
struct Attributes {
let power: String
let toughness: String
}
}
extension Card {
struct Set {
let id: String
let name: String
}
}
This code defines Card.Attributes, which unifies power and toughness and
Card.Set, which unifies set and setName.
In this specific case, you can’t simply conform these objects to Decodable, because
they don’t work on a single property of your original response. You’ll want to decode
each value as needed and construct these objects by hand to achieve the wanted
result.
raywenderlich.com 229
Expert Swift Chapter 8: Codable
Next, at the end of your custom Decodable initializer, add the following code:
// 1
// Set
self.set = Set(id: try container.decode(String.self,
forKey: .set),
name: try container.decode(String.self,
forKey: .setName))
// 2
// Attributes
if let power = try container.decodeIfPresent(String.self,
forKey: .power),
let toughness = try container.decodeIfPresent(String.self,
forKey: .toughness) {
self.attributes = Attributes(power: power,
toughness: toughness)
} else {
self.attributes = nil
}
In the code above, you attempt to create both Set and Attributes:
2. power and toughness are optional, as they only apply to creature cards. You use
decodeIfPresent to try and decode the two values from the container. If they
exist, you initialize a new instance of Attributes from them. Otherwise, you set
Attributes to nil.
You now have most of the data from the JSON response decoded into your Card
model. Replace the print statement inside the for loop at the top of your
playground with the following:
print(
"$ \(card.name) #\(card.number) is a \(card.rarity)",
"\(card.type) and needs \(card.manaCost).",
"It's part of \(card.set.name) (\(card.set.id)).",
card.attributes.map { "It's attributed as \($0.power)/\
($0.toughness)." } ?? ""
)
Here, you simply print out everything about the card in a prettified way and
conditionally print the card’s attributes using Optional’s map method.
raywenderlich.com 230
Expert Swift Chapter 8: Codable
Run the playground and you’ll see output like the following:
You’ll wrap up this section with one final property. How exciting!
To be honest, though, the ruling dates aren’t too interesting. Having a simple array
of String rulings instead would be great, in this case.
Now, it’s time to add the decoding you need in Card’s initializer. You can take one of
two approaches:
1. Define a private Ruling struct that conforms to Decodable, only to extract its
text.
2. Decode the property directly to a Dictionary and extract only its text key
directly.
Although both approaches are entirely valid, you’ll use the second option today.
Despite this rather lengthy introduction, you’ll only need to add the following two
lines to the end of the initializer:
// Rulings
let rulingDict = try container.decode([[String: String]].self,
forKey: .rulings) // 1
self.rulings = rulingDict.compactMap { $0["text"] } // 2
raywenderlich.com 231
Expert Swift Chapter 8: Codable
1. Decode the rulings key into a raw array of dictionaries. This correlates directly
to the way rulings is structured in the API response.
2. Use compactMap on the array and try to extract only the text of each ruling
dictionary.
Rulings are quite verbose, so you’ll add a separate print statement for them. Inside
the for loop at the top of the file, add:
Run the playground, and you’ll see the various rulings printed out for each of the
cards:
Wow, you’ve gone through a massive decoding session here. Kudos to you! Now, it’s
finally time to head over to the last Decodable challenge for this chapter.
Sometimes, you get something special to work on. A massive, non-standard response
that makes you scratch your head and say: “I have no idea how to decode this thing!”
This section will deal with an API in this very category: Alpha Vantage.
Alpha Vantage provides free financial APIs around Stocks, FX, Cryptocurrency etc.
The one you’ll work with today is the Stocks API.
raywenderlich.com 232
Expert Swift Chapter 8: Codable
Open av_1min.json in the Resources folder and you might notice some of the
challenges this response poses:
1. All of the static keys are relatively strange, and some are numbered.
2. The Time Series key is actually dynamic, depending on the interval in minutes
you ask of the API. It could be Time Series (1 min), Time Series (5 min), or
any other key.
3. If that’s not enough, each key inside the Time Series key is both dynamic and a
date.
Yikes.
This response might be quite painful to deal with on its own, but Swift’s strictly
typed nature makes this even more challenging. It’s nothing you can’t deal with,
though!
{
"Meta Data": {
"1. Information": "Intraday (1min) open...",
"2. Symbol": "RAY",
"3. Last Refreshed": "2020-08-14 20:00:00"
},
"Time Series (1min)": {
"2020-08-14 20:00:00": {
"1. open": "101.9000",
"2. high": "102.0000",
"3. low": "101.9000",
raywenderlich.com 233
Expert Swift Chapter 8: Codable
To start with the basics, add the following two properties to the Stock struct:
// 2
enum CodingKeys: String, CodingKey {
case metaData = "Meta Data"
case updates = "Time Series (1min)"
}
// 3
enum MetaKeys: String, CodingKey {
case info = "1. Information"
case symbol = "2. Symbol"
case refreshedAt = "3. Last Refreshed"
}
raywenderlich.com 234
Expert Swift Chapter 8: Codable
That piece of code causes a compilation error, but don’t worry about that for now. It:
2. Defines CodingKeys for that top level, which represents the metadata and time
series keys.
3. Define MetaKeys to represent the individual keys inside the metadata key.
With that done, you can now decode your stock’s info and symbol properties. But
how?
If you remember the explanation about containers earlier in this chapter, the answer
might already pop into your head — simply use a nested container.
In the following line, you ask the top-level container to give you a new, nested
container, which represents the metadata. Basically, you’re asking the top container:
“Give me a new decoding context for the .metaData key that I can decode using the
MetaKeys coding keys”.
Now that you have access to the meta data scope, all that’s left is decoding the
individual properties. Add the following lines immediately after the previous one:
You decode the two individual String values from their appropriate keys. Notice how
the available keys in the forKey argument are the ones from MetaKeys since that’s
the decoding container you’re working on at the moment.
Swift’s type system really helps with getting the right keys for the task.
raywenderlich.com 235
Expert Swift Chapter 8: Codable
Great work! With that done, there are still two more pieces of information to decode:
the last refresh date and the individual updates for the stock.
Then, add the following line at the end of Stock’s initializer to decode the date:
By default, decoding to date expects a Unix timestamp as its source. But in Alpha
Vantage’s API, you have a string date formatted like: 2020-08-14 20:00:00.
Fortunately, much like JSONDecoder’s key-decoding strategy, you can also pick a
date-decoding strategy. As of the time of writing this chapter, there are six date-
decoding strategies in JSONDecoder.
Although you might be tempted to reach to the custom decoding strategy, like in the
case of a key decoding strategy, there’s a much more fitting strategy called
formatted which takes a DateFormatter and uses it to decode the Date. It’s time for
you to try this out.
You just created a DateFormatter with the appropriate dateFormat for Alpha
Vantage’s JSON response.
raywenderlich.com 236
Expert Swift Chapter 8: Codable
Now, all you have to do is apply the decoding strategy to your decoder. After creating
JSONDecoder inside getStock(interval:), add the following line:
decoder.dateDecodingStrategy = .formatted(dateFormatter)
This will cause JSONDecoder to always defer decoding of Dates to the underlying
NSDateFormatter.
Run your playground again and everything should work, with output similar to the
following:
Excellent, everything is almost ready to go, but you still have a big challenge ahead —
decoding the individual updates in the time series key.
In this case, it would be much nicer to have an array of updates available directly on
Stock, instead of needing to dig through a dictionary of dictionaries, like in the
original response.
You’ll start bottom-up, by defining what an update should look like. Add the
following extension to the end of your playground:
extension Stock {
struct Update: Decodable, CustomStringConvertible {
// 1
let open: Float
let high: Float
let low: Float
let close: Float
let volume: Int
var date = Date.distantPast
// 2
enum CodingKeys: String, CodingKey {
raywenderlich.com 237
Expert Swift Chapter 8: Codable
// 3
self.open = try Float(container.decode(String.self,
forKey: .open)).unwrapOrThrow()
self.high = try Float(container.decode(String.self,
forKey: .high)).unwrapOrThrow()
self.low = try Float(container.decode(String.self, forKey:
.low)).unwrapOrThrow()
self.close = try Float(container.decode(String.self,
forKey: .close)).unwrapOrThrow()
self.volume = try Int(container.decode(String.self,
forKey: .volume)).unwrapOrThrow()
}
// 4
var description: String {
"\(date)|o:\(open),h:\(high),l:\(low),c:\(close),v:\
(volume)"
}
}
}
This code represents what each update looks like. It’s meant to decode the internal
dictionaries inside each dated key in the time series key.
1. You create properties to match the individual updates in the response. You’ve
also added a date property, which you’ll use momentarily.
2. You define the necessary custom coding keys for each of the decoded properties.
3. Because the values in the response are strings, you decode them as such and
attempt to cast them to Float or Int, as needed. To verify the casting succeeded,
you also use a custom unwrapOrThrow() on Optional, which can be found in
Optional+Ext.swift.
raywenderlich.com 238
Expert Swift Chapter 8: Codable
The tricky part is that each update is keyed under an unknown, dynamic key. Not
only that, but that key is also the date you wish to associate with each Update.
Dynamic keys… um, does that ring any bells? It’s time for some AnyCodingKey
magic!
This specific case is a bit trickier — you don’t even know what the dynamic keys you
need to create are, so you’ll start by getting those.
Because you don’t know the dynamic keys inside the Time Series (1 min) key, you
simply decode the entire structure as a raw dictionary of dictionaries. You then get
only the keys (i.e., the dates) and create new AnyCodingKeys from them. Finally, you
store them in timeKeys.
To use these keys, you’ll need to use a nested container once again. Add the
following line:
As before, you create a nested container for the .updates coding key, meaning Time
Series (1 min). This time, however, note it’s keyed by AnyCodingKey because
there’s no finite set of keys like your enum-based coding keys.
Everything is now in place to decode your individual updates. All you have to do is
iterate over the new keys you’ve created, and decode each of them.
raywenderlich.com 239
Expert Swift Chapter 8: Codable
// 1
self.updates = try timeKeys
.reduce(into: [Update]()) { updates, currentKey in
// 2
var update = try timeContainer.decode(Update.self,
forKey: currentKey)
// 3
update.date = dateFormatter
.date(from: currentKey.stringValue) ?? update.date
// 4
updates.append(update)
}
.sorted(by: { $0.date < $1.date }) // 5
This might look a bit complicated, but it’s easier once you break it down. In the code
outlined above, you:
1. Use reduce(into:) on the timeKeys you created. You start with a seed of an
empty array of Updates, which you’ll fill up as you go.
2. Use the timeContainer, the one scoped to the time series key, to decode the
individual update found in the currentKey key.
3. Use the same DateFormatter you created earlier and try to parse the string date
into a Date object. This is because, as mentioned before, the date of the update is
actually the key itself.
4. Append the new update into the reduced result array: updates.
5. Sort all updates by date, once they are parsed out of a dictionary.
print("\(stock.symbol), \(stock.refreshedAt):",
"\(stock.info) with \(stock.updates.count) updates")
for update in stock.updates {
_ = update.open
raywenderlich.com 240
Expert Swift Chapter 8: Codable
(update.volume)")
}
Here, you simply print out the stock info and each individual update. Run your
playground and you’ll see output similar to the following:
Click the show result icon next to the line that says _ = update.open:
You’ll see a nice graph showcasing the opening value updates for the stock:
Note: If you can’t see the graph, right-click the value area and change the
mode to Graph.
How cool is that? Through some fiddling, nested containers, and custom raw
AnyCodingKeys, you massaged this extremely massive and oddly structured JSON
response to a strictly typed, easily consumable Swift struct.
You’ve got all of your decoding logic in place, but there’s still something missing.
Can you guess what it is? Handling other time intervals!
raywenderlich.com 241
Expert Swift Chapter 8: Codable
Run the playground, and you’ll see an error similar to the following:
Whoops! The code you’ve written always expects the specific key Time Series (1
min), but in this case, the interval is actually 15 minutes.
You could use an AnyCodingKey here as well, but how would you know what the API
call’s interval is from inside your decodable initializer? You have no access to that
data in that scope.
extension CodingUserInfoKey {
static let timeInterval = CodingUserInfoKey(
rawValue: "timeInterval"
)!
}
raywenderlich.com 242
Expert Swift Chapter 8: Codable
Now that the decoder has all the needed information, you need to modify your
Stock’s initializer accordingly. Replace the following line in Stock.init(from:):
// 1
guard let time = decoder.userInfo[.timeInterval] as? Int else {
throw DecodingError.dataCorrupted(
.init(codingPath: [],
debugDescription: "Missing time interval")
)
}
// 2
let metaKey = AnyCodingKey(stringValue: "Meta Data")!
let timesKey = AnyCodingKey(stringValue: "Time Series (\
(time)min)")!
// 3
let container = try decoder.container(
keyedBy: AnyCodingKey.self
)
1. Access the decoder’s user info and try to get the passed time interval. If it doesn’t
exist, you throw an error.
2. Create two AnyCodingKeys representing the metadata and time series key, using
the dynamic time interval. You switch the metadata key to AnyCodingKeys
because a container has to be decoded by a single concrete type.
raywenderlich.com 243
Expert Swift Chapter 8: Codable
With that completed, run your playground a final time and you’ll see that everything
works as expected, with the interval dynamically passing down to the decoder:
Encoding
You’ve spent the majority of this chapter working on decoding because that’s where
most of the challenges arise in daily work. But you’re definitely not going to leave
this chapter without at least touching a bit on the other side of the equation:
encoding.
In the decoding process, you take an external representation (such as JSON) and
decode it into a Swift type. On the other hand, encoding lets you describe how to
encode arbitrary Swift types into various external representations.
In this final section, you’ll write your own Encodable conformance for a Swift struct
to explore the various options at your disposal.
2. An instance of Customer.
3. Code using JSONEncoder() to print out the JSON representation resulting from
the encoding.
Note that, like Decodable, adding the Encodable protocol conformance is enough
for Swift to automatically synthesize encoding for your struct by using the same keys
as its properties.
Also, notice that JSONEncoder.encode(_:) returns Data and not a String. You use
String(data:encoding:) to make things nice and readable.
raywenderlich.com 244
Expert Swift Chapter 8: Codable
Run your playground to see the default output you get from JSONEncoder
(abbreviated):
{"street":"3828 Piermont
Drive","atmCode":"1132","city":"Albuquerque",...}
Encoder customizations
Like JSONDecoder’s various customizable properties, JSONEncoder packs quite a
punch. The first customization opportunity for you is output formatting. Add the
following line immediately after creating your JSONEncoder:
encoder.outputFormatting = [.prettyPrinted,
.sortedKeys,
.withoutEscapingSlashes]
Run your playground and you’ll see output similar to the following:
{
"accessKey": "S|_|p3rs3cr37",
"addedOn": 619552896.56153595,
[...]
"website": "http://github.com/freak4pc",
"zip": 87112
}
Wow, this already looks quite different. The code you’ve added customizes how
JSONEncoder outputs its data:
• .prettyPrinted: Properly indents and adds new lines to the JSON to make it
easier to read.
• .sortedKeys: Sorts the JSON keys alphabetically. Notice how the initial JSON keys
look, and how the current output looks. It starts with accessKey and ends with
zip.
Encoding strategies
Exactly like Decodable has decoding strategies, Encodable has encoding
strategies: keyEncodingStrategy, dateEncodingStrategy and
dataEncodingStrategy.
raywenderlich.com 245
Expert Swift Chapter 8: Codable
Add the following two lines after the previous one to try these out:
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
Run your playground and you’ll see that all your keys have automatically been
converted to snake case and that the date is now presented as ISO8601 instead of a
regular timestamp.
Before:
"addedOn": 619553518.18203104
After:
"added_on": "2020-08-19T18:12:23Z"
You could create a custom encoder and manually encode and decode these values as
needed, but a more elegant option is to add an intermediate type responsible for
this.
// 1
let key = SymmetricKey(data:
"Expert Swift !!!".data(using: .utf8)!)
// 2
init(stringLiteral value: StringLiteralType) {
self.value = value
}
// 3
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let combined = try container.decode(Data.self)
let result = try AES.GCM.open(.init(combined: combined),
raywenderlich.com 246
Expert Swift Chapter 8: Codable
using: key)
self.value = String(data: result, encoding: .utf8) ?? ""
}
// 4
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let data = value.data(using: .utf8)!
let sealed = try AES.GCM.seal(data, using: key)
try container.encode(sealed.combined)
}
}
1. You define a CryptoKit symmetric key, which uses a hard-coded weak key for
educational purposes. In production, you should use a strongly hashed key stored
securely in your keychain.
{
"access_key" : "6CFbMLD0IojD7MaJwDH[...]iS4cr9i2vu0C2N/Q=",
"atm_code": "mZjZ17+VM8Nh0e3DwceF8hfT/6gplOD+n5c/jpNVIws="
[...]
}
raywenderlich.com 247
Expert Swift Chapter 8: Codable
Your strings were encrypted specifically when encoded to JSON, and all you had to do
was replace the String type with your new type.
This concept has many useful applications: conforming custom property wrappers,
custom formats specific to encoding, etc.
You got some free auto-synthesized encoding, but like decoding, it’s quite common
to want a different output or structure for your consumers than you have in your app.
Here you define three sets of coding keys: the first for the top level, and two more
sets of coding keys specific for the address details and contact info.
raywenderlich.com 248
Expert Swift Chapter 8: Codable
You simply ask the encoder for a keyed container based on CustomerKeys to build
the top-level response. Notice that the customer container is mutable so you can
write into it.
{
"access_key": "AwENrgpbFvL[...]XS57sWpOQ==",
"added_on": "2020-08-19T18:44:27Z",
"atm_code": "AwFkTXStHHy[...]FoUOdAGHfjuUwkw==",
"name": "Shai Mishali"
}
Only the keys you’ve encoded are part of the response. This is great because it means
you can customize your response to whatever you see fit for your consumer.
1. Ask the main container, customer, for a nested keyed container. The first one is
based on AddressKeys and the second on ContactInfoKeys.
2. Simply encode the appropriate properties from Customer into each container.
Once you have these nested containers, each represents its own encoding
context.
raywenderlich.com 249
Expert Swift Chapter 8: Codable
{
"access_key": "AwE4[...]DFL+m6NOPNw==",
"added_on": "2020-08-19T18:50:26Z",
"address": {
"city": "Albuquerque",
"street": "3828 Piermont Drive",
"zip": 87112
},
"atm_code": "AwGw[..]Y7w==",
"contact_info": {
"cellular_phone": "+972 542-288-482",
"email": "[email protected]",
"home_phone": "+1 212-741-4695"
},
"name": "Shai Mishali"
}
Notice how both address and contact_info represent their appropriate keys and
the nested containers you’ve defined.
This is great! Think about it — you could have a Server-Side Swift app that uses
custom encoding to provide a consumer-friendly representation of your data models
without compromising your own Swift models.
As an exercise to you, the reader, try writing the Decodable part of Customer to
“reverse” the encoding JSONEncoder is doing. You’ll be surprised by how similar the
Decodable and Encodable implementation will end up looking like.
raywenderlich.com 250
Expert Swift Chapter 8: Codable
Key points
You covered a lot in this chapter. Here are some of the key takeaways:
• Codable is a mechanism that lets you define how to decode and encode various
data representations in and out of your models.
• Swift can do a lot of the heavy lifting for you automatically if your keys match up
perfectly with your properties.
• When the automatically synthesized code just doesn’t cut it, you have full control
to customize decoding, encoding or both.
• You can use key strategies, data strategies, date strategies etc. to further customize
how specific encoders and decoders treat specific types of properties or their keys.
• When writing your own custom encoding and decoding, you can use various types
of containers to gain access to different encoding and decoding contexts.
• If your keys are dynamic or aren’t known in advance, you can still leverage
Codable by using an AnyCodingKey type as needed.
Wow, what a chapter this has been! You’ve learned almost everything there is to
know about decoding data and touched a bit on how to encode data and customize
the encoding using various container types.
There’s still much more to learn and experiment with: abstracting containers for
shared structures, leveraging property wrappers with their own Codable
conformances, writing custom encoders and decoders for custom types, and more,
but there are only so many pages in this book.
Now, you’re ready for anything this JSON-centric API world has to throw at you!
raywenderlich.com 251
9 Chapter 9: Unsafe
By Ehab Amer
Swift is an easy language to learn. It can take care of a lot of things for you and help
you keep your code safe and clear to minimize bugs. If you were to compare it to C++,
many people would say C++ is harder. Swift takes care of type checking, memory
allocation, and many things on your behalf so you can focus on what you want to do
in your code, and not how the machine will handle your code. But C++ gives you more
power and more control. As we’re told in the Spider-Man comics and movies, “With
great power comes great responsibility”.
By default, Swift is a memory-safe and type-safe language. This means you cannot
access uninitialized memory and can only treat an instance as the type it was
created. You can’t treat a String as if it were an Int or a Numeric and vice-versa. But
this doesn’t cover completely what the word safe means.
For a more general description, Swift validates any input, whether it’s valid or
invalid, and behaves accordingly. So storing a number in a string property, for
example, will fail. Additionally, forcing a value from an optional that doesn’t have a
value is not a valid behavior. Neither is storing a number that exceeds the maximum
allowed value of your variable. All of those are different cases related to safety.
In some cases, you might need your code to be extremely optimized, in which case
the tiny overhead added by the safety checks from Swift might be too expensive. You
might be dealing with a huge stream of real-time data, manipulating large files or
other large operations that deal with large data. Or you might even be working with
C++ code within your app. In such cases, you want to have full control over your
objects, or in other words: Pointers.
raywenderlich.com 252
Expert Swift Chapter 9: Unsafe
In this chapter, you’ll learn how you can gain this control. You’ll learn about:
• The memory layout of types, and what size, alignment and stride are
• Binding memory to a type and the rules you must follow to rebind it to another
type
• Other unsafe operations in the standard library and overflow arithmetic operations
But before going into those points, you need to understand a few things first.
Before going deeper into what this keyword means, you must understand how Swift
behaves when you violate any of the type safety rules. Some violations are checked at
compile time, while others are checked during runtime — and those consistently
cause a runtime crash. A rule to remember: Safe code doesn’t mean no crashes. It
means that if your code received unexpected input, it will stop execution. One of the
ways it can do that is to throw a fatal error. But with unsafe code, it will use the
invalid input, work with it and eventually — maybe — provide an output. Such
situations are hard to debug.
This is how the keyword unsafe works. The moment a rule is violated, the behavior
of your code is completely unknown. Your code might crash, or it might resume. It
might give you a wrong value or change the value of another property. How your
application will proceed is undefined and can change from one execution to another.
It’s extremely important to know how your code will behave and what to expect once
you start using unsafe so you’re careful with it.
The Swift standard library provides pointers for unsafe that are similar in concept to
C++ pointers. There is no better way to learn how to use these pointers than to
understand how they work.
raywenderlich.com 253
Expert Swift Chapter 9: Unsafe
What is a pointer?
Swift has a linear memory layout, so imagine your app’s address space is from 0x0000
to 0xFFFF. The actual address space is represented by 64 bits rather than 16. But to
keep it simple here, this chapter will use smaller numbers.
This address space contains the executable of your app, dynamic libraries, assets, etc.
A pointer is something that points to a specific address in memory. So say that you
created an object at 0x0AC4, and this object was 4 bytes in size. This object’s memory
space extends from 0x0AC4 to 0x0AC7.
To write in memory, you specify what you want to write at which address. This means
that if you want to write the number 9 in the second byte (index 1), you specify in the
application to go to address 0x0AC4 + 1, and write 9.
If you happen to write 0x0AC4 + 100, then you might overwrite an existing value and
corrupt your app’s state. This is clearly out of bounds for your object. But in this case,
you’re trusted to know what you’re doing.
Another possible problem is the object that was in this address location was deleted,
but you still have the pointer to this location and used it to store a value. In this case,
you’ll overwrite an existing object or an undefined behavior will occur. You might not
know what will happen, but this will cause a problem.
A pointer is simply the address of where your information resides in memory. The
size of the object in memory is a different story, which you’ll cover shortly.
Now that you think about it, what is the difference between reference and pointer?
A reference can also be described as something that refers to an object in memory,
which is the same thing as a pointer.
raywenderlich.com 254
Expert Swift Chapter 9: Unsafe
Memory layout
To use pointers properly, you must understand how memory itself is organized.
Memory layout for value types is quite different than the layout of reference types.
This section will start by covering value types.
• Size: This refers to the number of bytes it takes to store a value of this type. A size
of four means this type requires four bytes of storage.
• Stride: This refers to how many bytes to increment on your pointer to read the
next object.
The size influences the other two values, as the alignment and stride can never be
smaller. Size has a clear meaning, but the other two require explanation.
raywenderlich.com 255
Expert Swift Chapter 9: Unsafe
Consider a type that has an alignment value of four. This means this type must be
stored on an address divisible by four. So why does this matter?
Imagine that the device reads only four bytes at a time. Such a system can read only
bytes 0-3, 4-7, 8-11, etc. Imagine now that an object was stored from bytes 2 through
5. To read these bytes from memory, the device would need to read the first set of
bytes, then cut out only its second half (2-3), then read the second set, and then,
finally, concatenate the first half (4-5) with the previous half to properly construct
the object’s value. This describes a misaligned value.
Misaligned values severely affect performance. Compilers keep this from happening
even if it costs more memory.
For stride, imagine you have an array of items in which each item takes up eight
bytes of memory. Here, it makes sense that each time you want to read the next
value, you do so by incrementing the pointer by the item size.
But now imagine that the item size is nine. If the compiler depended only on the size
to increment the pointer, then there would be misaligned objects. And as you just
learned, the compiler actively tries to avoid these.
With an item size of nine, you would need to have a stride of 16. Thus, moving the
pointer that accesses the first item by 16 bytes will point to the second item. Those
remaining 7 bytes (16 minus 9) are called padding and aren’t used at all.
On a 64-bit system, an Int will have size, alignment and stride with a value of
eight.
Note: On a 64-bit system, Int defaults to the Int64 type, which has eight
bytes. On a 32-bit system, it defaults to Int32, which has four bytes.
raywenderlich.com 256
Expert Swift Chapter 9: Unsafe
MemoryLayout<Int16>.size // returns 2
MemoryLayout<Int16>.alignment // returns 2
MemoryLayout<Int16>.stride // returns 2
MemoryLayout<Bool>.size // returns 1
MemoryLayout<Bool>.alignment // returns 1
MemoryLayout<Bool>.stride // returns 1
MemoryLayout<Float>.size // returns 4
MemoryLayout<Float>.alignment // returns 4
MemoryLayout<Float>.stride // returns 4
MemoryLayout<Double>.size // returns 8
MemoryLayout<Double>.alignment // returns 8
MemoryLayout<Double>.stride // returns 8
You could also use type inference to determine the same values:
In the next section, you’ll see how a combination of types affects the memory layout
of the struct itself.
Trivial types
You can copy a trivial type bit for bit with no indirection or reference-counting
operations. Generally, native Swift types that don’t contain strong or weak references
or other forms of indirection are trivial, as are imported C++ structs and enums.
In other words, the basic data types such as Int, Float, Double and Bool are all
trivial types. Structs or enums that contain those value types and don’t contain any
reference types are also considered trivial types.
struct IntBoolStruct {
var intValue: Int
var boolValue: Bool
}
This is a struct that has the first property as an Int and the second property of Bool.
raywenderlich.com 257
Expert Swift Chapter 9: Unsafe
MemoryLayout<IntBoolStruct>.size // returns 9
MemoryLayout<IntBoolStruct>.alignment // returns 8
MemoryLayout<IntBoolStruct>.stride // returns 16
An Int has a size of 8, and a Bool has a size of 1, so it makes sense the struct has a
size of 9.
For the alignment, it makes sense that it is 8 to ensure that intValue is not
misaligned. As for stride, it has a value of 16 to maintain the alignment and to
reserve enough space for the struct. It can’t be 9, nor can it be 8.
Ordering properties
Now, consider this other example:
struct BoolIntStruct {
var boolValue: Bool
var intValue: Int
}
MemoryLayout<BoolIntStruct>.size // returns 16
MemoryLayout<BoolIntStruct>.alignment // returns 8
MemoryLayout<BoolIntStruct>.stride // returns 16
This struct is almost identical to the previous one except for the order of the
properties inside it: The boolean appears before the integer.
For the struct to be aligned, all the properties inside it must also be aligned. To have
the boolean property stored before the integer, this means that a seven-bit padding
is required right after the boolean to allow the integer to be properly aligned. This
causes the padding to be considered in the size of the struct directly. The alignment
and the stride values are the same as in IntBoolStruct.
raywenderlich.com 258
Expert Swift Chapter 9: Unsafe
struct EmptyStruct {}
MemoryLayout<EmptyStruct>.size // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride // returns 1
This struct has no properties at all, so it’s logical to have a size of zero bytes. But you
can’t have a non-existent object in memory. Anything in memory should have a size!
Thus, one byte is allocated to the object, and this value is represented in the stride.
This is why you should depend on stride instead of size when you allocate memory
yourself.
Reference types
Reference types have a quite different memory layout. When you have a pointer of
such a type, you’re pointing to a reference of that value and not the value itself.
Think of it as if you have a pointer on a pointer.
class IntBoolClass {
var intValue: Int = 0
var boolValue: Bool = false
}
MemoryLayout<IntBoolClass>.size // returns 8
MemoryLayout<IntBoolClass>.alignment // returns 8
MemoryLayout<IntBoolClass>.stride // returns 8
class BoolIntClass {
var boolValue: Bool = false
var intValue: Int = 0
}
raywenderlich.com 259
Expert Swift Chapter 9: Unsafe
MemoryLayout<BoolIntClass>.size // returns 8
MemoryLayout<BoolIntClass>.alignment // returns 8
MemoryLayout<BoolIntClass>.stride // returns 8
class EmptyClass {}
MemoryLayout<EmptyClass>.size // returns 8
MemoryLayout<EmptyClass>.alignment // returns 8
MemoryLayout<EmptyClass>.stride // returns 8
All three classes have a value of eight for their size, alignment and stride.
Regardless of whether the class had properties, the values remained the same.
Pointer types
Swift provides different pointer types. Each provides its own control safety levels or
unsafety levels.
• UnsafeRawPointer
• UnsafePointer<Type>
The first is the basic raw pointer that doesn’t know any information of the type it is
pointing at. It’s a basic pointer on a specific byte.
The second is a pointer that knows the type of the object it points at. It’s also called a
typed pointer.
Raw pointers can’t work on reference or non-trivial types. For those, you must use a
typed pointer.
If you’re working with arrays, there’s a set of pointer types that can make things a
little easier for you:
• UnsafeRawBufferPointer
• UnsafeBufferPointer<Type>
All those pointers are read-only access. To allow read-and-write access, you need
mutable pointers. Any of the above pointer types can be mutable, as seen below:
• UnsafeMutableRawPointer
• UnsafeMutablePointer<Type>
• UnsafeMutableRawBufferPointer
• UnsafeMutableBufferPointer<Type>
raywenderlich.com 260
Expert Swift Chapter 9: Unsafe
This gives you a total of eight different pointer types you can use. But still … what is
the need for all those, and what is the real difference?
Raw pointers
To understand raw pointers, consider the following example. Create a playground
and add the following code:
This creates an unsigned-Int-16 with the value 4386. Then, as you saw previously,
you’re checking the values of size, stride and alignment. As expected, the UInt16
has a size of two bytes. How about using a raw pointer to get the value of each byte
separately?
This defines a new mutable raw pointer, allocates two bytes for it and specifies that it
must have an even alignment (alignment of two). For pointers, you’re taking care of
everything. You’re responsible for allocating the memory and for deallocating it.
Don’t forget the deallocation part or your code will leak memory. Afterward, you
store the hex value 0x1122, which is equivalent to 4386 as an UInt16. You must
specify the type of the value that you’ll save in the raw pointer.
Note: If you print the value of int16bytesPointer itself, it will give you the
memory address where 4386 is stored. Each time you run your playground, it
will give a different address.
raywenderlich.com 261
Expert Swift Chapter 9: Unsafe
Notice that the first byte is the least significant. This is because your values are
stored in little-endian format, as you learned in Chapter 5: Numerics & Ranges.
The first line creates a new pointer that points to an address one value above what’s
in int16bytesPointer, thus referring to the next byte. The second line is just as you
did before, you’re loading the contents of this address in a UInt8 variable. Its value is
0x11, as expected.
Just as before, you’re reading the third byte of the stored value, although the stored
value itself is only two bytes in size. But because you’re manually specifying the
address, Swift assumes you know what you’re doing and won’t stop you.
This will work, but there’s no way to guarantee what its result will be. In your
playground, this might give you a value of zero, but in a real app, you can never
know.
To make it worse, you can store a value in an address that isn’t yours:
This is a more dangerous operation. You’re changing a value you don’t own, and
Swift won’t stop you from doing that.
raywenderlich.com 262
Expert Swift Chapter 9: Unsafe
In this example, the line will always crash and there’s no way the execution will pass
it. But if you use different types with different alignment values, there’s a chance the
alignments will coincidentally match, and times it won’t. For example, if you create
the pointer with a type of alignment of four and later try to read it with a type that
has an alignment of eight, sometimes it will work and sometimes it won’t. This won’t
give your users a good experience at all.
As in the previous example, you reserve eight bytes of memory and store a UInt with
the value 0x0102030405060708.
This defines a raw buffer pointer starting from the raw pointer you previously
defined. It also sets the length of the buffer to eight, because that’s how many bytes
you allocated, which equals the size of UInt.
raywenderlich.com 263
Expert Swift Chapter 9: Unsafe
The buffer provides an enumeration that you can loop on to go through all the bytes.
After you run the playground, the log will show the following:
byte 0: 8
byte 1: 7
byte 2: 6
byte 3: 5
byte 4: 4
byte 5: 3
byte 6: 2
byte 7: 1
Buffers provide a way to go through multiple bytes with clear and specific
boundaries. UnsafeRawBufferPointer accesses the values as UInt8. To access them
as a different type, you’ll need to use typed pointers.
Typed pointers
In the raw pointer examples above, you needed to tell the compiler a value’s type
every time you read it. This can be very tedious if you’re using the same type over
and over.
A typed pointer means that the compiler tracks the data type for this pointer and
reads the number of bytes matching the type’s size to give you a proper value. This
means you don’t need to specify the type anymore.
let count = 4
raywenderlich.com 264
Expert Swift Chapter 9: Unsafe
pointer.pointee // 10001
pointer.advanced(by: 1).pointee // 10002
(pointer+1).pointee // 10002
pointer.advanced(by: 2).pointee // 10003
(pointer+3).pointee // 10004
// 4
let bufferPointer = UnsafeBufferPointer(
start: pointer,
count: count)
for (offset, value) in bufferPointer.enumerated() {
print("value \(offset): \(value)")
}
1. First, you allocate the number of items you want in memory. Notice that you
didn’t specify the number of bytes or alignments — you just specified the count.
But you also specified the type Int to UnsafeMutablePointer. That was more
than enough for allocate(:) to know exactly how to do it.
2. Then, you initialize the buffer with an initial value. This is important to do in
general when you work with buffers so you clean up the memory from whatever
was previously stored there. And, of course, you want to de-initialize and de-
allocate the memory after you finish using it.
6. Looping over the buffer to print all the values that you saved, the log will show:
value 0: 10001
value 1: 10002
value 2: 10003
value 3: 10004
Once you create a raw pointer, you can bind it to a type by creating a typed pointer
and use it normally. Also, you can rebind a typed pointer to a different type. But this
might cause you some serious problems if you aren’t careful. First, before you learn
how to do that, you should understand some rules and concepts that affect
rebinding.
Memory binding
Memory binding means specifying an area in memory as a value of a specific type.
For example, if you specify the four bytes between 0x0010 and 0x0013 as an Int32,
this means you bound them to that type. If you just read or write on them once as
Int32, that doesn’t count as binding.
raywenderlich.com 265
Expert Swift Chapter 9: Unsafe
You should understand a few concepts before diving deeply into memory binding:
• Type punning
• Related types
• Strict aliasing
• Layout compatibility
Punning
Type punning is when a part of memory is bound to a type, then you bind it to a
different and unrelated type.
You created a raw pointer of two bytes and then bound it twice with two different
typed pointers: a Float16 pointer and another for two UInt8.
uint8Pointer.pointee // 0x5E = 94
uint8Pointer.advanced(by: 1).pointee // 0x79 = 121
You save the hex value of 0xABC0, which is equivalent to 43968 in the float pointer.
When you read the two other values from the UInt8 pointers, you get the hex values
of 0x5E and 0x79. Two hex values don’t resemble anything from the float value at
all.
uint8Pointer.pointee -= 1
float16Pointer.pointee // 43936
raywenderlich.com 266
Expert Swift Chapter 9: Unsafe
Note: You must never use two differently typed pointers to the same memory
locations. Doing so will cause unknown and unexpected behavior. This
example uses primitive data types to show you a way that minor changes can
cause larger, unexpected changes.
When you alter the value of one lower byte of the float, the value of the float gets
reduced by a value much more than one. Remember that values are saved in small-
endian. That’s why the lower byte comes first.
The binary representation of floats differs from integers, as you learned in Chapter
5: Numerics & Ranges. Thus, any small changes in the binary representation will
cause a more significant change in the value itself.
Although this example may seem harmless, the effects can be much greater with
different types. And if you’re punning between a value type and a reference type, the
consequences will be severe, to say the least.
Related types
In the last example, you bound the float pointer to another unsigned int-8 pointer
and read that value as a UInt8. That value was completely unrelated to what was
stored for the Float16. Thus, the rebinding here was wrong. So when is the
rebinding right?
To rebind from one type to another type safely, both types should be related and
layout compatible, and should respect strict aliasing rules. To begin, have a look at
the first of those requirements.
To say two types are related, they must match one of the following rules:
• Both types are identical or one is a typealias of the other. Somewhat logical, isn’t
it?
• One type may be a tuple, a struct or an enum that contains the other type.
• One type may be an existential (a protocol) that conforming types will contain the
other type.
raywenderlich.com 267
Expert Swift Chapter 9: Unsafe
Layout compatibility
Remember the memory layout explanation at the beginning of this chapter? To say
two types are mutually layout compatible means they have the same size and
alignment or contain the same number of layout compatible types.
Further, types can be layout compatible but not mutually. They’re compatible if one
aggregate type is layout compatible with a larger type containing the same common
types. For example, the tuple (Int,Int) is memory compatible with
(Int,Int,Float) because they both have the same common types, but they aren’t
mutually compatible.
Strict aliasing
If you have two pointers of value types or class types, they both must be related. This
means that changing the value of one pointer changes the other pointer in the same
way. In such cases, both pointers are aliases to each other.
One reason it’s important to ensure the two types are related and not only layout
compatible is to make sure that compiler optimizations don’t break your code in
future versions.
Safe rebinding
Swift provides three different APIs to bind/rebind pointers:
• bindMemory(to:capacity:)
• withMemoryRebound(to:capacity:)
• assumingMemoryBound(to:)
You have already used the first to bind a raw pointer to a typed pointer. What are the
other two for?
If you have a typed pointer and you want to temporarily bind it to a different type,
withMemoryRebound(to:capacity:) is what you need. Add the following in a
playground:
let count = 3
let size = MemoryLayout<Int16>.size
let stride = MemoryLayout<Int16>.stride
let alignment = MemoryLayout<Int16>.alignment
let byteCount = count * stride
raywenderlich.com 268
Expert Swift Chapter 9: Unsafe
Here, you allocate memory for three UInt16 objects in a raw pointer. Then, you bind
it to a UInt16 typed pointer.
typedPointer1.withMemoryRebound(
to: Bool.self,
capacity: count * size) {
(boolPointer: UnsafeMutablePointer<Bool>) in
print(boolPointer.pointee)
}
Next, you temporarily rebind that typed pointer to another one. The other typed
pointer’s life is only within the closure of withMemoryRebound(to:capacity:).
It’s extremely important that you don’t access self within this closure or return the
newly bound pointer outside this closure. Doing either will cause undefined
behaviors.
For the next example, consider having a C++ API that initializes memory with two
different objects and returns them as a raw pointer. A Swift imitation for this API
would look like this:
return rawPtr
}
raywenderlich.com 269
Expert Swift Chapter 9: Unsafe
This function creates a UInt16 and a Float16 and returns both together through a
raw pointer. The return type completely erases the types. Add this code to use it:
This did not rebind the memory to those types. It relied on the precondition that the
memory is already bound to this type. And, of course, if the memory wasn’t already
bound to that type, an undefined behavior will occur.
Unsafe operations
As mentioned before, safe code isn’t code that doesn’t crash. It’s code that behaves
consistently.
You’re used to seeing crashes when you force unwrap a variable with a null value or
perform an arithmetic operation with a value that exceeds the boundaries of the data
type you’re using. Those operations will consistently signal an error.
You can bypass those errors, but that doesn’t mean your code will be more stable.
Crashes aren’t the worst thing you can encounter, as mentioned before. This is a
different area for undefined behaviors. They can have their benefits, but you need to
be careful when using them.
Unsafe unwrap
Consider the following code:
The print statement obviously will cause a crash. But if you’re performing a complex
operation and are sure an optional property can’t be nil, you can use
unsafelyUnwrapped.
raywenderlich.com 270
Expert Swift Chapter 9: Unsafe
In debug mode, the print statement will also crash. But if optimization is enabled,
like in a release build, this line won’t crash. Instead, it will proceed normally with
whatever data was already stored in the memory of unsafeString.
This is good because while you’re developing and working on your code, the compiler
will show you mistakes and trap any nils for you. That will prevent you from
struggling with undefined behaviors while your project remains a work in progress.
This MUST NOT make you overconfident with trivial nil checks and guards, saying:
“Hey, I could just use this and skip the whole extra code I write to check for nils”. If
you do that, I can promise you things will not be better. You won’t gain any
noticeable performance improvements or even safer code in 99% of your day-to-day
situations. Just like pointers, this can over-complicate your life if not used properly.
Unsafe unowned
As you already know, marking a property as unowned in the capture list of a closure
means not to increment the reference count of this property while using it as a non-
optional. In a way, saying it’s unowned somewhat guarantees that this will never be
nil when the closure executes. If for any reason it is a nil, your code will crash.
This is a dangerous operation that you shouldn’t use lightly. Here, you tell the
compiler not to track the lifetime of the object at all. If the object is deallocated, the
property will be a pointer to an area in memory that was deallocated. This is called a
dangling pointer.
Dangling pointers are dangerous because you have no idea what they point to.
Attempting to read or write from them can produce unexpected results that can
include an immediate crash or data loss.
Overflow operations
The last point relates to arithmetic operations. When you do any operation on a
number, the compiler makes sure that the data-type you’re using can store the value.
UInt8 has a possible range from 0 to 255. Representing the number 256 requires 9
bits.
raywenderlich.com 271
Expert Swift Chapter 9: Unsafe
UInt8.max + 1
There’s an alternative operand that won’t crash. Update the last line to this:
UInt8.max &+ 1 // 0
The result of this add operation is zero. You must be asking yourself how?
The new value from this addition should be 256, but this requires 9 bits, where the
most significant bit (the 9th bit) is 1 and all the others are 0.
The overflow operations disregard bits beyond their limits, just like a rollover
counter works (aka mechanical counter). If the counter contains only two digits, then
the maximum number it can represent is 99. The next number will be zero.
And negative numbers will suddenly jump to large numbers as they keep decreasing:
raywenderlich.com 272
Expert Swift Chapter 9: Unsafe
Key points
• Safe code means the behavior is always expected even if the input is unexpected.
Crashing is considered a safe behavior if the input is not allowed as long as this
crash is consistent.
• References are pointers in origin. But the standard library handles their allocation,
initialization and entire cycle.
• Each type has size, alignment and stride values that control how its memory is
allocated. Also, the order of the properties in each type affects those numbers.
• The standard library has different types of unsafe pointers. Each gives a certain
level of control, from pointers that access memory as raw bytes to those that know
exactly the type of the bytes accessed.
• There are several rules you must follow before you bind or rebind memory to a
type.
• Unsafe operations and overflow arithmetic can skip the safety validations on the
standard library.
• And you can learn more details and examples about related types and layout
compatible examples here https://github.com/atrick/swift/blob/type-safe-mem-
docs/docs/TypeSafeMemory.rst.
raywenderlich.com 273
Section III: Techniques
The final section of this book covers advanced techniques to super-charge your Swift
powers, and use all of what Swift has to offer.
We’ll cover topics like Higher order functions, Functional reactive programming,
Objective-C interoperability, using Instrumentation, and API design.
raywenderlich.com 274
10 Chapter 10: Higher-Order
Functions
By Ehab Amer
As you’ll soon learn, higher-order functions can simplify your code significantly by
making it more readable, a lot shorter and easier to reuse.
raywenderlich.com 275
Expert Swift Chapter 10: Higher-Order Functions
class TextPrinter {
var formatter: ParagraphFormatterProtocol
init(formatter: ParagraphFormatterProtocol) {
self.formatter = formatter
}
protocol ParagraphFormatterProtocol {
func formatParagraph(_ text: String) -> String
}
The code above is for a text printer. All it does is receive an array of strings, run each
through a formatter and then print the formatted value. TextPrinter doesn’t
specify how the formatting operation is done. But it does specify that it needs an
object that conforms to ParagraphFormatterProtocol, which will implement this
functionality.
raywenderlich.com 276
Expert Swift Chapter 10: Higher-Order Functions
This simple formatter conforms to the required protocol, and its format function
does the following:
1. Makes sure the provided string is not empty. If it is, then there’s nothing to
format and the formatter returns the string as-is.
3. Checks whether the last character is punctuation. If it isn’t, then it adds a full-
stop “.”.
let exampleParagraphs = [
"basic text example",
"Another text example!!",
"one more text example"
]
textPrinter.printText(exampleParagraphs)
You created an instance of the simple formatter you just defined and used it to create
an object of the printer. The output in the console from the code above should be:
First-order functions
The two main methods here are first-order functions. Both
ParagraphFormatterProtocol.formatParagraph(_:) and
TextPrinter.printText(_:) take normal values. You didn’t go into the higher-
order ones yet.
raywenderlich.com 277
Expert Swift Chapter 10: Higher-Order Functions
}
}
This extension adds a method to Array only if the type of the stored elements is
String. So this method won’t be available to an array of Int or an array of Any but
only to String.
Use it by calling:
exampleParagraphs.printFormatted(formatter: simpleFormatter)
There’s nothing wrong at all with the implementations above. They deliver the
needed operations and offer a clear integration method to provide different
formatting operations. But creating a formatter every time or using the same one
over and over is not particularly Swifty. What if there were a way to send the
formatting operation itself as a parameter instead of packaging it in an object?
raywenderlich.com 278
Expert Swift Chapter 10: Higher-Order Functions
This extension has the same idea only for [String]. But in its method, notice the
different type of the parameter. ((String) -> String) means that this is not a
property. Instead, it’s a method that takes a String as a parameter and returns a
String.
In the parentheses, anything that precedes the arrow describes parameters, and what
follows the arrow defines a return type.
That method you added in the extension is a higher-order function. For it to work,
it takes another function as a parameter and not a normal property. You can use it
like this:
let exampleParagraphs = [
"basic text example",
"Another text example!!",
"one more text example"
]
exampleParagraphs.printFormatted(formatter: formatParagraph(_:))
You sent the actual function itself as a parameter. Another way to pass the function
as a parameter is like this:
The variable theFunction is actually a function and not a normal property. You can
see its type as ((String) -> String) and can describe it as a reference to a
function.
Closures
There’s also another form of passing over the function as a parameter that you’re
familiar with:
exampleParagraphs.printFormatted { text in
guard !text.isEmpty else { return text }
var formattedText = text.prefix(1).uppercased() +
text.dropFirst()
if let lastCharacter = formattedText.last,
!lastCharacter.isPunctuation {
formattedText += "."
}
return formattedText
}
raywenderlich.com 279
Expert Swift Chapter 10: Higher-Order Functions
In this call, you sent a closure, not a function. Practically, both are the same. This is
identical to sending the function reference directly. Another way is to call the
function in the closure.
exampleParagraphs.printFormatted { formatParagraph($0) }
This form is perfect when the function you want to pass in as a parameter to the
higher order function doesn’t match the parameter signature. An example of this is if
the function you’re going to call or use has two parameters, but you’ll send one of
them as a constant, and the other will be from the main operation. Then you can
have a format as follows:
All those examples will work the same, but they each have a slight difference. Think
of a closure as if it’s a function that’s created only for the current scope. The last two
examples used a closure, but the first gave a ready-made function directly to the
printFormatted.
You can still use functions directly as a parameter, but the signature of
someOperation(::) will need to change to match what aHigherOrderFunction()
expects. This change is called Currying and is covered in the second half of the
chapter.
The Swift standard library has many of those operations that use this concept. Each
of them does some operation, but there’s a small detail inside it that’s left out. Take
the sort function as an example. There are many sorting algorithms. Some perform
better in a small set but not as well in a large one. However, in the end, they all have
a comparison between two items that define if the final ordering will be ascending or
descending. Which algorithm is used and how it’s used aren’t relevant when calling
the function, except for how the final result is ordered.
raywenderlich.com 280
Expert Swift Chapter 10: Higher-Order Functions
• map
• compactMap
• flatMap
• filter
• reduce
• sorted
• split
You’ll go through them in more detail now to see an example of their usage.
map
Array.map(_:) applies an operation on all the elements of the array, and the result
is a new array of the same size. This is a much shorter version of iterating over the
elements and adding the new items of an operation to a new array. Try this example
in a playground:
print(newNumbers)
This creates a new array that contains the square of each number. An alternative way
to do this using map(_:) is this:
raywenderlich.com 281
Expert Swift Chapter 10: Higher-Order Functions
This code is much simpler and shorter than the previous one. This is because it uses
the shorthand provided by Swift and all the code reductions possible for a Swift
closure. But as you learned above, higher-order functions can receive a function
instead of a closure. Try this example:
This function squares an Int value and prints its original value and its square value.
You can use map(_:) by sending the new function to it like this:
compactMap
Array.compactMap(_:) is like the previous one, but the result array doesn’t need to
be the same size as the original array. This one filters out nil values from the
resulting operation. The operation in the parameter has an optional result, but
map(_:) didn’t allow for that.
A common usage for this method is to transform the items in the array to a different
type, knowing that not all the items can be transformed and that you’ll get only the
elements that were successfully transformed. Consider the following example:
The function receives a string and finds out if this string is a number represented in
letters. For example, the word “one” is the number “1”. If it is, then the function
returns that number. Otherwise, it returns nil.
wordsToInt("Three") // 3
wordsToInt("Four") // 4
wordsToInt("Five") // 5
wordsToInt("Hello") // nil
raywenderlich.com 282
Expert Swift Chapter 10: Higher-Order Functions
This function receives any value and attempts to convert it to an integer. If it’s a
string, it passes it over to the previous function that you added. Otherwise, it
attempts to cast it to an integer.
convertToInt("one") // 1
convertToInt(1.1) // nil
convertToInt(1) // 1
The value “1.1” failed to convert because it’s a Double. Now, try it with this sample
array:
So far, it looks good. But while working on the project, you learned the values “3.0”
and “7.1” should be converted to integers. The problem was not related at all to
compactMap(_:), but it’s in the method that you’re using for conversion all over your
project. Update it to the following:
This gave your function the ability to convert doubles to integers. The result changed
to “[1, 2, 3, 4, 5, 7]”.
Later, you can refactor and improve convertToInt(_:) to convert more values
without going through many places in your project. This specific example shows a
better usage for a function instead of a code block or a closure as a parameter to a
higher-order function.
raywenderlich.com 283
Expert Swift Chapter 10: Higher-Order Functions
flatMap
As you saw in compactMap(_:), you can have fewer results than in your original
array. But what if the operation you want will provide an array for each item?
Consider that you want to calculate the square, the cube and the fourth power of
each number in an array. Consider the following function:
The result contains all the values you want, but it still has the same size as the
original array. The only difference is that the array is now an array of arrays [[Int]].
For each element in the original collection, you created another collection for it.
If you want to have this in a single, flat array, you can do the additional step of
joining them together:
But the Swift standard library provides a more convenient way to do that using fewer
steps. Instead of using map(_:) use flatMap(_:):
The two ways are equivalent and provide the same result. The latter is just simpler to
write.
raywenderlich.com 284
Expert Swift Chapter 10: Higher-Order Functions
filter
filter(_:) is one of the simplest higher-order functions. As its name suggests, you
want to filter a collection of many items based on criteria. If the element meets the
criteria, keep it. But if it doesn’t, remove it from the list.
This function expects criteria in the form of a function that returns true or false.
To be in the list or not to be in the list.
Say you want to get the list of number words (numbers in English words, not in
digits) that contain only four letters for the numbers between zero and one hundred.
First, build the whole list of words:
Next, create the function that will decide if this string should stay or not:
It’s correct. But if your actual check is complex and used all over your application,
then it would be better to define it as a function rather than as a closure. Your code
will look better and have fewer places to maintain if any changes are needed.
raywenderlich.com 285
Expert Swift Chapter 10: Higher-Order Functions
reduce
reduce(_:_:) is a handy method when you want to merge a group of elements.
Common examples include adding or multiplying a group of numbers. But this
doesn’t mean it works only on numbers. You also can use it on your own custom
types. Say you have scores for a soccer team and you want to merge them all to see
the team’s totals. Consider this Score type:
struct Score {
var wins = 0, draws = 0, losses = 0
var goalsScored = 0, goalsReceived = 0
init() {}
if goalsScored == goalsReceived {
draws = 1
} else if goalsScored > goalsReceived {
wins = 1
} else {
losses = 1
}
}
}
Score can represent a single match or a group of matches. The initializer receives
the goals scored and received by the team in one match and sets the wins, draws and
losses values according to this score.
var teamScores = [
Score(goalsScored: 1, goalsReceived: 0),
Score(goalsScored: 2, goalsReceived: 1),
Score(goalsScored: 0, goalsReceived: 0),
Score(goalsScored: 1, goalsReceived: 3),
Score(goalsScored: 2, goalsReceived: 2),
Score(goalsScored: 3, goalsReceived: 0),
Score(goalsScored: 4, goalsReceived: 3)
]
Next, create a function to easily add the scores of two matches. Using operator
overloading is ideal for this:
extension Score {
raywenderlich.com 286
Expert Swift Chapter 10: Higher-Order Functions
return newScore
}
}
Finally, reduce this team’s scores into a single Score object using reduce(_:_:) and
the new operator method you created:
A nice thing about operators is that you don’t need to write the whole signature. The
operator symbol by itself is enough, and there’s no need to write it like this: +
(left:right:).
The first parameter in reduce(_:_:) is the initial value you want to add on top of. In
this case, you don’t have anything other than the existing matches, so an empty
Score object is enough. But if you wanted to add the second season’s scores to this
season’s scores, then this parameter would be quite handy:
var secondSeasonMatches = [
Score(goalsScored: 5, goalsReceived: 3),
Score(goalsScored: 1, goalsReceived: 1),
Score(goalsScored: 0, goalsReceived: 2),
Score(goalsScored: 2, goalsReceived: 0),
Score(goalsScored: 2, goalsReceived: 2),
Score(goalsScored: 3, goalsReceived: 2),
Score(goalsScored: 2, goalsReceived: 3)
]
Instead of supplying an empty score object, you provided the ones from the first
season as an initial value, and all the matches from the second season are added to
it.
raywenderlich.com 287
Expert Swift Chapter 10: Higher-Order Functions
In this example, you couldn’t have created separate types for match scores and
season scores. The type returned by reduce(_:_:) matches the type of the elements
in the array. This is why a single type was used to represent both.
sorted
This might be one of the most frequently used functions in day-to-day work. It sorts
an array of elements for you.
Multiple sorting algorithms exist, and each has different complexities and usages.
Some are better than others in different situations. For example, the insertion-sort is
better for small sets. However, for a larger set, it’s not as efficient as others. You can
learn about the different sorting algorithms from our book “Data Structures &
Algorithms in Swift” — it explains the differences between algorithms.
In the Swift standard library, the sort implementation has changed over the years.
With Swift 5.0, the implementation for it uses “Timsort”, which is a stable sorting
algorithm. Here, stable means that if two elements have the same ordering score,
then the order in which they appeared in the original set is maintained in the final
ordered result.
To see how sorting works in action, try it on an array of the English words for
numbers from zero to ten.
The sort worked in ascending alphabetical order, starting with “E” and ending with
“Z”. When the items in the array are comparable, the default ordering is ascending,
which is equivalent to words.sorted(<).
sorted(_:) expects an expression that takes two parameters and defines if the first
should appear before the second.
Using the same Score as previously defined, create a new method to check if the two
provided match scores are ordered:
raywenderlich.com 288
Expert Swift Chapter 10: Higher-Order Functions
if firstDifference == secondDifference {
return first.goalsScored > second.goalsScored
} else {
return firstDifference > secondDifference
}
}
}
1. This method gives priority to the winning match if only one of them was a win.
3. If both matches were wins, draws, or losses, get the difference between the goals
scored and goals received. If both differences were the same, then return the
match that had more goals or the match that had a higher difference. Loss
matches with a higher difference give a -ve value so they stay at the end of the
ordering.
Sorting a set of matches with this function gives the following result:
var teamScores = [
Score(goalsScored: 1, goalsReceived: 0),
Score(goalsScored: 2, goalsReceived: 1),
Score(goalsScored: 0, goalsReceived: 0),
Score(goalsScored: 1, goalsReceived: 3),
Score(goalsScored: 2, goalsReceived: 2),
Score(goalsScored: 3, goalsReceived: 0),
Score(goalsScored: 4, goalsReceived: 3)
]
raywenderlich.com 289
Expert Swift Chapter 10: Higher-Order Functions
goalsReceived: 0),
// Score(wins: 0, draws: 0, losses: 1, goalsScored: 1,
goalsReceived: 3)]
The final result has matches with the largest scoring difference, followed by draws
with the highest scoring and finally losses with the smallest difference and then
larger difference.
As explained earlier, higher-order functions can do a lot for you. They can simplify a
complex functionality for you while keeping a tiny, simple part of its overall
operation to be provided at the time of calling.
So far, you went deeply through one of the forms of higher-order functions:
functions that expect part of their functionality as a parameter. This is the easy part.
Before starting the next section, take a break, grab a cup of coffee (strong coffee is
advisable) and do whatever you need to give your mind a break. It will help.
Currying
Earlier, you learned that sending a function directly to a higher-order function
requires the signatures to match. Thus, the example below is best done through a
closure because the signatures don’t match.
Usually, this is fine. But it’s not impossible to remove the usage of the closure and
send a function directly.
raywenderlich.com 290
Expert Swift Chapter 10: Higher-Order Functions
aHigherOrderFunction(curried_SomeOperation("a constant"))
Now, take a step back and observe curried_SomeOperation. There are two major
changes:
1. The parameters are now sent separately, each in its own brackets.
Currying is breaking down a function that takes multiple parameters into a chain of
functions in which each takes a single parameter. To see this in action, you’ll practice
by implementing the above example, going from someOperation(_:_:) to
curried_SomeOperation(_:).
Now that the abstract has become more concrete, start with the signature of
curried_SomeOperation. First, break it down: Instead of taking two parameters,
create a chain that takes one parameter at a time. Worry about the re-ordering of the
two parameters later.
The first step is done. Well, it’s done, but it could still use a cleanup. The signature
looks straightforward. The function takes a single parameter and returns another
function with the signature (String) -> ().
raywenderlich.com 291
Expert Swift Chapter 10: Higher-Order Functions
}
}
The body returns a closure that takes one parameter of type String. This closure is
(String) -> (), and inside it, the p1 that was sent to the original function is
captured and used.
Note: To avoid retain cycles, you’ll need to pay attention to the capture list if
the properties are of reference types and not value types.
Like before, you’re using a closure but it’s still needed. To eliminate the usage of the
closure, you’ll need to re-order the parameters. But before doing that, you should
know why.
aHigherOrderFunction(_:) expects the type (Int) -> (). Your function after the
Int parameter is provided doesn’t return (). Instead, it returns (String) -> ().
If you change the order of the parameters, you’ll have the (Int) -> () signature
that you need to avoid using the closure. Change curried_SomeOperation to this:
Now that the string is expected first, the closure takes the Int as a parameter. This
closure now matches the signature expectations of aHigherOrderFunction(:). The
usage now is:
raywenderlich.com 292
Expert Swift Chapter 10: Higher-Order Functions
aHigherOrderFunction(curried_SomeOperation("a constant"))
You now understand what currying is and why flipping parameters is useful. But
what if there were a way to make a generic curry and flip so you didn’t need to
create a curried/flipped version for each method you encounter?
Notice that you mention a return type (C), but in someOperation, there was no
return type. Technically, the return type is Void.
Before going through the implementation, there’s one important difference between
what this generic method is doing and what you did in the previous example. Here,
you’re generating what you did yourself in the example, meaning that this will
transform the signature of the original function. Although this might be obvious, it’s
worth pointing out because it introduces two key differences from the previous
currying example.
The first difference is that there’s an additional closure level. Previously, you
returned the closure (Int) -> (). Here, you return a longer chain because the first
set of arguments is the actual function that you’ll transform.
raywenderlich.com 293
Expert Swift Chapter 10: Higher-Order Functions
The second difference is the explicit keyword @escaping. This is needed because the
function you pass to it will execute after the curry function itself finishes.
curry is a higher-order function for both of these reasons: It’s taking a function as a
parameter and it returns a function, too.
Try using this function and compare it with the original someOperation:
Using the new method in the playground will give you this expected result, proving
that the original function and the curried one are working as expected:
The return type from curry(someOperation) is (Int) -> (String) -> (). This is
identical to the first step you did before. Next, you need to flip the arguments to be
(String) -> (Int) -> ().
aHigherOrderFunction(flip(curry(someOperation))("a constant"))
You don’t need to write any more code to adapt your existing functions to pass them
directly to higher-order functions.
For the specific case of flip, you might feel you can design your APIs to completely
avoid needing it. But Swift contains some higher-order functions that make flipping
necessary.
raywenderlich.com 294
Expert Swift Chapter 10: Higher-Order Functions
extension Int {
func word() -> String? {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
return formatter.string(from: self as NSNumber)
}
}
1.word() // one
10.word() // ten
36.word() // thirty-six
By default, Swift will generate a higher-order function for this method for you:
To use it with the same three numbers used above, your code will look like this:
Int.word(1)() // one
Int.word(10)() // ten
Int.word(36)() // thirty-six
If you have an array of integers and want to map them to their word equivalents, you
can either create a closure and call word() on each object or restructure Int.word to
match the signature requirements for map(_:).
raywenderlich.com 295
Expert Swift Chapter 10: Higher-Order Functions
This overload moves the part that doesn’t take any parameters to the beginning,
allowing the parameter to be sent at the end.
flip(Int.word)()(1) // one
You can avoid repeating flip by creating a new variable that carries the function
flip(Int.word)():
[1, 2, 3, 4, 5].map(flippedWord)
// ["one", "two", "three", "four", "five"]
On some occasions, the extra brackets () can be inconvenient. You might want to
convert any generated higher-order function for a method/instance-function that
doesn’t take any parameters and use it if it was a standard class function. Create this
new function:
reducedWord and flippedWord are identical and are both (Int) -> String?. But if
you pay attention to the declaration of the latter, you’ll find that the brackets were
already added as if it contains the result of the outer closure of flip. But reduce
doesn’t have an outer closure in the first place.
extension Int {
func word() -> String? {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
raywenderlich.com 296
Expert Swift Chapter 10: Higher-Order Functions
If you wanted to have one function that would do both, it would look like this:
It’s definitely not wrong. But you might have many more than just two functions that
you want to bundle. There’s a nice way to do that. Add this generic function merger:
You might be wondering if there isn’t a nicer way to use it. This almost looks like you
added the two functions. How about doing the same but with operator overloading?
raywenderlich.com 297
Expert Swift Chapter 10: Higher-Order Functions
}
}
This is called function composition. You composed a function using two smaller
functions. This is a bigger topic, but once you understand how you can play with
higher-order functions, function composition becomes a lot clearer.
You’re free to define the operator you want. But know that once you start having too
many operators in your code, it will look unfamiliar to others. Your proprietary
conventions will make it complicated for other eyes.
raywenderlich.com 298
Expert Swift Chapter 10: Higher-Order Functions
Key points
• A higher-order function is a function that deals with other functions, either as a
parameter or as a return type.
• Using a function signature instead of a closure can simplify your code if the
operation is complex or gets repeated across your code.
• map, compactMap, flatMap, filter, reduce, sorted and split all are examples of
higher-order functions in the standard library.
• Function currying means breaking down a function that takes multiple parameters
into a chain of functions that each takes one parameter.
• Currying and argument flipping are ways to alter a function’s signature to fit a
higher-order function.
• You can use operator overloading to create an adding function for higher-order
functions, making function composition easier.
You can read about different algorithms in the book “Data Structures &
Algorithms in Swift” or from the swift-algorithms-club on https://github.com/
raywenderlich/swift-algorithm-club, which the book is based on.
raywenderlich.com 299
11 Chapter 11: Functional
Reactive Programming
By Shai Mishali
As a developer, you probably bump into buzzwords daily. Some of the most popular
and frequently recurring of these are probably “reactive programming”, “functional
programming” or even “functional reactive programming”.
Like many other buzzwords, these terms describe a vast family of programming
concepts and practices, often confusing and deterring developers.
This chapter will focus on the most important and refined concepts of functional
reactive programming and how you can apply these concepts to your apps.
raywenderlich.com 300
Expert Swift Chapter 11: Functional Reactive Programming
Functional? Reactive?
Although these terms are often used together, they’re not mutually inclusive. This
means that each term stands by itself.
Reactive programming
The idea of reactive programming is that instead of manually and imperatively
reading the state or value of some entity, you listen, or subscribe, to changes of that
entity and get notified whenever your state changes — in which case, you can react to
the change and update your app accordingly. These changes are emitted over time:
In the example above, there is a stream of price values called totalPrice. Whenever
it emits a new total price, you update your app’s UI accordingly.
This minimal concept is powerful in ensuring data integrity, but it also has many
other benefits such as composition and easy transformations, which you’ll learn
about in this chapter.
// 1
var userBalance = 5
let productPrice = 10
// 2
let canMakePurchase = userBalance >= productPrice
print(canMakePurchase)
// 3
userBalance += 20
print(canMakePurchase) // 3
raywenderlich.com 301
Expert Swift Chapter 11: Functional Reactive Programming
2. Define a canMakePurchase boolean to make sure the user has a sufficient balance
to purchase the product. Its value is false because the user has only $5.
3. Add $20 to the user’s balance, which means they should be able to buy the $10
product. But printing canMakePurchase still prints out false. Also, any other
views in your app that depend on canMakePurchase still use a wrong false
value.
In this pseudo-code example, canMakePurchase will always have the correct boolean
value whenever either userBalance or productPrice changes. Also, any views
depending on canMakePurchase are automatically updated based on that new value:
raywenderlich.com 302
Expert Swift Chapter 11: Functional Reactive Programming
This is how basic composition looks in reactive programming, and it’s the key to
developing your reactive thinking: “Everything is a stream of values.”
Because basically anything that occurs in your app occurs over time, it can easily be
represented as a stream of values. When you shift your mind to think of all pieces of
data as streams of data, the compositional options become endless.
iOS developers have used these concepts with third-party frameworks such as
RxSwift and ReactiveSwift. But became even more widespread with the
introduction of Combine — Apple’s reactive programming framework, built into the
SDK.
Functional programming
Functional programming, unsurprisingly, revolves around functions, but more
specifically pure functions.
1. Always produce the same output for the same input: Consider min(3, 7) —
no matter how many times you run it, you’ll always get back 3.
raywenderlich.com 303
Expert Swift Chapter 11: Functional Reactive Programming
The most frequently occurring example of this is operators — simple functions that
transform these reactive pieces you can subscribe to. Most of these operators are
pure functions, and many of them are higher-order functions (functions that take
other functions, like map, filter and reduce), which you learned about in the
previous chapter.
Many of these operators have parallels with the same name in the Swift standard
library and also perform the same sort of work. An example of this is map, which
transforms each element in a stream:
Don’t worry if this feels a bit confusing at the moment. You’ll work with operators a
lot throughout this chapter.
Reactive basics
There are many attempts at defining a unified standard for how streams behave. The
most common ones are Reactive Streams (https://www.reactive-streams.org/) and
Reactive Extensions (Rx) (http://reactivex.io/). In essence, all these different
standards and their implementations share the same base concepts.
Naming
The basic streams, or producers, that emit updates to subscribers have different
naming across implementations. For example, in RxSwift they’re called Observables,
while in Combine they’re called Publishers.
raywenderlich.com 304
Expert Swift Chapter 11: Functional Reactive Programming
Events
These producers emit not only values, but something called an event.
There are three kinds of events, which might be named a bit differently in each
implementation:
• A value event, which carries a value of a specific type. You might get an endless
stream of these values until completion of the stream occurs.
Good examples of this would be mouse movements or keystrokes. These are events
that occur endlessly and never complete, unlike a network request, which would
emit a single value and complete.
Thinking of water
Streams of data are analogous to streams of water. Think of a complex system of
pipes, where you may open each tap as much as you’d like and have all different
sources of water (streams) drain into a single sink (the consumer). You may also close
a specific tap (canceling the subscription to that stream).
raywenderlich.com 305
Expert Swift Chapter 11: Functional Reactive Programming
These different water sources are the streams, while the multiple points where these
water sources connect in the pipes represent the various composition options
streams allow.
An iterator lets you iterate over a list of values, which could theoretically be either
infinite or finite:
The code above creates an iterator from an array of Events and calls next() to
retrieve the next event as long as one is available. This is the same as a reactive
stream.
raywenderlich.com 306
Expert Swift Chapter 11: Functional Reactive Programming
The main difference between the two is that streams push data to consumers, while
an iterator pulls data when asked by a consumer. It’s still an easy and useful way to
understand streams and how simple they are in their most basic form.
You’ll build a SwiftUI and Combine-based app that lets you choose guitar parts for a
custom guitar and then order it.
raywenderlich.com 307
Expert Swift Chapter 11: Functional Reactive Programming
• Build: In this view, you’ll select the various pieces of the guitar and see a preview
of the guitar, a price and a checkout button.
• Checkout: Here, you’ll see an overview of all the parts you’ve ordered, select a
shipping option and finalize your purchase.
• Services: This folder includes various API services to fetch guitar information, as
well as currency conversions that you’ll use in the checkout view.
• Models: These are the different models that drive the app. A Guitar may have a
specific shape, body wood, fretboard and color attached to it. Each of these
additions is represented as an enum with various selections.
• Helpers: As the name suggests, this folder includes various helpers you’ll use
throughout this chapter.
Don’t worry about all these bits and pieces right now because you’ll use all of them
sooner or later in this chapter.
Building a guitar
Build and run the starter project, and you’ll notice a preview of the base model guitar
along with a dummy “Checkout” button. Nothing too fancy:
raywenderlich.com 308
Expert Swift Chapter 11: Functional Reactive Programming
In this section, you’ll add various SwiftUI Pickers and handle the user’s selection
through the various powers of reactive streams: composition, reactivity, bindings,
etc.
For this app, you’ll use an MVVM (Model-View-View Model) architecture, where
each view has a view model that provides the actual business logic, while the view
layer only performs drawing. There are many other alternatives, but SwiftUI makes
this choice quite a natural fit. The knowledge you’ll gain in this chapter isn’t tied to a
specific architecture, though.
You’re going to use SwiftUI’s Picker to show the user the possible modifications
they can make to the guitar. To track each of the user’s selections, you’ll need some
bindings in your view model.
Note: Some of the terms in this chapter are specific to SwiftUI. Because this
chapter’s focus is Functional Reactive Programming, you won’t dive into the
SwiftUI portions except to leverage your reactive streams.
Create a new BuildViewModel.swift file in your Build folder and add the following
code to it:
import Combine
In the code above, you’ve defined a new view model that conforms to
ObservableObject. This means that, using some SwiftUI Black Magic™, your
BuildView will automatically be notified whenever any @Published properties
change.
raywenderlich.com 309
Expert Swift Chapter 11: Functional Reactive Programming
You’ve also added four @Published properties to act as bindings for the various
guitar part pickers. You’ll use these in a moment.
Because all guitar additions conform to the Addition protocol, you can leverage such
a generic method to create all your pickers easily and link them to your new view
model.
As mentioned earlier, using @StateObject ensures your view keeps itself updated
based on the view model.
Then, in body below GuitarView but still inside the ScrollView, add the following
code:
VStack(alignment: .center) {
additionPicker(
for: Guitar.Shape.self,
selection: $viewModel.selectedShapeIdx
)
additionPicker(
for: Guitar.Color.self,
selection: $viewModel.selectedColorIdx
)
additionPicker(
for: Guitar.Body.self,
selection: $viewModel.selectedBodyIdx
)
additionPicker(
for: Guitar.Fretboard.self,
selection: $viewModel.selectedFretboardIdx
)
Spacer()
}
raywenderlich.com 310
Expert Swift Chapter 11: Functional Reactive Programming
You just added a vertical stack view with the four different pickers a user can change
in their guitar: shape, color, body wood and fretboard. Each of these is linked to a
specific binding in your view model, using the special $ annotation, which lets you
use these @Published properties as bindings.
Build and run your project, and you’ll notice the four different additions a user can
set for their guitar. Tapping each of these will show a list of options per addition,
along with any price adjustments:
To have a reactive guitar object, you’ll want to have it as an output of your view
model.
Go back to BuildViewModel.swift and add the following code to the end of your
current view model:
// Outputs
@Published private(set) var guitar = Guitar(
shape: .casual,
color: .natural,
body: .mahogany,
fretboard: .rosewood
)
This new @Published property will be the view’s source of truth as to what the
current guitar state is for the view to draw. Notice that it’s marked as read-only
outside the view model and can only be mutated by the view model itself.
How do you connect all the user’s choices into a single guitar, though? Not only do
you need to track the user’s selection, but you also need to make sure it happens
every time a change is made.
raywenderlich.com 311
Expert Swift Chapter 11: Functional Reactive Programming
This is quite simple, using a great operator called combineLatest. It tracks multiple
publishers and emits whenever any of them changes. Because all the user’s selections
are marked as @Published, you can use them just as if they were Combine
Publishers, using the $ prefix.
init() {
// 1
$selectedShapeIdx
.combineLatest($selectedColorIdx,
$selectedBodyIdx,
$selectedFretboardIdx)
.map { shapeIdx, colorIdx, bodyIdx, fbIdx in
// 2
Guitar(
shape: Guitar.Shape.allCases[shapeIdx],
color: Guitar.Color.allCases[colorIdx],
body: Guitar.Body.allCases[bodyIdx],
fretboard: Guitar.Fretboard.allCases[fbIdx]
)
}
// 3
.assign(to: &$guitar)
}
This might seem intimidating, but it’s quite simple once you get the hang of it. Your
new code does the following:
1. Combines the latest values of the selected shape, color, body wood and fretboard
indices. Whenever any of them changes, this operator will emit the latest values
tracked for all four publishers.
2. Uses another useful operator called map. In essence, this is like the map you know
from Swift’s standard library. Instead of transforming each element in an array,
you transform each emission from a publisher. You use it to create a new Guitar
object made from the user’s latest selection.
Now, your guitar property will always emit the actual latest Guitar object, based on
the user’s selection.
raywenderlich.com 312
Expert Swift Chapter 11: Functional Reactive Programming
GuitarView(viewModel.guitar)
Build and run the project, and you’ll notice that every change you make to any of the
guitar additions is immediately and reactively reflected in the guitar preview image,
as well as its caption.
You can now leverage this Guitar object for other portions of your view, such as the
current price.
"Checkout (\(viewModel.guitar.price.formatted))"
raywenderlich.com 313
Expert Swift Chapter 11: Functional Reactive Programming
Build and run the app, and you’ll notice that every change you make updates the
guitar price in the checkout button:
Subscription lifecycle
So far, so good. It seems viewModel.guitar is constantly emitting new Guitar
updates based on your selection. But how can you confirm that’s the case?
.print("guitar")
Build and run your app. Then, make a few changes to the selected guitar parts.
You’ll notice your console output showing output similar to the following:
raywenderlich.com 314
Expert Swift Chapter 11: Functional Reactive Programming
As the name suggests, print prints everything that goes through a specific point in
your reactive chain. Here, you’ll notice that every change you make to your guitar
prints a new value event with a new computed Guitar object.
What do you think would happen if you removed the assignment to $guitar,
though? The easiest way to find out is simply to try it.
Comment out the assign(to:) operator. Then, build and run again and make a few
changes to your guitar. What do you expect to happen here?
The most obvious issue you’ll notice is that your changes won’t be reflected in your
UI because you’re not assigning them to $guitar. But there’s a less apparent side-
effect going on here.
In Combine and other reactive implementations, your chain won’t produce any
events until you subscribe to it. In most cases, it won’t even perform the underlying
work until there’s at least one subscriber.
This makes sense — if there’s no one to listen or subscribe to a stream, why should it
perform work for nothing?
Before moving on, uncomment the assign(to:) operator and remove the print
operator.
raywenderlich.com 315
Expert Swift Chapter 11: Functional Reactive Programming
Getting to checkout
The basic functionality of your build view is done, but you’re still missing a few more
pieces to be able to move to checkout. Specifically, you’ll want to:
You’ll want to perform all these API calls simultaneously, show a loader and move
over to the checkout view when you have all the needed information.
It seems like a lot of work, but you’ll take care of it swiftly. Are you ready? Alright!
To start, it could be nice to sketch out a high-level implementation plan. Here goes:
Triggering requests
First, you’ll need some way to tell the view model “The user tapped checkout” so you
can react to that action and call the three API calls.
Open GuitarViewModel.swift. Below your guitar output, add the following private
property:
raywenderlich.com 316
Expert Swift Chapter 11: Functional Reactive Programming
Diving into subjects is unfortunately outside the scope of this book. But, in essence,
subjects are simple units that let you imperatively send values to them and have
those values published to all their subscribers. You can think of them as manually
controlled streams of data.
func checkout() {
shouldCheckout.send()
}
All you’re doing here is exposing some public interface, the method checkout, to
send an event to this subject. Next, you’ll react to this subject inside your view model
to actually perform some work.
Before moving on, you’ll also want to trigger this method when the user presses the
checkout button.
Back in BuildView.swift, find the ActionButton and add the following code inside
its closure:
viewModel.checkout()
Checkout
As mentioned in the previous section, you’ll need to make three separate but parallel
API calls to fetch all the data needed for the checkout screen.
All these calls are available to you under GuitarService. In BuildViewModel, below
your shouldCheckout subject, add an instance of GuitarService:
raywenderlich.com 317
Expert Swift Chapter 11: Functional Reactive Programming
2. Using an operator called handleEvents, which lets you sort of “peek in the
middle of the water flow” to perform side-effects on your stream in the specific
point where you add it. In this case, you only print the result.
If you run and build your app, you’ll notice this code doesn’t do anything. As
outlined in the previous section, publishers usually don’t do anything until
subscribed to. But before you do this, you should get the other two calls out of the
way. Add the following code below the previous code:
These two blocks are identical to the previous one, except that they’re for fetching a
time estimate for the guitar build and shipping options.
Note: In a real application, these publishers wouldn’t have a Never failure but
rather an actual error type. To keep this chapter more consumable, we’ve left
error handling out.
raywenderlich.com 318
Expert Swift Chapter 11: Functional Reactive Programming
There are many ways to connect publishers. For example, combineLatest, which you
used earlier, would emit all of the publisher values whenever any of them changed,
whereas merge would interleave results of different publishers of the same type.
What you want to do here is run these three requests in parallel and wait for all
publishers to emit a value, interlocked, and only then emit a single result.
To do this, you’ll use an operator with a known counterpart in the Swift standard
library — zip. To zip all three publishers, all you have to do is:
shipment.zip(estimate, availability)
But remember, you want to do this as a reaction to the user’s tap on the checkout
button. In this case, the shouldCheckout subject will be quite helpful. Add the
following code below your three requests:
shouldCheckout
.flatMap { shipment.zip(estimate, availability) }
.sink(receiveValue: { print("Got responses: ", $0, $1, $2) })
In this subscription chain, you’re starting from the trigger shouldCheckout. Once it
emits, you use an operator called flatMap on the zipped emission of the three
publishers: shipment, estimate and availability.
In essence, flatMap means “Transform this publisher into a different publisher”. This is
exactly what you’re doing here: sort of trading a user tap publisher into a network
request publisher.
There’s still an issue preventing you from actually trying this piece of code, though.
If you remember from the Subscription lifecycle section, when a subscription is
deallocated, it’s canceled. In this case, because no one is holding this subscription,
it’s immediately canceled.
To get over this, add the following temporary property to your class:
raywenderlich.com 319
Expert Swift Chapter 11: Functional Reactive Programming
Cancellable represents the subscription to the publisher and lets you call cancel()
on it.
cancellable = shouldCheckout
.flatMap { shipment.zip(estimate, availability) }
.sink(receiveValue: { print("Got responses: ", $0, $1, $2) })
Now, you’re ready to rock! Build and run your app, make some changes and tap the
checkout button.
The order of these requests might differ on your side because they run in parallel.
But note how the requests are running individually, while the zip emits a single
value only when all of them complete their work.
Before moving to the next section, remove the temporary cancellable property and
replace the previous section of code with:
This is the same code you had earlier but with a map operator added to transform the
different results into a singular CheckoutInfo result.
raywenderlich.com 320
Expert Swift Chapter 11: Functional Reactive Programming
This is a boolean property you’ll use to set the loading state of the checkout button
in BuildView. But how can you represent something loading?
Simple — user tapped the button? Loading. Any response returned? Not loading. This
calls for a different type of composition mentioned in the previous section, using an
operator called merge.
Add the following code to the end of your view model’s initializer:
Publishers
.Merge(shouldCheckout.map { _ in true },
response.map { _ in false })
.assign(to: &$isLoadingCheckout)
As mentioned earlier, this code uses Publishers.Merge, the underlying type for the
merge operator, to interleave the emissions of two different publishers that emit the
same output type. In this case — Bool.
raywenderlich.com 321
Expert Swift Chapter 11: Functional Reactive Programming
This sort of state machine represents the loading state of the checkout button. Here’s
the full data flow, so far:
ActionButton("Checkout (\(viewModel.guitar.price.formatted))",
isLoading: viewModel.isLoadingCheckout) {
viewModel.checkout()
}
This sets the isLoading state of the action button based on the emissions of the
view model.
Build and run your project. Then, tap the checkout button. You’ll notice the loading
state shows up and switches back to an active button when the response is loaded
successfully:
raywenderlich.com 322
Expert Swift Chapter 11: Functional Reactive Programming
This publisher will hold the checkout info once it’s available and will tell the build
view it’s time to switch to the checkout view.
All that’s left is to assign your response to this new property. At the end of your
initializer, add:
response
.map { $0 as CheckoutInfo? }
.assign(to: &$checkoutInfo)
This simply assigns the response to your new published property while mapping to
an optional version of CheckoutInfo to match checkoutInfo’s type.
Sharing resources
Your code works well right now, but there is a tiny (or yet, quite large) issue with it
that is quite hidden.
Notice that response is being used in two different streams: the loading state and
the assignment to checkoutInfo.
Build and run the app, and tap the checkout button. Looking at your logs, you’ll see
something like:
Due to the way Combine and other reactive frameworks work, each subscription gets
its own resource.
raywenderlich.com 323
Expert Swift Chapter 11: Functional Reactive Programming
To solve this issue, add the share() operator at the end of the response stream:
This will share the response resource among its subscribers, like so:
raywenderlich.com 324
Expert Swift Chapter 11: Functional Reactive Programming
Wrapping up BuildView
All that’s left for you to do is to present CheckoutView in response to
viewModel.checkoutInfo firing a value.
SwiftUI has a nifty trick to reactively present a view modally, using a modifier called
sheet.
It takes a binding of optional type: When it’s nil, the view is hidden, and when it has
a value, it’s presented. It will also take care of setting the binding back to nil if the
user actively dismisses the view.
.sheet(
item: $viewModel.checkoutInfo,
onDismiss: nil,
content: { info in
CheckoutView(info: info)
}
)
• item defines the optional binding the sheet works with. Using the $ prefix here
provides a Binding for the underlying published property.
• onDismiss lets you define what happens when the user dismisses the view. You’ll
come back to this in a moment.
• content is where you return the presented view from. CheckoutView has a specific
initializer to take in the needed checkout info.
raywenderlich.com 325
Expert Swift Chapter 11: Functional Reactive Programming
With all this done, build and run your app again, make a few changes and tap the
checkout button. You’ll notice that once the loading is completed, a checkout view is
modally presented, waiting for you to implement some new reactive features in it:
Note that if the guitar is unavailable to purchase, the entire screen is not
interactable.
Before moving on to checkout, you might notice that if you dismiss the view, the
price and the user’s selection are still visible on the screen. Usually, you might want
to reset the selection to let the user build a new guitar after checkout or an active
dismissal.
raywenderlich.com 326
Expert Swift Chapter 11: Functional Reactive Programming
func clear() {
selectedShapeIdx = 0
selectedColorIdx = 0
selectedBodyIdx = 0
selectedFretboardIdx = 0
}
This resets the user’s selection to the default indices, which is reflected in your view
as well.
Then, in BuildView, change the onDismiss argument in the sheet you’ve just added
to:
onDismiss: { viewModel.clear() },
Build and run your app again, and you’ll notice that dismissing the modal sheet also
resets your build view to its default state. Awesome!
Performing Checkout
Your checkout view already includes a solid layout of the screen you’ll work on,
displaying the guitar parts you chose in the previous step, the estimated build time
and availability you calculated, as well as available shipping options.
Most of the work has been done for you to not repeat what you learned in the
previous section. If you browse through CheckoutView and CheckoutViewModel,
you’ll notice it’s mostly similar to what you’ve done so far — connecting various
basic inputs and outputs for the view model.
Don’t worry though, there are still two big challenges ahead. In this section, you’ll:
• Allow changing the currency to one of four available currencies and present
updated pricing based on exchange rates fetched from a web service.
• Perform checkout, show a success message to the user and dismiss CheckoutView.
Here goes!
raywenderlich.com 327
Expert Swift Chapter 11: Functional Reactive Programming
To do this, you’ll:
1. Show the user a currency selector along with their current selection.
2. Get the exchange rate from U.S. dollars (USD) to the chosen currency from an
API.
3. Update all presented prices to reflect the exchanged currency with an appropriate
currency symbol.
You’ll use the first four properties to show the appropriate string price, including the
correct currency symbol. This is instead of directly accessing
guitar.basePrice.formatted, for example. The last isUpdatingCurrency
property will be used for the API call’s loading state.
raywenderlich.com 328
Expert Swift Chapter 11: Functional Reactive Programming
You’ll fix this next. Replace the entire contents of the method with:
URLSession.shared
// 1
.dataTaskPublisher(
for: URL(
string: "https://api.raywenderlich.com/exchangerates"
)!
)
// 2
.map(\.data)
.decode(type: ExchangeResponse.self, decoder: JSONDecoder())
// 3
.map { response in
guard let rate = response.rates[currency.code] else {
fatalError()
}
return rate
}
// 4
.eraseToAnyPublisher()
This might seem a bit convoluted, but you just created your first Combine-based
network request — hooray! You:
3. Retrieve the actual exchange rate from within the decoded object. If it doesn’t
exist for the provided currency, you crash because this is an invalid state.
raywenderlich.com 329
Expert Swift Chapter 11: Functional Reactive Programming
You’ll start with reacting to any selection of a currency. Add the following code to
CheckoutViewModel’s initializer:
return self.currencyService
.getExchangeRate(for: currency)
.map { (currency, $0) } // 2
.replaceError(with: (.usd, 1.0)) // 3
.eraseToAnyPublisher()
}
// 4
.receive(on: RunLoop.main)
.share()
1. React to each change of the currency. If the selected currency is USD, you
immediately return a rate of 1.0. Otherwise, you use
currencyService.getExchangeRate(for:) to fetch the right exchange rate.
2. Map the exchange rate as a tuple along with the requested currency.
4. Use a method called receive(on:) to ask the stream to deliver its values on the
main run loop and use the share() operator as you did before, so multiple
subscribers won’t cause multiple network requests.
Now, all that’s left to do here is to use your new currencyAndRate to calculate the
right price for each piece.
Add the following code immediately below the code you just added:
currencyAndRate
.map { currency, rate in
(Guitar.basePrice * rate).formatted(for: currency)
}
.assign(to: &$basePrice)
currencyAndRate
.map { currency, rate in
(self.guitar.additionsPrice * rate)
.formatted(for: currency)
raywenderlich.com 330
Expert Swift Chapter 11: Functional Reactive Programming
}
.assign(to: &$additionsPrice)
currencyAndRate
.map { [weak self] currency, rate in
guard let self = self else { return "N/A" }
In the portion above, you do the same thing multiple times. Whenever
currencyAndRate emits a new value, you take the guitar base price, the additions
price and the total price and multiply them by the exchange rate to get the correct
price.
You also pass currency to formatted(for:) to get the correct currency symbol
presented to the user.
The last missing piece here is the shipping option prices. All you have to do is replace
the way you’re instantiating your shippingPrices property.
At the top of your view model’s initializer, find the code of blocks that assigns
shipping options to self.shippingPrices and remove it.
// 1
currencyAndRate
.map { [weak self] currency, rate in
guard let self = self else { return [:] }
return self.shippingOptions
.reduce(into: [ShippingOption: String]()) { opts, opt in
opts[opt] = opt.price == 0
? "Free"
: (opt.price * rate).formatted(for: currency)
}
}
.assign(to: &$shippingPrices)
// 2
$shippingPrices
.combineLatest($selectedShippingOption, $isUpdatingCurrency)
.map { pricedOptions, selectedOption, isLoading in
guard selectedOption.price != 0 else { return "Free" }
raywenderlich.com 331
Expert Swift Chapter 11: Functional Reactive Programming
These two code blocks might look long, but they’re pieces of code you’ve already
written before:
1. You use the same code you instantiated shippingPrices with before. The only
difference is that you multiply the selected option by the exchange rate and use
formatted(for:) with a specific currency.
2. combineLatest, which you used before, will emit whenever either the shipping
prices or the selected shipping options change. Then, it would simply pick the
pre-calculated and exchanged price for the selected option.
Now’s the fun part — making your CheckoutView use all this data! Head over to
CheckoutView and find the “Totals” Section.
Picker("Currency",
selection: $viewModel.currency) {
ForEach(Currency.allCases) {
Text($0.symbol).tag($0)
}
}
.pickerStyle(SegmentedPickerStyle())
You just allowed the user to pick a specific currency instead of showing only a hard-
coded USD currency.
Next, you’ll change the USD guitar prices to your new, rate-aware prices.
TextRow("Additions", viewModel.additionsPrice)
TextRow("Shipping", viewModel.shippingPrice)
raywenderlich.com 332
Expert Swift Chapter 11: Functional Reactive Programming
Build and run the app, get to checkout and change the currency. You’ll notice that
not only the totals but also the prices of the various shipping options are updated:
The first issue is that every change the user makes will cause a network request, and
many users tend to just tap around the currencies.
You’re using two operators here — debounce will let through only the last change
made within the previous 500 milliseconds, while removeDuplicates won’t let
through duplicate currency changes.
Next, replace:
raywenderlich.com 333
Expert Swift Chapter 11: Functional Reactive Programming
With:
Finally, you’ll add a loading and disabled state quickly. You already created a
Published property for this. Add the following code to fill it as you did in the build
view:
Publishers.Merge(
currency.dropFirst().map { _ in true },
currencyAndRate.map { _ in false }
)
.assign(to: &$isUpdatingCurrency)
Back in CheckoutView, find the four TextRows of the totals section and add the
following last argument to all four initializers:
isLoading: viewModel.isUpdatingCurrency
This will set the loading state of the text row accordingly when the currency is being
updated.
It would also be good to disable ordering and other changes while getting a currency.
Find the existing disabled modifier and update it to take isUpdatingCurrency into
account, too:
!viewModel.isAvailable || viewModel.isUpdatingCurrency
Build and run the app, get to checkout and change the currency.
raywenderlich.com 334
Expert Swift Chapter 11: Functional Reactive Programming
You’ll notice the loading state and network request fires only when the user lets go of
the currency picker for over 500 milliseconds and that the various fields are redacted
until a valid response returns:
Checking out
All that’s left to do is check out. Knock this one out of the park!
You’ll use the first two to track the loading state of the ordering and whether the
user performed an order already.
You also use a subject to model the user’s tap on the checkout button, just like you
did in the build view.
raywenderlich.com 335
Expert Swift Chapter 11: Functional Reactive Programming
func order() {
shouldOrder.send()
}
All that’s left to do is to call the guitar service and update the various states. Add the
following code to the end of your view model initializer:
// 1
let orderResponse = shouldOrder
.flatMap { [weak self] _ -> AnyPublisher<Void, Never> in
self.map { $0.guitarService.order($0.guitar) } ??
Empty().eraseToAnyPublisher()
}
.share()
// 2
Publishers.Merge(
shouldOrder.map { true },
orderResponse.map { false }
)
.assign(to: &$isOrdering)
// 3
orderResponse
.map { true }
.assign(to: &$didOrder)
2. Assign isOrdering with the merge of the user’s tap on the order button and
getting back a response, just like you did before.
All the work and state are now in place, and all that’s left to do is finalize
CheckoutView.
raywenderlich.com 336
Expert Swift Chapter 11: Functional Reactive Programming
And, as cherry on top, find the .padding(.bottom, 40) modifier, and add below it:
if viewModel.didOrder {
ConfettiView()
}
This code will show a nice surprise for the user (and you) when successfully ordering
a guitar.
The last step is showing a successful alert and navigating the user back to the build
view.
// 1
.alert(isPresented: $viewModel.didOrder) {
Alert(
title: Text("Congratulations!"),
message: Text("We're working on your new guitar! " +
"Hang tight, we'll be in touch"),
dismissButton: .default(Text("Dismiss")) {
// 2
presentationMode.wrappedValue.dismiss()
}
)
}
1. Use the didOrder binding to show the alert when didOrder turns to true.
2. Upon the user tapping the “Dismiss” button, use the view’s presentation mode to
explicitly ask the view to dismiss itself.
You should also actually call viewModel.order() at some point to trigger this entire
chain.
Find the view’s ActionButton and change its isLoading argument from false to
viewModel.isOrdering. Then, inside the action closure, add the following line:
viewModel.order()
raywenderlich.com 337
Expert Swift Chapter 11: Functional Reactive Programming
That’s it! Build and run your app one final time, pick some guitar parts and order a
guitar. You’ll find the alert you just added, along with a fun surprise:
Yay, confetti!
raywenderlich.com 338
Expert Swift Chapter 11: Functional Reactive Programming
Key points
• Reactive programming is the notion of publishing changes for a specific piece of
state so your app can keep itself updated.
• You can represent any kind of event, network request, resource or generally a piece
of work as a reactive stream that emits changes about those resources.
• Many frameworks provide reactive capabilities for Swift developers. The most
common ones are Combine, RxSwift and ReactiveSwift.
• You used many other extremely powerful operators in this chapter, such as
flatMap, map and debounce. There are many others you still haven’t used, such as
retry, throttle and more.
• Reactive is what you make of it! Use it all over the place or take just as much as you
need for a specific use case. It’s a tool at your disposal.
• Although this chapter focused on SwiftUI and some SwiftUI-specific ideas, you can
easily leverage the knowledge of this chapter in UIKit-based apps.
You started by learning the basics of reactive programming as a paradigm, and then
put your skills to work by building a fully reactive SwiftUI app that uses Combine for
encapsulating various pieces of compound logic and state.
Even with this chapter’s length, it barely scratches the surface. To dive deeper into
Combine, check out our full book on the topic: Combine: Asynchronous
Programming with Swift.
raywenderlich.com 339
12 Chapter 12: Objective-C
Interoperability
By Shai Mishali
You love Swift. Otherwise, you probably wouldn’t be reading this book! Heck, you
might’ve even started your iOS journey with Swift without touching its much more
battle-tested ancestor — Objective-C.
In your own apps, you’ll often have a sizable Objective-C codebase that just doesn’t
feel at home inside your Swift code or want to use some of your shiny new Swift code
in your Objective-C code.
Luckily, Apple provides relatively thorough interoperability — hence, the ability for
Objective-C code to “see” Swift code and vice versa. But there’s only so much Apple
can do for you automatically, which is where this chapter kicks in!
raywenderlich.com 340
Expert Swift Chapter 12: Objective-C Interoperability
In this chapter, you’ll work on an app called BabyTrack, which lets you track what
your baby does: eat, drink, sleep, etc.
The version of the app you’ll start with uses its own Objective-C based framework —
BabyKit. You’ll spend this entire chapter creating a wholesome experience for
consumers of both the Objective-C and Swift portions of your codebase in a way that
feels as if it were designed for either.
Getting started
Open the starter project in the projects/starter folder and then
BabyTrack.xcodeproj.
• BabyTrack is the main app itself. It has several folders, which you’ll explore
throughout this chapter. The UIKit folder includes the current version of the app,
written entirely in Objective-C.
• BabyKit is an Objective-C based framework that the main target uses. It features a
Feed class and a FeedItem representation of a single item.
Pick the BabyTrack scheme. Then, build and run it. You’ll see an empty screen
momentarily, and then … darkness falls. Seriously though, it’s a black screen. What
gives?
raywenderlich.com 341
Expert Swift Chapter 12: Objective-C Interoperability
Umbrella header
You can think of an umbrella header as the master header of a framework. In the
context of a framework, it tells its consumers which framework portions are publicly
available without the headers needing to be manually imported one by one.
#import <BabyKit/Feed.h>
#import <BabyKit/FeedItem.h>
Try to comment these out and build the project, and you’ll immediately notice your
current code won’t build.
raywenderlich.com 342
Expert Swift Chapter 12: Objective-C Interoperability
#import <BabyKit/BabyKit.h>
This is all a consumer needs to do to use BabyKit, because its umbrella header
already takes care of everything. This is a common practice when creating your own
framework and a useful component of defining module maps.
Note: Module maps are not in the scope of this chapter, but as the name
suggests they are files that map different modules of a framework to their
respective headers. If you’re curious, find your DerivedData folder and open
the folder that starts with BabyTrack-. Inside Build/Products/Debug-
iphonesimulator/BabyKit.framework/Modules, you’ll find the coveted
module.modulemap file.
Although this is useful for your BabyKit framework, it still doesn’t solve the initial
issue. On to the next type of header.
Bridging header
A bridging header belongs to the scope of an app instead of a framework. As its name
suggests, it bridges Objective-C files into Swift by exposing any headers imported
into it to your Swift files.
#import "ViewController.h"
raywenderlich.com 343
Expert Swift Chapter 12: Objective-C Interoperability
All that’s left to do is to actually tell your app to use your new bridging header.
In your project, select the BabyTrack target and then the Build Settings tab on top.
Use the search bar to find the Objective-C Bridging Header field, then set the field’s
value to $(SRCROOT)/BabyTrack/Boilerplate/BabyKit-Bridging.h:
Build succeeds, huzzah! Run the app and you’ll finally see the basics of it on the
screen.
The top bar features buttons to track various activities in your baby’s day. Tap a few
of the buttons on top, and you’ll notice all items show up as “Ate food”:
Sounds like a quick bug for you to fix. Time to get to it.
raywenderlich.com 344
Expert Swift Chapter 12: Objective-C Interoperability
Enriching FeedItem
Open FeedCell.m and find configureWithFeedItem:. You’ll notice that there is
absolutely nothing in this configuration method that modifies the title or icon on the
left. The only “right” piece is the subtitle — the date itself.
Open FeedItem+Ext.swift and you’ll find several extensions waiting there for you:
an attachmentURL available on the FeedItem as well as an extension on
FeedItemKind you’ll use to get the appropriate title, emoji and color per item
kind.
extension FeedItem
With:
raywenderlich.com 345
Expert Swift Chapter 12: Objective-C Interoperability
Press the Related Items icon at the top-left of your editor window (or press Ctrl+1),
and then select Generated Interfaces ▶ BabyTrack-Swift.h:
As you see, Swift automatically synthesized this Objective-C property to mirror the
Swift counterpart you’ve annotated.
Now, go back to FeedItem+Ext.swift and add the @objc annotation to the second
extension on FeedItemKind.
raywenderlich.com 346
Expert Swift Chapter 12: Objective-C Interoperability
It’s not all magic and sparkles. There are some limits to the great bridging
capabilities provided to you. In this case, FeedItemKind isn’t a regular Swift enum as
you may come to expect.
Before continuing, remove the @objc annotation from the FeedItemKind extension
again.
Open FeedItem.h and you’ll notice typedef enum. But how does Swift represent
this? Like you did before, press the top-left Related Items button, then select
Generated Interface.
You can now see exactly how your Objective-C code is exposed to Swift. Look at the
definition of FeedItemKind:
It’s not a Swift enum at all, as expected! It’s actually just a plain C enum represented
by a UInt32, which is exactly why you can’t extend it from Swift.
No worries, though, because you can still work around this. However, before you do,
you’ll take a little detour to learn about what can and can’t be directly (or easily)
bridged between Swift and Objective-C.
raywenderlich.com 347
Expert Swift Chapter 12: Objective-C Interoperability
• Structs
• Tuples
• Global functions
• Type aliases
• Nested types
• Curried functions
Also, full-blown generics aren’t supported. However, Objective-C has quite robust
lightweight generics, which support a relatively large set of generic scenarios, such
as bridging Array<FeedItem> to NSArray <FeedItem *>*, Dictionary<String,
Any> to NSDictionary<NSString*, id>* and vice versa. It will even keep generic
constraints of your own Objective-C classes, so the following Objective-C class:
raywenderlich.com 348
Expert Swift Chapter 12: Objective-C Interoperability
Another interesting tidbit is the throw keyword. Throwing errors is a Swift feature
not supported in Objective-C, so instead an Objective-C error pointer reference
(NSError **) is provided as an argument to your Objective-C method. That method
would also automatically get a BOOL return type if it didn’t have any other return
type, so you can get both a yes/no failure as well as a detailed error object.
You can wrangle many of these unsupported Swift language features by wrapping
them in Objective-C supported types. For example, you can wrap a struct in a
lightweight class or type-erase a generic object that can’t easily be bridged.
Bridging back from Objective-C to Swift is also automatic as long as you import the
required headers into your bridging header. But similar rules apply here as well. For
example, you cannot annotate a non-class extension as @objc as you just noticed
when trying to use the extension on FeedItemKind.
These three static methods let you get the color, emoji and title for a specified kind.
Because they’re part of an @objc-annotated extension, they will also be exposed to
your Objective-C code.
Switch over to FeedCell.m and add the following import to the top of the file:
#import "BabyTrack-Swift.h"
This exposes all Swift content in the BabyTrack target to your FeedCell.
raywenderlich.com 349
Expert Swift Chapter 12: Objective-C Interoperability
These lines simply configure the emoji and title elements in the cell with the right
contents.
Then, find:
[NSURL new]
Replace it with:
feedItem.attachmentURL
All ready to go. Build and run your project, and you’ll finally see the items you’ve
added get listed correctly:
raywenderlich.com 350
Expert Swift Chapter 12: Objective-C Interoperability
@objc(colorForKind:)
static func color(for kind: FeedItemKind) -> UIColor {
kind.color
}
@objc(emojiForKind:)
static func emoji(for kind: FeedItemKind) -> String {
kind.emoji
}
@objc(titleForKind:)
static func title(for kind: FeedItemKind) -> String {
kind.title
}
You just used a variation of @objc in which you explicitly provide the full name
exposed to your Objective-C code.
Neat! As you just learned, Swift gets you most of the way there. But when you need
that granular level of control, it’s right there at your fingertips.
Note: There is one more variation of the @objc annotation, which you won’t
use in this chapter but is worth mentioning, called @objcMembers. Annotate a
class with it, and all of its members are automatically exposed to Objective-C
without having to attach @objc by hand to each member.
raywenderlich.com 351
Expert Swift Chapter 12: Objective-C Interoperability
Luckily, Objective-C provides a modern way to define an enum that is favorable for
Swift.
typedef enum {
With:
Also, at the closing line of the enum, remove the redundant FeedItemKind, leaving
only };.
Switch to the generated Swift interface for this header like you did before, and you’ll
notice your enum looks a lot different now:
Wow, the difference is like night and day! Not only do you get a real Swift enum, but
your cases no longer include the FeedItemKind prefix, exactly like you would expect
in a native Swift codebase. Also, each case is represented by a regular Int and not a
UInt32.
Build your project, and you’ll discover a dozen or so compilation errors caused by
your enum case names having changed.
Once you have fixed all your compilation errors, you’ll need to make one minor fix.
Head over to ViewController.m and find the for loop inside reload.
raywenderlich.com 352
Expert Swift Chapter 12: Objective-C Interoperability
Then, add the next two lines to the end of the loop:
These added lines ensure the buttons in the top action bar are also fed by your new
extensions.
Build and run. You’ll notice everything is working as expected. More impressive than
that, though, is that your Objective-C enum cases are still called
FeedItemKindBottle and the like, while your Swift case is simply called .bottle.
You just created an entirely native experience for consumers of your framework from
either side of the language war, and you’ll keep perfecting your code this way
throughout this chapter.
Before you get to the SwiftUI-specific part, you’ll take some time improving the
Swift-related part of your interaction with the BabyKit framework.
Improving nullability
Nullability in Objective-C is the parallel of using Optional in Swift. Generally
speaking, these nullability constraints are automatically bridged in a decent way but
not good enough for a demanding developer, such as yourself.
Open FeedCell.swift and add the following print statement inside the initializer:
print(feedItem.date)
raywenderlich.com 353
Expert Swift Chapter 12: Objective-C Interoperability
Wait, unwrap optional? All feed items must have a date attached to them, so why is
the date property optional to begin with?
To understand this, remove the print statement and switch over to FeedItem.h.
Take a look at the generated Swift interface as you’ve done earlier:
Notice the alarming force-unwrap sign ! scattered all over the place to denote an
implicitly unwrapped optional in your initializers, properties and return types.
Some of these should be optional, but most of them should not. The problem arises
because Objective-C types are implicitly unwrapped by default. Think about it: If
you’ve ever worked with Objective-C, you never thought too much about nullability at
the compiler level, because there is no compile-time nullability, just runtime
nullability, as is fit for a dynamic language.
Because there’s no obvious bridging to be done here, Swift marks all these various
pieces as implicitly unwrapped, as they are in Objective-C. It’s time for you to
improve this.
Head back to FeedItem.h. The portions that should be optional are attachmentId
and the date in the third initializer.
Replace:
raywenderlich.com 354
Expert Swift Chapter 12: Objective-C Interoperability
With:
Note that as soon as you add a single nullability type specifier, the compiler will start
emitting warnings until you resolve all pointers without one in that specific header
file.
In this case, the date and attachmentId parameters are both marked as nullable.
What’s the opposite of _Nullable, though? Simply — _Nonnull.
Replace the return type of the first method from FeedItem * to FeedItem *
_Nonnull, so it looks like this:
This annotation tells Swift that this initializer will return a non-optional FeedItem.
You’re probably flooded with warnings at this point and not too eager to add non-
null definitions to all the remaining code. Fortunately, the good folks at Apple have
you covered.
At the top of the file, immediately below the import statement, add:
NS_ASSUME_NONNULL_BEGIN
NS_ASSUME_NONNULL_END
Build your app, and you’ll see that all your warnings are gone!
This statement is the equivalent of “innocent until proven guilty” — or, in this case,
“non null until defined otherwise”.
raywenderlich.com 355
Expert Swift Chapter 12: Objective-C Interoperability
Feel free to remove the single _Nonnull you’ve added at this point.
If you switch back to the generated Swift interface for the header, you should see the
following definition for FeedItem:
Notice how clean everything looks now. Everything is non-optional, except for the
few occurrences you’ve defined, as if you’d written this in Swift to begin with (I won’t
tell if you won’t!).
Similar optionality fixes have already been done for you in Feed.h. Feel free to jump
over and check those out if you’re curious.
Setting up SwiftUI
With Feed optimized, it’s time for you to start working on the SwiftUI part of your
app.
raywenderlich.com 356
Expert Swift Chapter 12: Objective-C Interoperability
Nothing too exciting; just a simple “Hello, World!” view. Time for you to build the
SwiftUI counterpart of your Objective-C ViewController.
You’ll start by creating the top bar with the different kinds of activities you can track
in the app. Open AddFeedItemBar.swift and add the following two properties to
AddFeedItemBar:
// 1
HStack(spacing: 16) {
// 2
ForEach(kinds, id: \.self) { kind in
// 3
let type = kind == .sleep && isBabySleeping ? .awake : kind
Button(type.emoji) {
// 4
onKindTapped(type)
}
.frame(minWidth: 52, maxWidth: .infinity,
minHeight: 52, idealHeight: 52)
.background(Color(kind.color))
.cornerRadius(4)
}
}
.padding([.leading, .trailing])
raywenderlich.com 357
Expert Swift Chapter 12: Objective-C Interoperability
3. If kind is sleep but the baby is currently sleeping, you’ll decide to show the
awake action instead.
4. Display a button with the appropriate emoji and color, invoking onKindTapped
with the appropriate kind once tapped by the user.
In Objective-C, it is quite common to have such verbose naming, but in Swift, you
might have wanted to structure your code as follows:
extension FeedItem {
enum Kind {
// cases
}
}
Head over to FeedItem.h and find the FeedItemKind enum. Replace the ending
curly brace, e.g. }; with:
} NS_SWIFT_NAME(FeedItem.Kind);
With this tiny macro, you tell your code to still use FeedItemKind for Objective-C,
but — specifically for Swift — to expose this type as FeedItem.Kind. This is the sort
of attention to detail that makes your consumers not even notice they’re using an
underlying Objective-C framework.
Build your app, and you’ll notice a bunch of compiler errors. The quickest way to deal
with these would be to do a quick Find and replace for FeedItemKind to
FeedItem.Kind in all Swift source files, but you can also fix these quickly by hand.
raywenderlich.com 358
Expert Swift Chapter 12: Objective-C Interoperability
There will be a bunch of additional opportunities to use this trick throughout the
rest of this chapter, so keep reading!
It’s time to put something on the screen. Go to FeedView.swift, and replace the
contents of body with the following:
NavigationView {
VStack {
AddFeedItemBar(isBabySleeping: false) { kind in
}
.padding([.top, .bottom], 8)
List(1..<5) { i in
FeedCell(feedItem: .init(kind: .food))
}
}
.navigationTitle("BabyTrack")
}
.navigationViewStyle(StackNavigationViewStyle())
In this code, you wrap two items in a vertical stack — the AddFeedItemBar you just
created and a List with five fake FeedCells. You also wrap your screen in a
NavigationView.
Note: To save you time, FeedCell was already provided for you. Feel free to
check it out in FeedCell.swift.
raywenderlich.com 359
Expert Swift Chapter 12: Objective-C Interoperability
Nice! You’ve got the basics looking good, but you’re still not doing anything or
feeding this UI with real data. You’ll take care of this next.
raywenderlich.com 360
Expert Swift Chapter 12: Objective-C Interoperability
With SwiftUI, the information the user sees in the UI is always a function of your
state. This means that SwiftUI should be notified when the feed changes and update
the UI accordingly. But how?
If you were to build this entire app from scratch, making your Feed an
ObservableObject would be a sure choice — because your SwiftUI Views could
immediately be fed and updated by it.
Unfortunately, you already have a bunch of existing code in Feed.m, which you
definitely won’t want to rewrite. In a perfect world, you could have both a Swift Feed
and an Objective-C Feed, each tailored to its specific needs.
You can use this macro to hide Objective-C code from Swift consumers while
providing your own, Swifty alternative to it. This sounds like exactly what you need!
NS_REFINED_FOR_SWIFT
That’s it! It’s quite common to use this on individual properties or methods. But in
this case, refining the entire class would prove quite useful.
raywenderlich.com 361
Expert Swift Chapter 12: Objective-C Interoperability
More interestingly, if you start typing Feed, you’ll notice it doesn’t show up in auto-
complete anymore:
So where is it? The macro hid it for you using a neat trick — prepending two
underscores (__) to the class name. This prevents the auto-complete engine from
seeing it while still giving you access to it for refinement purposes.
In FeedView.swift, replace:
With:
Your code will now build with no issues. This also frees up the Feed symbol for your
Swift code, so you could make your own just like defined earlier.
In the project navigator, right-click the BabyKit folder and pick New file…. Then,
choose Swift File and name it Feed.swift. Make sure you have the BabyKit target
selected.
Next, replace the content of your new file with the following code:
import Foundation
import Combine
import SwiftUI
// 1
public class Feed: ObservableObject {
// 2
@Published public var items: [FeedItem] = []
// 3
private let legacyFeed = __Feed()
public init() {
// 4
items = legacyFeed.loadItems()
}
raywenderlich.com 362
Expert Swift Chapter 12: Objective-C Interoperability
// 5
public var isBabySleeping: Bool {
legacyFeed.babySleeping
}
This code is quite long, but all it does is wrap the original Objective-C Feed in a
SwiftUI-friendly shell. You:
3. Instantiate a copy of __Feed, your Objective-C feed. You’ll relay any of the heavy
work to it.
5. From here on, you simply mirror calls to the Objective-C feed while wrapping
each call in a SwiftUI-friendly way, making sure to update your @Published
items property as you go.
All done! Before moving on, there are two pieces that stand out as non-Swifty:
legacyFeed.babySleeping and legacyFeed.store(). You can use the same
NS_SWIFT_NAME trick here, as well.
raywenderlich.com 363
Expert Swift Chapter 12: Objective-C Interoperability
With:
Also, replace
With:
You’ve now configured Swifty names for both of these entities. Build your project,
spot the compiler errors and make the appropriate renames.
Fortunately, because Feed is a Swift class now, you can leverage some powerful Swift
tricks, such as @dynamicMemberLookup.
@dynamicMemberLookup
public class Feed: ObservableObject {
public subscript<T>(
dynamicMember keyPath: KeyPath<__Feed, T>) -> T {
legacyFeed[keyPath: keyPath]
}
This lets you perform a dynamic lookup to any Key Path between __Feed and a
generic T, meaning any property of __Feed.
Finally, remove the isBabySleeping property from Feed. With the power of dynamic
member lookup, you can now access every property of the Objective-C Feed directly
through the Swift Feed, with no additional boilerplate.
raywenderlich.com 364
Expert Swift Chapter 12: Objective-C Interoperability
Finalizing FeedView
You have almost everything ready to go to finalize FeedView. Head over to
FeedView.swift.
Replace:
With:
You’ve just replaced the legacy Objective-C Feed with your newly created SwiftUI-
centric Feed, which is annotated with @StateObject so it can automatically keep the
view up to date.
This code uses your Feed’s @Published items and creates a cell for each. This will
automatically be re-invoked whenever the items change.
raywenderlich.com 365
Expert Swift Chapter 12: Objective-C Interoperability
Build and run your app, and you’ll finally see your feed items show up on the screen:
If you look at the screenshot, you’ll notice that even though the baby is currently
sleeping, you’re seeing the sleep action instead of awake action. This is because
you’re still passing a hard-coded false to AddFeedItemBar.
raywenderlich.com 366
Expert Swift Chapter 12: Objective-C Interoperability
Replace:
With:
Build and run again, and your toolbar should show up correctly:
You’ll notice that tapping the buttons doesn’t do anything, yet. It’s time for you to
wrap this chapter up with two final changes.
// 1
print("Selected \(FeedItemKindDescription(kind))")
// 2
if kind == .moment {
// ???
return
}
// 3
feed.addItem(of: kind)
raywenderlich.com 367
Expert Swift Chapter 12: Objective-C Interoperability
2. If the user selects a moment, you’ll have them pick a photo. You still don’t have
all the parts to do this, so you’ll skip this for now.
3. If the user selected a non-moment action, you ask the Feed to add an item of the
selected kind.
Selected Awake
Selected Bottle
Selected Diaper
Selected Food
Selected Sleeping
You’ll also notice that, finally, items are shown in your list as you tap the buttons on
the action bar. Everything is working in sync and reactively updated by your Feed,
which is an ObservableObject.
As long as you’re here, how about taking care of this ugly global
FeedItemKindDescription function?
Head over to FeedItem.h and find the definition of the global function at the bottom
of the file:
NSString * FeedItemKindDescription(FeedItemKind);
How can you make this nicer? Wrap it in a different object? Refine it using
NS_REFINED_FOR_SWIFT? You could. But there’s a nicer way.
NSString * FeedItemKindDescription(FeedItemKind)
NS_SWIFT_NAME(getter:FeedItemKind.description(self:));
You just used a specialized version of NS_SWIFT_NAME that lets you define not only
your own name but also this global function as a getter of a different type. In this
case, FeedItemKindDescription(kind) is now simply kind.description, as you’d
expect. How magical!
raywenderlich.com 368
Expert Swift Chapter 12: Objective-C Interoperability
Build your project, and you’ll get this informative compiler error:
You can use the Fix button to automatically use your new Swifty naming, while your
existing Objective-C code can still use the regularly named global function.
Finally, to wrap up this massive undertaking, you’ll take care of adding a new
moment.
There are two problems with this in your new SwiftUI app:
Open AddMomentView.swift, where the basic shell of this has already been
provided for you with an AddMomentView struct conforming to
UIViewControllerRepresentable.
raywenderlich.com 369
Expert Swift Chapter 12: Objective-C Interoperability
Everything is done except for handling the user’s photo selection. At the end of
picker(_:didFinishPicking), add:
// 1
result.itemProvider
.loadObject(ofClass: UIImage.self) { [weak self] obj, err in
// 2
defer { self?.parent.isPresented = false }
// 3
guard let image = obj as? UIImage,
let parent = self?.parent else { return }
// 4
if let err = err {
print("Error in picked image: \(err)")
return
}
// 5
DispatchQueue.main.async {
parent.feed.addMoment(with: attachmentId)
}
}
In the above code, which won’t compile yet, you handle the user’s picked photo by:
2. Using a defer statement to set isPresented back to false at the end of the
current scope. This will cause the view to be dismissed.
3. Making sure you can unwrap the resolved object as a UIImage and that the
parent object (e.g. AddMomentView) is accessible
raywenderlich.com 370
Expert Swift Chapter 12: Objective-C Interoperability
To make the code compile, add the following property before the isPresented
binding at the top of the struct:
All that’s left to do is to use your new view. Head back over to FeedView.swift.
You’ll use this @State property to keep track of whether the add moment view is
currently displayed.
isPickingMoment = true
This sets the presentation state to true if the user tapped the moment action
button.
.sheet(isPresented: $isPickingMoment) {
AddMomentView(feed: feed,
isPresented: $isPickingMoment)
}
Inside the closure, you create a new AddMomentView, passing the feed so it can add
the new moment and passing the binding so the view can set it back to false when
it’s time for it to be dismissed.
raywenderlich.com 371
Expert Swift Chapter 12: Objective-C Interoperability
You could use NS_REFINED_FOR_SWIFT, but there is actually a much more fitting
macro to use here. Add the following macro at the end of
addMomentOnPresenter:completion:, before the ;:
For the final time, build and run the project and press the Add moment button in
the actions bar. A photo picker is shown to you:
raywenderlich.com 372
Expert Swift Chapter 12: Objective-C Interoperability
Once you select a photo, a new moment is added to your feed with the photo
attached:
Two languages (Objective-C and Swift), two UI Frameworks (UIKit and SwiftUI),
working in harmony, each served by its own naming conventions and APIs. This is
the essence of a perfectly implemented interoperability.
raywenderlich.com 373
Expert Swift Chapter 12: Objective-C Interoperability
Key points
• Objective-C is a powerful language, with relatively comprehensive bridging to
Swift. When that automatic bridging isn’t enough, you can get as granular as you
want with your Swift-specific customizations.
• A bridging header exposes Objective-C headers inside your app, while an umbrella
header exposes all Objective-C headers for a given framework.
• You can use the @objc or @objc(name) annotation to expose Swift types to
Objective-C.
• You can use NS_CLOSED_ENUM to bridge Objective-C enums into entirely Swifty
enums.
• If you want to have Swift-specific names for properties, objects or even global
functions as getters, use NS_SWIFT_NAME.
raywenderlich.com 374
Expert Swift Chapter 12: Objective-C Interoperability
You’ve also entirely replaced and wrapped the Objective-C Feed with your own Swift
Feed and used it in a SwiftUI app, leveraging features such as ObservableObject
and @dynamicMemberLookup that aren’t available to Objective-C, but still leveraging
the underlying logic of the SDK.
We highly recommend further exploring how you can stretch the boundaries of
bridging, and also explore Apple’s great documentation on the topic available at
https://developer.apple.com/documentation/swift/objective-
c_and_c_code_customization.
raywenderlich.com 375
13 Chapter 13:
Instrumentation
By Ehab Amer
Being a great iOS software engineer isn’t only about being a grandmaster of the Swift
language. It’s also about knowing which tools the platform puts at your disposal, how
to use them to sharpen your skills and how to identify areas of improvement in your
code.
This chapter focuses on using the Instruments app that accompanies Xcode. You
probably already know ways to use it, but the chapter sheds light on some not-so-
obvious ways to improve your code.
• How to identify lines in your code that take too much time to build.
raywenderlich.com 376
Expert Swift Chapter 13: Instrumentation
Getting started
In the starter folder, you’ll find multiple projects. You’ll use them all throughout this
chapter. To begin, open the project “TheExhibition”. Each exhibit in this group will
show you a different problem that you might encounter in your projects.
Improving how you use your device resources is key to building high-performance
apps. For the first section, you’ll cover memory.
Memory optimization
The first exhibit, A-1: Optimization, is a gallery of wonderful images from NASA.
These are high-quality photos. One of them has a resolution higher than 6,000×6,000
pixels!
raywenderlich.com 377
Expert Swift Chapter 13: Instrumentation
This exhibit has an effect on the app’s memory. You’ll use Instruments to identify
what’s causing this memory problem.
From Xcode, press Command-I to start profiling the app. Choose Allocations in
Instruments. Then press the Record button at the top-left corner.
As the app runs on the simulator, Instruments will show you every allocated object in
memory. The very top item in the list, All Heap & Anonymous VM, shows all your
app’s memory usage.
raywenderlich.com 378
Expert Swift Chapter 13: Instrumentation
While Instruments is recording, tap the first item in the simulator. The app will
freeze for a few seconds and Instruments will show a huge spike in memory usage.
One new allocation entry responsible for this large increase is VM:
ImageIO_AppleJPEG_Data. It has 21 allocations of almost 800 MB.
It might seem strange that your images can take up so much space when the overall
file sizes are much much smaller. You’ll find out why that happens in the next
section.
The first image has a resolution of 5011×3341 pixels. The size of each pixel is 4 bytes,
meaning that when this image is loaded in memory, it occupies almost 64 MB.
Images with such large resolution come at a cost.
But the gallery has only 12 images. Why does Instruments list 21 allocations? To
answer that, open MemoryOptimizationViewController.swift. Reduce the value of
totalImages to 11. Profile and record the app again.
Open the first screen and notice the new number of allocations on VM:
ImageIO_AppleJPEG_Data.
It’s 11, as it should be. How did that one image make a difference in 10 allocations?
The answer lies in the image itself.
raywenderlich.com 379
Expert Swift Chapter 13: Instrumentation
The last image has a resolution of 6228×6198 pixels. This takes over 147 MB of
memory to load! But the OS didn’t load this large amount of memory under one
object. It broke it down to 10 objects, which is why there are 21 total objects.
Next, you’ll see how you can change your app to use memory more thoughtfully.
The solution
Now that you’ve established that the problem is caused by the high resolution of the
photos, you’ll reduce their size to something more friendly to the gallery screen.
Open each image with the Preview app. Resize it from Tools ▸ Adjust Size. Change
the unit to pixels and the width to 1024. Leave the padlock closed to maintain the
aspect ratio of the image.
raywenderlich.com 380
Expert Swift Chapter 13: Instrumentation
It’s helpful to know the resolutions of the devices that will run your app. The
resolution of the iPhone 11 Pro Max is 2688×1242 pixels. A 12.9-inch iPad has a
resolution of 2732×2048 pixels. Knowing those numbers can help you fine-tune the
images so they look good but don’t take up unnecessary space.
Once you finish updating the sizes, profile and record your app on Instruments and
open the first screen.
You’ll notice that there was barely any freeze while the gallery loaded, and the
increase in memory use is much smaller.
Congratulations, you reduced the memory use of the gallery screen without having
to change any code. You can see that it’s worth paying attention to the assets you use
in your project. Higher-quality images mean a better-looking app, but they might use
too much memory. In this case, that extra improvement no longer adds value —
rather, it affects your app negatively.
In the next part, you’ll continue to improve your app’s memory usage. But now it’s
time to make sure all references to an object are cleaned from memory when you
finish with them. This sounds basic, but there are many details around the tools that
help you improve your object handling.
Memory leaks
Handling leaks is like the ABC’s of memory management. “Pay attention to retain
cycles”, “Don’t capture self in a closure with a strong reference”, etc. So what’s new
to say about the issue?
It’s the same story. But it might not always be as clear as you think!
In exhibit A-2: Leaks, you’ll see a memory leak that Instruments doesn’t capture at
all.
raywenderlich.com 381
Expert Swift Chapter 13: Instrumentation
The exhibit shows a very basic screen. It has a single button and a joke above it. Each
tap on the button fetches a random, awesome joke from https://
icanhazdadjoke.com/.
In your current Instruments window, add the Leaks item from the library. If the +
button is greyed out, you’ll need to stop recording first.
Profile and start recording the app, then open the second screen. Wait a few seconds,
then tap Back to close the screen, then open it again. Wait a few seconds and repeat
a couple more times.
raywenderlich.com 382
Expert Swift Chapter 13: Instrumentation
Here’s where this gets interesting. Select the Allocations section and search for
MemoryLeaksViewController.
How is that possible? Take it even farther. Build and run the app from Xcode this
time. Repeat the same process of opening and closing the second screen. Then open
the Debug Memory Graph from Xcode.
They are there! Instruments knew about them. Xcode knew about them. And yet the
Leaks meter knew nothing about them? Why not?
raywenderlich.com 383
Expert Swift Chapter 13: Instrumentation
protocol WriterProtocol {
func writeText(_ text: String)
}
Now, look at InformationWriter.swift. Its init takes a writer as the parameter and
creates a closure that calls writeText(_:) on that writer:
init(writer: WriterProtocol) {
writeOperation = { info in
writer.writeText(info)
}
}
You probably already noticed where the problem lies. The closure created in the
init(:) captures a strong reference to the writer. So adding [weak writer] should
do the trick.
But before you fix anything, build and run the app and open and close the leaks
screen a few times. Then, open the Debug Memory Graph and choose one of the
MemoryLeaksViewController instances that are still allocated.
raywenderlich.com 384
Expert Swift Chapter 13: Instrumentation
You’re familiar with ARC, short for Automatic Reference Count. It’s the system
Apple uses to manage memory, and it’s been around since iOS 4. You know about
weak, unowned and strong references. Those are the only possible combinations for
any reference type. The compiler tracks the count of references for each object. When
the count reaches zero, the compiler releases the object from memory.
The Debug Memory Graph works in a different way, however. It uses an approach
that’s much closer to a garbage collector than it is to ARC.
A graph can be precise or conservative, depending on how the collector constructs its
memory graph. To construct a precise graph, a collector must identify all pointers.
And for that, the code generator or compiler must keep pointers or any information
needed for the graph in an accessible and obvious way.
Xcode’s Debug Memory Graph is conservative. It’s built on top of the Leaks
instrument. In fact, they’re practically the same thing, just represented differently.
The Debug Memory Graph analyzes a snapshot of your app’s memory, regardless of
whether the app was built in Swift, Objective-C or even C. But the graph can identify
if the reference found was weak or strong.
raywenderlich.com 385
Expert Swift Chapter 13: Instrumentation
In the scenario you’re debugging, the memory leak is present, but the memory graph
is confused. It’s not sure if some references to the view controller are valid references
or not.
You can’t capture weak references for value types, and because protocols work for
value types and reference types, the [weak writer] part won’t be valid unless it’s
clear that WriterProtocol instances are always reference types.
Now that you know how to measure your memory usage, optimize it and make sure
that it’s cleaned up and nothing stays there longer than it should, it’s time to move
to the other valuable device resource you need to be aware of: the CPU.
You deserve to grab a drink and have a break before jumping to the next section.
raywenderlich.com 386
Expert Swift Chapter 13: Instrumentation
Performance
B-1 Time profiler, the 3rd cell in The Exhibition app, is a collection view of random
numbers along with how many times that number was generated. It doesn’t do
anything fancy. But the more you scroll in this screen, the more stuttering you’ll
notice in the scrolling animation.
You’ll use the Time Profiler instrument to help you identify what’s causing this
effect.
The number with the large font is the random number and the small digit in the
lower-right corner tells you how many times that number was generated.
The more you scroll, the rougher the scrolling gets. It might not be very obvious that
your app has a performance issue, especially in the beginning. But it’s worth making
sure either way.
raywenderlich.com 387
Expert Swift Chapter 13: Instrumentation
Profile and record the app using the Time Profiler instrument. Open the third
screen and spend some time scrolling.
Before looking at Instruments’ data, filter what it’s showing so you don’t get lost in
all the stack traces.
At the very bottom of the Instruments window, click Call Tree and check Hide
System Libraries. This will filter the call stack to calls related to your app, keeping
out system calls.
You can see something’s wrong here. For about 40 seconds of scrolling,
TimeProfilerViewController.collectionView(_:cellForItemAt:) is taking
over 80 percent of the execution time. And TrackedNumbersGenerator.generate()
is contributing to nearly all the CPU time.
raywenderlich.com 388
Expert Swift Chapter 13: Instrumentation
As you can see, saveTrackedNumbers() is the cause of all that CPU time.
Go to MachineTimer.swift and check the first line after the declaration of the
struct:
This method calculates the time spent between the creation of the timer and the
moment this method was called. The original value is measured in nanoseconds, so
to make it more human-friendly, it returns it in microseconds.
raywenderlich.com 389
Expert Swift Chapter 13: Instrumentation
cell.time = "\(timer.mark())μs"
Build and run. Open the third screen and see the new values:
Those numbers seem tiny in the beginning. Each is just a few milliseconds. Doesn’t
seem like much, right? But those few milliseconds are repeated for each cell, and as
you scroll, this spent time repeats over and over again.
Instruments already pointed out that this area accounts for over 80 percent of your
whole app’s CPU usage. You can definitely improve something here.
raywenderlich.com 390
Expert Swift Chapter 13: Instrumentation
Build and run. Open the third screen and see how the numbers changed:
Now, you see a few hundred instead of several thousand. Look at Instruments, too.
raywenderlich.com 391
Expert Swift Chapter 13: Instrumentation
You just turned off the saving feature, but your app still needs to save all the
generated numbers to the file. You’ll solve this in the next step.
The user will enjoy your fast and responsive app, and you’re safely storing the data
when the user is done.
Open AppDelegate.swift and add this method at the end of the class:
Disk access is an important performance factor, and you’ve just solved one more
issue on your app. Way to go!
In the next section, you’ll use Core Data to read info from the disk. Although Core
Data abstracts that operation for you, a few changes can highly improve how many
times you fetch data from the disk.
raywenderlich.com 392
Expert Swift Chapter 13: Instrumentation
Core Data
C-1: Faults Optimization lists all the countries in the world and the continent
where each resides. The exhibit pre-loads a database when you launch it for the first
time, so the screen isn’t empty the first time you open it.
The database schema has three tables: Countries, Continents and Languages:
Countries has a many-to-one relationship with Continents. Each country has only
one continent, but each continent can have many countries. It also has a many-to-
many relationship with Languages.
raywenderlich.com 393
Expert Swift Chapter 13: Instrumentation
Profile and record the app with the Core Data Instruments template, or add Faults
to your existing one. Open the last screen.
Before you scroll through the content, Instruments will show you six Core Data
Faults. Once you start scrolling, a seventh will appear, and that’s it! No matter how
long you scroll, up or down, no more faults will appear.
You might ask why they were seven faults? That’s easy. It’s because there are seven
continents, at least in this database.
So what is a fault? To explain quickly, it means that the app attempts to read an
object from memory, because that’s the first location and has a higher I/O speed.
However, that object is not present, so it automatically tries to read from another
location — the disk, with a slower I/O speed. It then updates the first location with
that information in case it was requested again.
Processors already use the concept of fault. When you launch an app, not all of it is
loaded in memory to be executed. Instead, the processor loads only the parts that are
needed at the time, and it will automatically bring in more parts of your app from the
disk as you continue to use it. Memory is much faster than disks, but also much
smaller — and, therefore, more expensive.
When you initialize Core Data, it won’t load all of its data to memory. It will load
only the information you fetch, which in this case is the list of countries.
Although each country has other relationships, those aren’t loaded in memory at
first. Reading each continent’s information for the first time triggers a fault to read
that object from the database and store it in memory.
raywenderlich.com 394
Expert Swift Chapter 13: Instrumentation
Each fault takes some time to resolve, and Instruments can show you how long each
fault took.
Those faults aren’t causing any performance impact, but as you saw in the previous
example, even tiny impacts can become a problem when they’re repeated too often.
raywenderlich.com 395
Expert Swift Chapter 13: Instrumentation
Now you can see there are many more faults going on. When each language object is
accessed for the first time, it’s read from the database. As you scroll, more faults will
appear.
Open CoreDataManager.swift, and in allCountries(), add this line right after the
creation of request:
request.relationshipKeyPathsForPrefetching =
["languages", "continent"]
Profile, record the app and open the last screen. You won’t see any faults occurring at
all on Instruments.
raywenderlich.com 396
Expert Swift Chapter 13: Instrumentation
That’s because you specified on the fetch request to pre-load the objects in those
relationship properties.
If you have a large operation on your database, this could greatly improve your
performance.
However, it’s also good to know that you can unload objects from memory. This is not
deallocation. This is the opposite of loading a fault. You are unloading the object
while maintaining its reference. When it’s accessed again, a fault will trigger.
func clearMemory() {
context.refreshAllObjects()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CoreDataManager.shared.clearMemory()
}
Profile and record the app. Open the last screen and start scrolling as soon as the
screen opens.
raywenderlich.com 397
Expert Swift Chapter 13: Instrumentation
Notice that faults only started to appear exactly one second from the fetch request.
clearMemory() returned all pre-fetched objects to their original state as faults.
So far, you’ve learned about optimizing your app for the user, making it run faster
and smoother. So far, 99% of the optimizations you learned about benefit the user.
What about optimizing the app for yourself, the developer? You’ll cover that next.
Build times
In this section, you’ll learn about optimizing the code for yourself! Yes, that’s right.
You can reduce the time you spend waiting for your app to build.
You might feel that the time you spend waiting for a build to be ready is barely
noticeable. That’s often true, because most of the time, you’re doing incremental
builds. Xcode is only building the parts that you’ve modified since the last time you
built the app. But do a clean build and you’ll notice that takes much longer.
If you work in a big team, and you have a build server with a CI/CD pipeline with unit
tests and a lot of fancy, automated stuff, you know a thing or two about waiting for a
build to complete.
Of course, the more your project grows, the longer it takes to build. However, this
isn’t the only factor affecting build times.
Some parts of your code, such as long equations or lots of chained commands, might
require a little extra effort for the compiler to compile as they might be long
statements or many things that need to be resolved or inferred before the compiler
knows what are you talking about similar to this sentence you are just reading now
that feels long and should be broken down into smaller sentences to be easier to
understand!
See what I did there? That long sentence probably took you a little longer to
understand. If I had split it into a few smaller sentences, it would have been easier to
read.
In this sample project, you’ll cover how to measure the build times of your code.
The SlowBuilder project demonstrates a few different slow building methods. The
project doesn’t do anything and you won’t need to run it.
raywenderlich.com 398
Expert Swift Chapter 13: Instrumentation
Open the project and build it. Nothing special happens. Open the Report Navigator
(Cmd+9), and click on the first entry in the list (that’s the latest build), then choose
All Messages if it’s not already chosen. Expand the information under Compile
Swift source files (x86_64) and under Compile SlowMath.swift (x86_64).
Nothing helpful here. Now, in your target’s Build Settings, search for Other Swift
Flags and add this flag for the Debug:
-Xfrontend -debug-time-function-bodies
Build the project again and look at the same part in the build log.
raywenderlich.com 399
Expert Swift Chapter 13: Instrumentation
It shows you the time it took to build each method in that file. The rest of the files
will also have the same new information. Add this flag, too:
-Xfrontend -debug-time-expression-type-checking
Then, build and look at the same part of the build log.
Even more information is written in the log. It now shows the time it took to build
each expression along with the time for each method. This is very informative but
not very user-friendly.
-Xfrontend -warn-long-expression-type-checking=25
-Xfrontend -warn-long-function-bodies=25
The first will display a warning when an expression exceeds the threshold. The
second will show a warning when a function or a method took too long. The
threshold for each is set for 25 milliseconds.
Clean the build folder, then build the project again. Notice the presence of a few new
warnings.
raywenderlich.com 400
Expert Swift Chapter 13: Instrumentation
func printArray() {
var doubleArray = Array(repeating: 1.123, count: 100)
doubleArray = doubleArray.map { $0 * Double.random(in: 0 ..<
1000) }
var intArray = Array(repeating: 1, count: 100)
intArray = intArray.map { $0 * Int.random(in: 0 ..< 1000) }
let doubleMultiply = zip(doubleArray, intArray)
.map { $0 * Double($1) }
let sum = doubleMultiply.sorted()
.map { $0 * 123 }
.reduce(0, +)
print(doubleMultiply)
print(sum)
}
raywenderlich.com 401
Expert Swift Chapter 13: Instrumentation
What seems to be common between all those slow building methods and expressions
is chaining operations together.
Xcode doesn’t seem to be complaining at all about how long this method took to
build. If you check the build log to make sure, you’ll find it took a very tiny amount of
time. For me, it took half a millisecond. So the theory that chaining expressions
causes long build times is not 100% accurate.
The factor most affecting the build time here is the chaining of different operations
that have different precedence and associativity. It probably took you awhile to read
it yourself.
Breaking down expressions with large build times into smaller ones is an
improvement. Sometimes you can save actual seconds, while in other instances the
time saved may not be noticeable. But it is worth measuring occasionally, especially
if you’re using a build server and have a large team.
The possible causes of a build-time spike are many, and the significance of this
impact will vary from one Xcode version to another. However, the way to identify and
measure the time taken for your code to compile is the same.
Now, you’ve learned how to use existing instruments to measure defined meters on
your app. In the next section, you’ll see how you can use a new instrument to
measure something new — and even measure anything you want, at your discretion.
raywenderlich.com 402
Expert Swift Chapter 13: Instrumentation
Timelane
Tracking the progress of asynchronous code is usually not a trivial feat. To help with
that, Marin Todorov developed a fantastic instrument called Timelane. You can
download it from: http://timelane.tools.
Timelane is a plugin that you install on Instruments, and it works like the other
templates you already have.
In this section, you’ll learn how to use Timelane to help you measure and track
asynchronous code. The FarOutPhotos project has a bigger gallery of NASA
pictures. You use the image library, Nuke, to download the images asynchronously.
raywenderlich.com 403
Expert Swift Chapter 13: Instrumentation
The project uses the standard Nuke library to download the gallery images. When
you tap one of them to open the details, the Combine extension for the library opens
the image in full size. The full-sized image displays over two steps. First, the
extension downloads the resized image from the gallery. Then, it shows the full-
sized image right after.
You’ll use Timelane to track when each image was requested, how long it took to be
retrieved, if it was completed or canceled and its download progress.
raywenderlich.com 404
Expert Swift Chapter 13: Instrumentation
To add Timelane’s Swift package, open the menu File ▸ Swift Packages ▸ Add
Package Dependency.
Note: At the time of writing this chapter Timelane 2.0.0 was the latest version.
import TimelaneCore
raywenderlich.com 405
Expert Swift Chapter 13: Instrumentation
Right after the declaration of the class, define a new subscription lane labeled
Gallery to log the events on:
Add those three lines in their correct locations in the switch statement:
// for .started
galleryLane.begin()
// for .cancelled
galleryLane.end(state: .cancelled)
// for .completed
galleryLane.end(state: .completed)
Profile and record the app. Use the Timelane template in Instruments.
This instrument now shows you visually the duration of each begin() and end(:).
However, it’s not clear from this graph which line represents which image.
raywenderlich.com 406
Expert Swift Chapter 13: Instrumentation
There’s also a slight problem with the code you added. You only defined one lane
named Gallery, and all the events are logged onto this lane. Because your code is
asynchronous, the order of begin() doesn’t need to call the same as end(:). For
that, you need to have a lane for each request.
Multiple lanes
Replace the lane you defined at the beginning of the class with a dictionary of lanes:
let imageName =
imageTask.request.urlRequest.url?.lastPathComponent ?? ""
switch event {
case .started:
let lane = Timelane.Subscription(name: "Request " + imageName)
lane.begin()
galleryLanes[imageTask] = lane
print("started " + imageName)
case .cancelled:
let lane = galleryLanes[imageTask]
lane?.end(state: .cancelled)
galleryLanes[imageTask] = nil
print("canceled " + imageName)
case .completed(result: _):
let lane = galleryLanes[imageTask]
lane?.end(state: .completed)
galleryLanes[imageTask] = nil
print("completed " + imageName)
case .progressUpdated(
completedUnitCount: let completed,
totalUnitCount: let total
):
let lane = galleryLanes[imageTask]
let percent = completed * 100 / total
lane?.event(value: .value("progress: \(percent)"))
print("progress for \(imageName): \(percent)")
default:
print("default")
}
raywenderlich.com 407
Expert Swift Chapter 13: Instrumentation
Uninstall the app from the simulator to remove any cached images. Profile, record
the app and look at Instruments again.
Also, under the subscriptions group, there’s another group for the events. You’ll find
all the progress events you triggered are grouped for each subscription. In the
graphical view, select the Events over time graph, then in the bottom select All
Events by Subscription.
raywenderlich.com 408
Expert Swift Chapter 13: Instrumentation
When you select one of the events in the list, the time marker at the top moves to the
location of that event to show you the moment that this happened.
Profile and record the app again. This time, scroll quickly up and down.
raywenderlich.com 409
Expert Swift Chapter 13: Instrumentation
Blue bars represent completed operations, green represents operations that are still
in progress and orange represents canceled operations.
When you scroll quickly, Nuke automatically optimizes requests. When a request for
an image on a cell isn’t completed and another one is triggered for that same cell,
Nuke cancels the first one in favor of the latter — hence the cancel event. A picture is
worth a thousand words, isn’t it?
TimelaneCombine
Open PhotoViewController.swift and import TimelaneCombine:
import TimelaneCombine
let resizedImagePublisher =
ImagePipeline.shared.imagePublisher(with: resizedImageRequest)
.lane("Resized Image")
let originalImagePublisher =
ImagePipeline.shared.imagePublisher(with: url)
.lane("Full Image")
Profile and record the app. Select any of the images in the gallery and notice the
addition of two new lanes in the subscriptions group.
The begin and end calls were automatically executed for the Combine subscriber, and
you didn’t need to track anything.
raywenderlich.com 410
Expert Swift Chapter 13: Instrumentation
Showing information visually is much easier than searching for it in a console log.
You can see how crowded operations were and when they all ended with a glance.
Visual representation can help you do much more than this. You can quickly spot
anything wrong with the order of your operations, or if something was supposed to
finish but gave an error instead.
To learn more about Timelane, visit http://timelane.tools. The more you learn about
it, the better you will use it — especially with Combine and RxSwift.
Signpost
Now that you saw a custom instrument, explore what made it possible and how the
events are logged from your app to instruments.
Two key points are used on the app side: OSLog & os_signpost.
The first uses the system’s log to write any logs directly from your app. The second is
a type of log entry to write down.
import os.signpost
This created an instance of a new log, with the AppID for its subsystem, and the
category PhotoGallery. The category can be anything, but it’s always helpful to
have relevant names.
raywenderlich.com 411
Expert Swift Chapter 13: Instrumentation
Profile the project, create a new Blank Instruments project and add the os_signpost
instrument from the library.
Record the project and right away you’ll see entries. You don’t want all of them, so
expand the instrument until you find the row/lane called com.raywenderlich.Far-
Out-Photos. Inside, you’ll find PhotoGallery. Those were the names you used to
define the log object. You’ll also see several ImageDownload intervals.
Each os_signpost end call pairs with a begin call that has the same value in the
name parameter. But because they all have the same name, Instruments seems to
have just paired them in reverse order: The last one that began is the first to end.
Also, this is not the order of the images loaded on the screen.
raywenderlich.com 412
Expert Swift Chapter 13: Instrumentation
You could think that this is easy: Why not just put the image name as that string?
But when you try that, Xcode complains. The name parameter must be a static string.
The way to disambiguate between different begin and end signposts is by using
OSSignpostID.
Providing this ID along with the name in the begin and end calls means that
matching is done when a begin and an end both have the same name and the same
SignPostID.
This created an OSSignpostID object with the image task. This makes sure that you
have the same ID for the same image task.
Remove the app from the simulator to delete the cached images, then profile and
record.
It doesn’t look like an upside-down pyramid anymore, so this solved the matching
problem. But it doesn’t show much information on which image has which bar.
raywenderlich.com 413
Expert Swift Chapter 13: Instrumentation
os_signpost(
.begin,
log: log,
name: "ImageDownload",
signpostID: signpostID,
"%{public}s",
imageName)
The parameters right after the signpost ID are a format string followed by the
arguments needed. The format string follows the same formatting rules used by the
Logging. Here, you only want to log the image name as a public string.
Now, the bars are much more informative. The next part is to track if the image was
completed or canceled. For that, you’ll add different metadata on their signposts.
The signpost you already have for the completed case should have "completed" as
its metadata, and the canceled one to have "canceled".
os_signpost(
.end,
log: log,
name: "ImageDownload",
signpostID: signpostID,
"completed")
raywenderlich.com 414
Expert Swift Chapter 13: Instrumentation
os_signpost(.end,
log: log,
name: "ImageDownload",
signpostID: signpostID,
"canceled")
Delete the app again, then profile and record. Scroll up and down a couple times as
soon as the app starts on the simulator.
The information on the bars might not be very clear, especially for the canceled
items, because the bar will be rather narrow — which is a good thing. Look at the
table below the graph.
You’ll see signposts that began, how many times, total duration, min and average
duration — some very useful statistics. Most importantly, they will also show you the
metadata you received with the end signposts. The screenshot shows two canceled
and one completed for this image. That’s a lot of useful info right there!
Your events are present in the graph, but it might be difficult to identify them. It’s
better to see them from the table below in the List: Events section.
raywenderlich.com 415
Expert Swift Chapter 13: Instrumentation
This only scratches the surface of what you can do with os_signpost. You can
provide metadata that Instruments can understand so it automatically creates useful
statistics for you.
You can create custom instruments that give you more control over the UI and how
the information is provided.
The Instruments Developer Help is an awesome place to find more about what you
can do with Instruments. The more you understand the tool and what it has to offer,
the more you’ll find yourself already knowing solutions to challenges you come
across.
raywenderlich.com 416
Expert Swift Chapter 13: Instrumentation
Key points
In this chapter, you learned a lot about some of the tools available to understand and
measure what’s happening inside your app. You learned how to:
• Measure the performance of your code and the time it takes to execute it.
• Understand how faults work in Core Data, how to avoid them and how to reduce
the memory usage when you’re done using fault objects.
• Use the fantastic measurement instrument, Timelane, which shows the progress
of your asynchronous code.
raywenderlich.com 417
14 Chapter 14: API Design
Tips & Tricks
By Shai Mishali
You’ve spent most of your time in this book diving into specific topics, learning how
they work, writing code to sharpen your instincts around them and working through
real-life examples.
Although using Swift and all its incredible capabilities is a wonderful skill to have, it
doesn’t help much without actually shipping code with it. More often than not,
though, you’ll find yourself creating code that’s used not only by you but also by your
team, or even other teams if you’re creating an open-source project.
In those cases, just knowing Swift as a language isn’t enough, and neither is just
practicing a specific language feature. That’s why this chapter is going to be a bit
different.
raywenderlich.com 418
Expert Swift Chapter 14: API Design Tips & Tricks
In this chapter, you’ll explore a few different topics. Each of these isn’t directly
related to the previous one, but they all tie into enhancing your skillset and intuition
for designing great APIs. You may freely explore each of these individual topics
based on your interest:
• How to separate and encapsulate your implementation details from your public
API using access levels.
• Powerful language features with examples you can leverage for your APIs,
including examples from Swift itself: Literals, Dynamic Member Lookup, Dynamic
Callable, Property Wrappers and others.
• Finally, a few important concepts and ideas related to the process of shipping your
API to the world.
This chapter will also be less code-heavy than previous ones and won’t require you
to copy-paste code or run any project. It’s more of a philosophical and exploratory
chapter that doesn’t cover one specific topic. You’re welcome to stop at any point to
experiment with a specific idea in Xcode. We’re opening a discussion together, me
and you, to hopefully help inspire you with some fresh new ideas and ways of
thinking about API design.
Note: API design is a highly opinionated topic. As such, you should take
everything in this chapter with a grain of salt and mainly as an inspiration
rather than a single truth. Take the portions that make sense for your use case
and taste, and discard the ones that don’t.
The first time a developer interacts with a new piece of code, they have certain hopes
and expectations. It doesn’t matter if that code is your app and the person is a new
developer on your team, or if it’s an open-source library you shared with the
community and the person is a new consumer of it.
raywenderlich.com 419
Expert Swift Chapter 14: API Design Tips & Tricks
In essence, developers look for some characteristics that make an API “feel good”:
• It should be obvious: This means using the API “makes sense” for a developer and
that your API’s design aligns with their expectations. For example, a Zip class
might have an expected method called unzip or extract rather than
pullContentsTo(path:), which is not common or obvious.
• Reduces mental load: This ties to being obvious but is a bit broader and more
open to interpretation. Some things that fall into this category include trying to
use the minimum obvious naming for APIs, using prior art if a convention exists in
the domain you’re developing for (you might use View instead of Screen if it
makes sense in that domain, for example) and using abstractions that are simple to
the consumer. As the Swift API guidelines sharply note: “Don’t surprise an expert.
Don’t confuse a beginner.”
• Being modern: This point touches a wide range of topics. Using proper language-
specific conventions, leveraging the correct language features a consumer would
expect to see and inspiring proper usage and creativity from the consumer are all
small parts of this point.
When exposing some API or functionality to the outside world, you should ask
yourself the same question, in a way: “What is the core functionality of this
framework or API?”
This starting point might seem obvious, but it’s so crucial because it helps dictate
where you’ll spend most of your effort forging an easy-to-use and explorable API.
raywenderlich.com 420
Expert Swift Chapter 14: API Design Tips & Tricks
It’s the exact point when you start differentiating your public-facing API from your
implementation details that aren’t quite relevant to the majority of your consumers.
If you’ve never written code meant to be consumed outside your app, you might not
understand the need for such a level of control over your code and its entities. But
it’s crucial to understanding what each level means and when to use it.
Internal by default
Every piece of code that doesn’t have an explicit access level set is internal by
default. This means other files in the same module can access it, but files outside of
the module can’t.
raywenderlich.com 421
Expert Swift Chapter 14: API Design Tips & Tricks
// In Module2
func getThings() {
// In Module1
Module2.getThings()
// Error: Module 'Module2' has no member named 'getThings'
This is great for most apps. It means every piece of code you write is accessible for
every part of your app because it’s usually a single module. But what if you want to
split your app into several modules or share code as a public library/framework?
This means you might want to make your UserService public to let anyone consume
it but keep NetworkService internal because only your module cares about it.
// In Module1
public class AmazingClass {
public init() { }
}
// In Module2
AmazingClass() // OK
WonderfulClass() // OK
raywenderlich.com 422
Expert Swift Chapter 14: API Design Tips & Tricks
Keeping it private
With public and open representing the more permissive side of the possible access
levels, it’s also critical to properly limit access to the private portions of your code.
These pieces of code are often implementation details and don’t concern consumers
of your public interface or even your internal interfaces.
• private makes an entity available only to the file it was defined in, and in the
specific scope it was defined in.
• In contrast, fileprivate makes an entity available only to the file it was defined
in but also in different object scopes.
For example, imagine an Encrypter type that can encrypt various types:
struct Encrypter<Encrypted> {
let base: Encrypted
}
Then, imagine you have a Person struct with a private property called password:
struct Person {
let id: UUID
let name: String
private let password: String
}
If you extend Encrypter in the same file to provide encryption for Person, like so:
You’ll get an error because password is only accessible in Person’s scope within the
file:
If you change private to fileprivate, the code will work, extending password’s
access scope to other types in the same file.
raywenderlich.com 423
Expert Swift Chapter 14: API Design Tips & Tricks
Finally…
No, this isn’t the end of the chapter, but final has a different meaning you should
know. I mentioned that public means an entity is public outside of a module but
can’t be overridden and subclassed.
Another useful keyword is final, which essentially means the same thing but also
applies to the module scope. This means a final class can’t be overridden or
subclassed, inside or outside of the module.
This is more useful in an app’s scope or a tightly-held module because it limits what
can be done with that class. It also helps the compiler perform optimizations because
it can know for a fact that none of the methods can be overridden and the class will
remain unchanged:
Some of this information might be harder to figure out in a large class or code base.
Luckily, Xcode can help out a bit with generated interface previews.
import Foundation
raywenderlich.com 424
Expert Swift Chapter 14: API Design Tips & Tricks
struct Test {
let id: UUID
let name: String
let topic: String
}
And then go to the Related Items icon and pick the generated interface for your
Swift file:
You’ll see the full interface generated from your Swift source:
raywenderlich.com 425
Expert Swift Chapter 14: API Design Tips & Tricks
Notice how the private methods aren’t part of the interface and all the implicitly
internal definitions show internal explicitly.
This a great way to get an “eagle’s eye” view of your codebase, stripping out the
implementation details and anything in the private scope.
Now that you have a good grasp of the higher-level ideas of good API design and
encapsulation, you’ll also want to know how to leverage specific Swift language
features to enrich your API.
Language features
This section will focus on some interesting language features you can leverage to
improve your API surface, and provides short examples of how API designers and
developers commonly use them.
Although using the latest and greatest language features isn’t a hard requirement for
API design, it’s extremely valuable to know the tools at your disposal to forge the
most natural and modern-feeling API possible for your consumers. You’ll learn more
about this throughout this section.
Literals
Literals are a great abstraction to let consumers initialize your types using typed
literals, such as String, Bool, Array and many others.
raywenderlich.com 426
Expert Swift Chapter 14: API Design Tips & Tricks
// Option 1
Path("/Users/freak4pc/Work/")
// Option 2
let path: Path = "/Users/freak4pc/Work/"
The great power of this feature is that it creates a relatively smooth and seamless
developer experience while still providing type safety and additional features related
to the specific initialized type. In Path’s case, it can expose a relativePath(to:)
method that isn’t relevant for any String but is interesting for Paths.
You can do the same with other expressible types, such as Arrays:
This example is a bit contrived, as you could achieve the same effect by simply using
sorted(by: <) internally. But it does provide a type-safe guarantee that you can
always expect values in that array to be sorted alphabetically, which improves clarity
in the API surface.
Another possible use case for literals is for a headers object in a networking library:
raywenderlich.com 427
Expert Swift Chapter 14: API Design Tips & Tricks
This implementation lets you initialize a new Headers object from a dictionary with
strictly typed keys, like so:
class HTTPRequest {
func addingHeaders(_ headers: Headers) -> Self {
// Implementation ...
}
}
You can take this a step further and add ExpressibleByArrayLiteral conformance
in addition to the dictionary literal conformance and count on enums with associated
values:
raywenderlich.com 428
Expert Swift Chapter 14: API Design Tips & Tricks
Because these both are literals, you can also instantiate them simply by providing an
explicit type when using an array or dictionary:
The options around literals are quite endless, but they’re all about making your API
surface pleasant and seamless to use while still providing a specialized experience
for the typed use case in question.
Take your time to experiment with the full list of possible literal conformances on
Apple’s documentation.
raywenderlich.com 429
Expert Swift Chapter 14: API Design Tips & Tricks
Luckily, Swift 5.1 introduced key path member lookup (SE-0252), which gives you
the same dynamic handling capabilities but for a key path to an object. This is one of
the most underrated and useful language features brought into Swift in recent years,
and it unlocks a wide range of opportunities to improve your APIs.
You might notice there’s a 1-to-1 relationship between a search bar and a text field.
For example, you might want SearchBar.isEnabled to disable the text field itself, or
SearchBar.keyboardType to change the underlying textField.
extension SearchBar {
var isEnabled: Bool {
get { textField.isEnabled }
set { textField.isEnabled = newValue }
}
But this is quite tedious, and it can also hinder maintainability and require a lot of
manual work. What if UITextField gets some new properties in the future?
raywenderlich.com 430
Expert Swift Chapter 14: API Design Tips & Tricks
@dynamicMemberLookup
class SearchBar: UIControl {
private var textField: UITextField
subscript<T>(
dynamicMember keyPath: WritableKeyPath<UITextField, T>
) -> T {
get { textField[keyPath: keyPath] }
set { textField[keyPath: keyPath] = newValue }
}
}
Once you add the @dynamicMemberLookup annotation to SearchBar, Swift will look
for both the string-based and key path-based subscripts.
In this case, a generic writable key path from UITextField to any of its properties
means you can access any property of UITextField directly from SearchBar without
more boilerplate code. For example:
This means you can wrap the typed key path in any other type to enrich the original
property with more capabilities.
@dynamicMemberLookup
struct Reactive<Base> {
// Additional implementation details...
subscript<Property>(
dynamicMember keyPath: WritableKeyPath<Base, Property>
) -> Binder<Property> where Base: AnyObject {
Binder(base) { base, value in
base[keyPath: keyPath] = value
raywenderlich.com 431
Expert Swift Chapter 14: API Design Tips & Tricks
}
}
}
This example lives under the .rx namespace in RxSwift and allows regular (as
opposed to “enriched”) access to the property:
myView.isEnabled // Bool
myView.rx.isEnabled // Binder<Bool>
Dynamic callable
Dynamic callable was introduced in Swift 5 (SE-0216) to provide syntactic sugar
when creating wrappers around dynamic languages/calls inside Swift and allows to
naturally invoke values as if they’re functions.
@dynamicCallable
struct Command {
let base: String
struct Shell {
static let swift = Command("swift")
}
So calling:
Shell.swift("--version")
Produces:
swift --version
raywenderlich.com 432
Expert Swift Chapter 14: API Design Tips & Tricks
You can use the Process API to execute the command, but it’s outside the scope of
this chapter.
You can even leverage string-based dynamic member lookup to make this a bit more
robust. Combining @dynamicMemberLookup with @dynamicCallable and adding the
following subscript to Command:
Shell.swift.build("--verbose")
Property wrappers
Property wrappers, introduced in Swift 5.1 (SE-0258), provide a way to abstract the
handling of the get/set accessor portions of properties. Some of the common built-in
ones are @Published, @State and @Binding, which you used in the Functional
Reactive Programming chapter.
When designing your APIs, property wrappers serve as a powerful tool in two ways:
abstracted reusability and capability layering.
@propertyWrapper
struct AppStorage<Value> {
var wrappedValue: Value {
get { defaults.object(forKey: key) as? Value ?? fallback }
set { defaults.setValue(newValue, forKey: key) }
raywenderlich.com 433
Expert Swift Chapter 14: API Design Tips & Tricks
This property wrapper lets you encapsulate reading and writing from UserDefaults
simply by writing:
You can make this a bit nicer by also allowing string-based RawRepresentables if
you add the following initializer to AppStorage:
init<R: RawRepresentable>(
wrappedValue fallback: Value,
_ key: R,
store: UserDefaults = .standard
) where R.RawValue == String {
self.init(wrappedValue: fallback,
key.rawValue,
store: store)
}
This lets you provide RawRepresentable keys with string values, such as enums:
If there’s no value in the provided user defaults key, the assigned value will be used
as the default one and written to the user’s defaults.
raywenderlich.com 434
Expert Swift Chapter 14: API Design Tips & Tricks
The main points to note here are how the consumer doesn’t care about
UserDefaults necessarily being used under the hood and that the property wrapper
entirely removes the unneeded repetition when accessing the value through the
wrapper’s wrappedValue property.
You can also use property wrappers to transform or limit the input of a consumer. For
example, an Assert or Clamped property wrapper:
@propertyWrapper
struct Clamped<T: Comparable> {
var wrappedValue: T {
get { storage }
set {
storage = min(max(range.lowerBound, newValue),
range.upperBound)
}
}
This lets you clamp a property into a specific range. Consider a human temperature
given in degrees Celsius:
struct Patient {
let id = UUID()
let name: String
@Clamped(35...42) var temperature = 37.5
}
p.temperature = 100
// Temperature is 42, the maximum value in the range
p.temperature = 20
// Temperature is 35, the minimum value in the range
raywenderlich.com 435
Expert Swift Chapter 14: API Design Tips & Tricks
The use cases are endless. swift-argument-parser, for example, uses it to define
arguments and their properties for command-line arguments.
@propertyWrapper
struct MyPublished<Value> {
var wrappedValue: Value {
get { storage.value }
set { storage.send(newValue) }
}
init(wrappedValue: Value) {
self.storage = CurrentValueSubject(wrappedValue)
}
}
You can then use this the same way you’d use @Published:
count // Int
$count // AnyPublisher<Int, Never>
raywenderlich.com 436
Expert Swift Chapter 14: API Design Tips & Tricks
// In a different class
@ObservedObject var viewModel = MyViewModel()
viewModel // MyViewModel
$viewModel // MyViewModel.Wrapper (which has
@dynamicMemberLookup)
viewModel.counter // Int
$viewModel.counter // Binding<Int>
All these examples are here to show that designing your APIs using fancy language
features isn’t the end goal but only means to achieve great ergonomics and overall
experience for the end-user.
But now that you have a beautiful API designed, it might be helpful to write some
documentation on how to properly use it!
raywenderlich.com 437
Expert Swift Chapter 14: API Design Tips & Tricks
Symbol documentation
Back in Objective-C days, Apple used a variation of Headerdoc for documentation.
Luckily, with Swift, you can now use (almost) full-blown markdown to write
documentation. Apple calls this Swift-flavored markdown Markup.
Starting with the basics, documentation starts with three forward-slashes (///) or
lives between the /** ... */ delimiters:
/**
This method does a different thing.
Xcode also handles and recognizes some specific metadata fields for information
such as parameters, return values and thrown errors:
raywenderlich.com 438
Expert Swift Chapter 14: API Design Tips & Tricks
As you see, the first sentence is the primary portion of the documentation, whereas
the rest (separated by an empty line) is split into the Discussion section. Also, all
metadata is presented in its own “fields” as expected.
When documenting individual properties, or even enum cases, you can use a subset
of the same metadata fields. For example, note:
raywenderlich.com 439
Expert Swift Chapter 14: API Design Tips & Tricks
return nil
}
}
And of course, as you might expect, you can embed code blocks right into your
documentation.
You can choose from three options to add a code block. The first is simply indenting
your code by four spaces:
The second option is using triple-backtick (```) before and after the code-block like
you would do in regular Markdown:
raywenderlich.com 440
Expert Swift Chapter 14: API Design Tips & Tricks
And a third, less common option, is using triple-tilde instead of backticks (~~~).
Finally, as mentioned earlier, you can use most portions of markdown in a comment:
paragraphs, ordered and unordered lists, headers, links and others. This allows you to
create quite detailed and rich documentation:
raywenderlich.com 441
Expert Swift Chapter 14: API Design Tips & Tricks
///
/// But it might have more points to discuss, such as:
///
/// * Item 1
/// * Item 2
///
/// # H1 header
/// Some descriptive text with ordered list:
/// 1. First item
/// 2. [Second link item](https://raywenderlich.com)
func myFunction(a: Int, b: String) {
// Implementation
}
raywenderlich.com 442
Expert Swift Chapter 14: API Design Tips & Tricks
Note: Some additional fields specifically available for playgrounds exist, but
they’re out of scope for this chapter. You can read more about all the available
fields on Apple’s Markup Functionality documentation (https://apple.co/
3rqAFQA).
Code markers
Aside from symbol-specific documentation, you can also divide your code using code
markers. Xcode supports three of these: MARK, TODO and FIXME:
}
}
raywenderlich.com 443
Expert Swift Chapter 14: API Design Tips & Tricks
Your code is now amazingly documented! But there are still some final parting
thoughts to share with you before you publish your code to the rest of the world.
raywenderlich.com 444
Expert Swift Chapter 14: API Design Tips & Tricks
Although this section applies more to library developers than to app developers, it’s
still a great reference because even app developers might need to understand why
library authors work a certain way.
Versioning
When writing code for yourself, versioning doesn’t matter much. But as soon as a
piece of code is bundled into some reusable dependency and consumed by other
developers, versioning becomes quite critical in ensuring consistency and
expectability.
• Major: You should bump this version component whenever you make a breaking
change to your code. It usually doesn’t matter how small that change is. As soon as
your consumer won’t be able to compile their code “as-is” due to changes in your
library, a major version bump is due.
• Patch: Whenever you fix bugs in your codebase, you should bump the patch
component of your framework.
You’d consider either a minor or patch version change safe to update because it
would not (or should not) cause any issues to an existing codebase. But you’d
consider a major version change breaking because you’d likely need to modify your
interaction with the framework after updating.
raywenderlich.com 445
Expert Swift Chapter 14: API Design Tips & Tricks
There are two more useful portions of a full-blown semantic version that are
sometimes used: the pre-release and metadata labels:
• Pre-release: Use the pre-release portion of the version to indicate the version
isn’t final yet. Some common values to use here are rc (release candidate), beta
and alpha with an associated number (e.g. rc.1, beta.3, alpha.12).
• Metadata: You can assign additional build information to the metadata portion of
the semantic version. This is seldom used, but when it is, most commonly it’s for
the build number (e.g., 4.1.7-rc.1+113245).
Deprecation
In the lifetime of every piece of code, you might need to retire some classes or
methods. First, you should almost always bump the major version of your framework
if you deprecate a piece of code because it means you’re making a breaking change to
your API contract with your consumer.
The nicest way to note this deprecation to consumers is using Swift’s @available
annotation. You can simply attach it to a specific method or even an entire class:
@available(*, deprecated)
func myOldMethod(value: Int) {
// implementation
}
raywenderlich.com 446
Expert Swift Chapter 14: API Design Tips & Tricks
Or optimally, if there is a new symbol to replace the old one, you can provide an
automatic fixit for your consumer by providing the renamed argument:
Using these statements correctly can help evolve your APIs because proper usage
enhances communication between you and your API consumers as well as defines
the developer experience (in case of an automatic fixit).
raywenderlich.com 447
Expert Swift Chapter 14: API Design Tips & Tricks
Key points
Great work on finishing this chapter!
You’ve learned quite a lot about how to think and go about creating great, expected
and modern APIs. Hopefully, you’ll keep some of this chapter’s points in mind when
designing your next APIs. But don’t forget there are probably endless opinions about
API design, and you should try to find your voice and aesthetic preference to it.
Here’s a quick recap of some of the key points you learned today:
• Knowing Swift as a language is one thing, but designing an API well is an entirely
different skill.
• Developers get a “feel” for how good an API is based on various characteristics:
how obvious, well documented and modern it is, and how much the API developer
focused on reducing the consumer’s mental load.
• You should use access levels to properly separate the implementation details
(private, fileprivate and internal) from the public-facing API portions
(public and open). You should focus on the “core” of your API and not needlessly
expose APIs to your consumer, because they might confuse the consumer when
exploring.
• Fully understanding the tools and language features Swift offers is extremely
helpful in designing APIs because they give you some creative freedom in crafting
the best possible API a developer would expect in its domain. You’ve explored
some such features and seen some usage examples: literals, dynamic member
lookup, dynamic callable, property wrappers and more. Mixing these features (like
in the case of @ObservedObject) can prove quite powerful.
• Although Swift is an easy to read language, you still should properly document
your APIs and especially their public-facing properties. You can use powerful
markup syntax as well as special metadata fields and markers to enrich the
documentation experience for consumers.
• Finally, although you can simply release your code to the world without any
“release etiquette”, it’s usually a good idea to provide proper versioning and to
deprecate code correctly to not frustrate consumers.
raywenderlich.com 448
Expert Swift Chapter 14: API Design Tips & Tricks
You can also read Swift’s API Design Guidelines (https://bit.ly/39uvYPD) document
to better understand what Apple expects you to name your APIs, along with other
useful pieces of information. As you can imagine, Apple’s expectations often align
with those of fellow developers and consumers of your APIs, so it’s a great idea to
dive into this document, which is well thought out and quite detailed.
To wrap up, congratulations on completing the last chapter of this book! The team
thanks you for reading and hopes you’ve enjoyed the various chapters, examples and
perspectives this book aims to showcase. Thank you for coming along on this ride!
raywenderlich.com 449
15 Conclusion
We hope you learned a lot about the advanced features of Swift in this book — and
had some fun in the process!
With the advanced knowledge you’ve acquired, you’re ready to write better Swift
code and improve your existing apps. If you want to read more, there are several
books to check out. For building apps in the Apple ecosystem, look at SwiftUI by
Tutorials and Combine: Asynchronous Programming with Swift. If you need to support
older platforms, you might want to check out RxSwift: Reactive Programming with
Swift. If you are interested in learning about classic data structures and algorithms,
check out: Data Structures & Algorithms in Swift. Finally, if you want to look at
another advanced framework that extends beyond the Apple ecosystem, check out:
Server-Side Swift with Vapor.
If you have any questions or comments as you continue to use Swift, please stop by
our forums at https://forums.raywenderlich.com.
Thank you again for purchasing this book. Your continued support is what makes the
tutorials, books, videos, conferences and other things we do at raywenderlich.com
possible — we truly appreciate it!
raywenderlich.com 450