Online Preview

Advanced Swift

A deep dive into Swift’s features, from low-level programming to high-level abstractions.

by Chris Eidhof, Ole Begemann, Florian Kugler, and Ben Cohen

See Buying Options

Table of Contents

Introduction

Advanced Swift is quite a bold title for a book, so perhaps we should start with what we mean by it.

When we began writing the first edition of this book, Swift was barely a year old. We did so before the beta of 2.0 was released — albeit tentatively, because we suspected the language would continue to evolve as it entered its second year. Few languages — perhaps no other language — have been adopted so rapidly by so many developers.

But that left people with unanswered questions. How do you write “idiomatic” Swift? Is there a correct way to do certain things? The standard library provided some clues, but even that has changed over time, dropping some conventions and adopting others. Since its introduction nearly five years ago, Swift has evolved at a high pace, and it has become clearer what idiomatic Swift is.

To someone coming from another language, Swift can resemble everything they like about their language of choice. Low-level bit twiddling can look very similar to (and can be as performant as) C, but without many of the undefined behavior gotchas. The lightweight trailing closure syntax of map or filter will be familiar to Rubyists. Swift generics are similar to C++ templates, but with type constraints to ensure generic functions are correct at the time of definition rather than at the time of use. The flexibility of higher-order functions and operator overloading means you can write code that’s similar in style to Haskell or F#. And the @objc and dynamic keywords allow you to use selectors and runtime dynamism in ways you would in Objective-C.

Given these resemblances, it’s tempting to adopt the idioms of other languages. Case in point: Objective-C example projects can be almost mechanically ported to Swift. The same is true for Java or C# design patterns and most functional programming patterns.

But then comes the frustration. Why can’t we use protocol extensions with associated types like interfaces in Java? Why are arrays not covariant in the way we expect? Why can’t we write “functor?” Sometimes the answer is because the part of Swift in question isn’t yet implemented. But more often, it’s either because there’s a different Swift-like way to do what you want to do, or because the Swift feature you thought was like the equivalent in some other language is, in reality, not quite what you think.

Swift is a complex language — most programming languages are. But it hides that complexity well. You can get up and running developing apps in Swift without needing to know about generics or overloading or the difference between static and dynamic dispatch. You can certainly use Swift without ever calling into a C library or writing your own collection type, but after a while, we think you’ll find it necessary to know about these things — whether to improve your code’s performance, or to make it more elegant or expressive, or just to get certain things done.

Learning more about these features is what this book is about. We intend to answer many of the “How do I do this?” or “Why does Swift behave like that?” questions we’ve seen come up again and again on various forums. Hopefully, once you’ve read our book, you’ll have gone from being aware of the basics of the language to knowing about many advanced features and having a much better understanding of how Swift works. Being familiar with the material presented is probably necessary, if not sufficient, for calling yourself an advanced Swift programmer.

Who Is This Book For?

This book targets experienced (though not necessarily expert) programmers, such as existing Apple-platform developers. It’s also for those coming from other languages such as Java or C++ who want to bring their knowledge of Swift to the same level as that of their “go-to” language. Additionally, it’s suitable for new programmers who started on Swift, have grown familiar with the basics, and are looking to take things to the next level.

The book isn’t meant to be an introduction to Swift; it assumes you’re familiar with the syntax and structure of the language. If you want some good, compact coverage of the basics of Swift, the best source is the official Apple Swift book (available on iBooks or on Apple’s website). If you’re already a confident programmer, you could try reading our book and the Apple Swift book in parallel.

This is also not a book about programming for macOS or iOS devices. Of course, since Swift is used a lot for development on Apple platforms, we’ve tried to include examples of practical use, but we hope this book will be useful for non-Apple-platform programmers as well. The vast majority of the examples in the book should run unchanged on other operating systems. The ones that don’t are either fundamentally tied to Apple’s platforms (because they use iOS frameworks or rely on the Objective-C runtime) or only require minimal changes. We can say from personal experience that Swift is a great language for writing server apps running on Linux, and the ecosystem and community have evolved over the past few years to make this a viable option.

Themes

This book is organized in such a way that each chapter covers one specific concept. There are in-depth chapters on some fundamental basic concepts like optionals and strings, along with some deeper dives into topics like C interoperability. But throughout the book, hopefully a few themes regarding Swift emerge:

Swift bridges multiple levels of abstraction. Swift is a high-level language — it allows you to write code similarly to Ruby and Python, with map and reduce, and to write your own higher-order functions easily. Swift also allows you to write fast code that compiles directly to native binaries with performance similar to code written in C.

What’s exciting to us, and what’s possibly the aspect of Swift we most admire, is that you’re able to do both these things at the same time. Mapping a closure expression over an array compiles to the same assembly code as looping over a contiguous block of memory does.

However, there are some things you need to know about to make the most of this feature. For example, it will benefit you to have a strong grasp on how structs and classes differ, or an understanding of the difference between dynamic and static method dispatch (we’ll cover topics such as these in more depth later on). And if you ever need to drop to a lower level of abstraction and manipulate pointers directly, Swift lets you to do this as well.

Swift is a multi-paradigm language. You can use it to write object-oriented code or pure functional code using immutable values, or you can write imperative C-like code using pointer arithmetic.

This is both a blessing and a curse. It’s great in that you have a lot of tools available to you, and you aren’t forced into writing code one way. But it also exposes you to the risk of writing Java or C or Objective-C in Swift.

Swift still has access to most of the capabilities of Objective-C, including message sending, runtime type identification, and key-value observation. But Swift introduces many capabilities not available in Objective-C.

Erik Meijer, a well-known programming language expert, tweeted the following in October 2015:

At this point, @SwiftLang is probably a better, and more valuable, vehicle for learning functional programming than Haskell.

Swift is a good introduction to a more functional style of programming through its use of generics, protocols, value types, and closures. It’s even possible to use it to write operators that compose functions together. That said, most people in the Swift community seem to prefer a more imperative style while incorporating patterns that originated in functional programming. Swift’s notion of mutability for value types, as well as its error handling model, are examples of the language “hiding” functional concepts behind a friendly imperative syntax.

Swift is very flexible. In the introduction to the book On Lisp, Paul Graham writes that:

Experienced Lisp programmers divide up their programs differently. As well as top-down design, they follow a principle which could be called bottom-up design—changing the language to suit the problem. In Lisp, you don’t just write your program down toward the language, you also build the language up toward your program. As you’re writing a program you may think “I wish Lisp had such-and-such an operator.” So you go and write it. Afterward you realize that using the new operator would simplify the design of another part of the program, and so on. Language and program evolve together.

Swift is very different from Lisp. But still, we feel like Swift also has this characteristic of encouraging “bottom-up” programming — of making it easy to write very general reusable building blocks that you then combine into larger features, which you then use to solve your actual problem. Swift is particularly good at making these building blocks feel like primitives — like part of the language. A good demonstration of this is that the many features you might think of as fundamental building blocks, like optionals or basic operators, are actually defined in a library — the Swift standard library — rather than directly in the language. Trailing closures enable you to extend the language with features that feel like they’re built in.

Swift code can be compact and concise while still being clear. Swift lends itself to relatively terse code. There’s an underlying goal here, and it isn’t to save on typing. The idea is to get to the point quicker and to make code readable by dropping a lot of the “ceremonial” boilerplate you often see in other languages that obscures rather than clarifies the meaning of the code.

For example, type inference removes the clutter of type declarations that are obvious from the context. Semicolons and parentheses that add little or no value are gone. Generics and protocol extensions encourage you to avoid repeating yourself by packaging common operations into reusable functions. The goal is to write code that’s readable at a glance.

At first, this can be off-putting. If you’ve never before used functions like map, filter, and reduce, they might come across as more difficult to read than a simple for loop. But our hope is that this is a short learning curve and that the reward is code that is more “obviously correct” at first glance.

Swift tries to be as safe as is practical, until you tell it not to be. This is unlike languages such as C and C++ (where you can be unsafe easily just by forgetting to do something), or like Haskell or Java (which are sometimes safe whether or not you like it).

Eric Lippert, one of the principal designers of C#, wrote about his 10 regrets of C#, including the lesson that:

sometimes you need to implement features that are only for experts who are building infrastructure; those features should be clearly marked as dangerous—not invitingly similar to features from other languages.

Eric was specifically referring to C#’s finalizers, which are similar to C++ destructors. But unlike destructors, they run at a nondeterministic time (perhaps never) at the behest of the garbage collector (and on the garbage collector’s thread). However, Swift, being reference counted, does execute a class’s deinit deterministically.

Swift embodies this sentiment in other ways. Undefined and unsafe behavior is avoided by default. For example, a variable can’t be used until it’s been initialized, and using out-of-bounds subscripts on an array will trap, as opposed to continuing with possibly garbage values.

There are a number of “unsafe” options available (such as the unsafeBitCast function, or the UnsafeMutablePointer type) for when you really need them. But with great power comes great undefined behavior. For example, you can write the following:

var someArray = [1, 2, 3]
let uhOh = someArray.withUnsafeBufferPointer { ptr in
    return ptr
}
// Later...
print(uhOh[10])

It’ll compile, but who knows what it’ll do. The ptr variable is only valid within the closure expression, and returning it to the caller is illegal. But there is nothing stopping you from letting it escape into the wild. However, you can’t say nobody warned you.

Swift is an opinionated language. We as authors have strong opinions about the “right” way to write Swift. You’ll see many of them in this book, sometimes expressed as if they’re facts. But they’re just our opinions — feel free to disagree! Swift is still a young language, and many things aren’t settled. Regardless of what you’re reading, the most important thing is to try things out for yourself, check how they behave, and decide how you feel about them. Think critically, and beware of out-of-date information.

Swift continues to evolve. The period of major yearly syntax changes is behind us, but important areas of the language are still unfinished (string APIs, the generics system), in flux (reflection), or haven’t been tackled yet (concurrency).

Terminology

‘When I use a word,’ Humpty Dumpty said, in rather a scornful tone, ‘it means just what I choose it to mean — neither more nor less.’

Through the Looking Glass, by Lewis Carroll

Programmers throw around terms of art a lot. To avoid confusion, what follows are some definitions of terms we use throughout this book. Where possible, we’re trying to adhere to the same usage as the official documentation, or sometimes a definition that’s been widely adopted by the Swift community. Many of these definitions are covered in more detail in later chapters, so don’t worry if not everything makes sense right away. If you’re already familiar with all of these terms, it’s still best to skim through to make sure your accepted meanings don’t differ from ours.

In Swift, we make the distinction between values, variables, references, and constants.

A value is immutable and forever — it never changes. For example, 1, true, and [1,2,3] are all values. These are examples of literals, but values can also be generated at runtime. The number you get when you square the number five is a value.

When we assign a value to a name using var x = [1,2], we’re creating a variable named x that holds the value [1,2]. By changing x, e.g. by performing x.append(3), we didn’t change the original value. Rather, we replaced the value that x holds with the new value, [1,2,3] — at least logically, if not in the actual implementation (which might actually just tack a new entry on the back of some existing memory). We refer to this as mutating the variable.

We can declare constant variables (constants, for short) with let instead of var. Once a constant has been assigned a value, it can never be assigned a new value.

We also don’t need to give a variable a value immediately. We can declare the variable first (let x: Int) and then later assign a value to it (x = 1). Swift, with its emphasis on safety, will check that all possible code paths lead to a variable being assigned a value before its value can be read. There’s no concept of a variable having an as-yet-undefined value. Of course, if the variable was declared with let, it can only be assigned to once.

Structs and enums are value types. When you assign one struct variable to another, the two variables will then contain the same value. You can think of the contents as being copied, but it’s more accurate to say that one variable was changed to contain the same value as the other.

A reference is a special kind of value: it’s a value that “points to” some other location in memory. Because two references can refer to the same location, this introduces the possibility of the value stored at that location getting mutated by two different parts of the program at once.

Classes are reference types. You can’t hold an instance of a class (which we might occasionally call an object — a term fraught with troublesome overloading!) directly in a variable. Instead, you must hold a reference to it in a variable and access it via that reference.

Reference types have identity — you can check if two variables are referring to the exact same object by using ===. You can also check if they’re equal, assuming == is implemented for the relevant type. Two objects with different identities can still be equal.

Value types don’t have identity. You can’t check if a particular variable holds the “same” number 2 as another. You can only check if they both contain the value 2. === is really asking: “Do both these variables hold the same reference as their value?” In programming language literature, == is sometimes called structural equality, and === is called pointer equality or reference equality.

Class references aren’t the only kind of reference in Swift. For example, there are also pointers, accessed through withUnsafeMutablePointer functions and the like. But classes are the simplest reference type to use, in part because their reference-like nature is partially hidden from you by syntactic sugar, meaning you don’t need to do any explicit “dereferencing” like you do with pointers in some other languages. (We’ll cover the other kind of references in more detail in the Interoperability chapter.)

A variable that holds a reference can be declared with let — that is, the reference is constant. This means that the variable can never be changed to refer to something else. But — and this is important — it doesn’t mean that the object it refers to can’t be changed. So when referring to a variable as a constant, be careful — it’s only constant in what it points to. It doesn’t mean what it points to is constant. (Note: If those last few sentences sound like doublespeak, don’t worry, as we cover this again in the Structs and Classes chapter.) Unfortunately, this means that when looking at a declaration of a variable with let, you can’t tell at a glance whether or not what’s being declared is completely immutable. Instead, you have to know whether it’s holding a value type or a reference type.

When a value type is copied, it generally performs a deep copy, i.e. all values it contains are also copied recursively. This copy can occur eagerly (whenever a new variable is introduced) or lazily (whenever a variable gets mutated). Types that perform deep copies are said to have value semantics.

Here we hit another complication. If a struct contains reference types, the referenced objects won’t automatically get copied upon assigning the struct to a new variable. Instead, only the references themselves get copied. These are called shallow copies.

For example, the Data struct in Foundation is a wrapper around a class that stores the actual bytes. However, the authors of the Data struct took extra steps to also perform a deep copy of the class instance whenever the Data struct is mutated. They do this efficiently using a technique called copy-on-write, which we’ll explain in the Structs and Classes chapter. For now, it’s important to know that this behavior doesn’t come for free.

The collections in the standard library also wrap reference types and use copy-on-write to efficiently provide value semantics. However, if the elements in a collection are references (for example, an array containing objects), the objects won’t get copied. Instead, only the references get copied. This means that a Swift array only has value semantics if its elements have value semantics too.

Some classes are completely immutable — that is, they provide no methods for changing their internal state after they’re created. This means that even though they’re classes, they also have value semantics (because even if they’re shared, they can never change). Be careful though — only final classes can be guaranteed not to be subclassed with added mutable state.

In Swift, functions are also values. You can assign a function to a variable, have an array of functions, and call the function held in a variable. Functions that take other functions as arguments (such as map, which takes a function to transform every element of a sequence) or return functions are referred to as higher-order functions.

Functions don’t have to be declared at the top level — you can declare a function within another function or in a do or other scope. Functions defined within an outer scope but passed out from it (say, as the returned value of a function), can “capture” local variables, in which case those local variables aren’t destroyed when the local scope ends, and the function can hold state through them. This behavior is called “closing over” variables, and functions that do this are called closures.

Functions can be declared either with the func keyword or by using a shorthand { } syntax called a closure expression. Sometimes this gets shortened to “closures,” but don’t let it give you the impression that only closure expressions can be closures. Functions declared with the func keyword are also closures when they close over external variables.

Functions are held by reference. This means assigning a function that has captured state to another variable doesn’t copy that state; it shares it, similar to object references. What’s more is that when two closures close over the same local variable, they both share that variable, so they share state. This can be quite surprising, and we’ll discuss this more in the Functions chapter.

Functions defined inside a class or protocol are methods, and they have an implicit self parameter. Sometimes we call functions that aren’t methods free functions. This is to distinguish them from methods.

A fully qualified function name in Swift includes not just the function’s base name (the part before the parentheses), but also the argument labels. For example, the full name of the method for moving a collection index by a number of steps is index(_:offsetBy:), indicating that this function takes two arguments (represented by the two colons), the first one of which has no label (represented by the underscore). We often omit the labels in the book if it’s clear from the context what function we’re referring to; the compiler allows you to do the same.

Free functions, and methods called on structs, are statically dispatched. This means the function that’ll be called is known at compile time. It also means the compiler might be able to inline the function, i.e. not call the function at all, but instead replace it with the code the function would execute. The optimizer can also discard or simplify code that it can prove at compile time won’t actually run.

Methods on classes or protocols might be dynamically dispatched. This means the compiler doesn’t necessarily know at compile time which function will run. This dynamic behavior is done either by using vtables (similar to how Java and C++ dynamic dispatch work), or in the case of some @objc classes and protocols, by using selectors and objc_msgSend.

Subtyping and method overriding is one way of getting polymorphic behavior, i.e. behavior that varies depending on the types involved. A second way is function overloading, where a function is written multiple times for different types. (It’s important not to mix up overriding and overloading, as they behave very differently.) A third way is via generics, where a function or method is written once to take any type that provides certain functions or methods, but the implementations of those functions can vary. Unlike method overriding, the results of function overloading and generics are known statically at compile time. We’ll cover this more in the Generics chapter.

Swift Style Guide

When writing this book, and when writing Swift code for our own projects, we try to stick to the following rules:

  • For naming, clarity at the point of use is the most important consideration. Since APIs are used many more times than they’re declared, their names should be optimized for how well they work at the call site. Familiarize yourself with the Swift API Design Guidelines and try to adhere to them in your own code.

  • Clarity is often helped by conciseness, but brevity should never be a goal in and of itself.

  • Always add documentation comments to functions — especially generic ones.

  • Types start with UpperCaseLetters. Functions, variables, and enum cases start with lowerCaseLetters.

  • Use type inference. Explicit but obvious types get in the way of readability.

  • Don’t use type inference in cases of ambiguity or when defining contracts (which is why, for example, funcs have an explicit return type).

  • Default to structs unless you actually need a class-only feature or reference semantics.

  • Mark classes as final unless you’ve explicitly designed them to be inheritable. If you want to use inheritance internally but not allow subclassing for external clients, mark a class public but not open.

  • Use the trailing closure syntax, except when that closure is immediately followed by another opening brace.

  • Use guard to exit functions early.

  • Eschew force-unwraps and implicitly unwrapped optionals. They’re occasionally useful, but needing them constantly is usually a sign something is wrong.

  • Don’t repeat yourself. If you find you’ve written a very similar piece of code more than a couple of times, extract it into a function. Consider making that function a protocol extension.

  • Favor map and filter. But don’t force it: use a for loop when it makes sense. The purpose of higher-order functions is to make code more readable. An obfuscated use of reduce when a simple for loop would be clearer defeats this purpose.

  • Favor immutable variables: default to let unless you know you need mutation. But use mutation when it makes the code clearer or more efficient. Again, don’t force it: a mutating method on a struct is often more idiomatic and efficient than returning a brand new struct.

  • Swift generics (especially in combination with protocol constraints) tend to lead to very long function signatures. Unfortunately, we have yet to settle on a good way of breaking up long function declarations into multiple lines. We’ll try to be consistent in how we do this in sample code.

  • Leave off self. when you don’t need it. In closure expressions, the presence of self. is a clear signal that self is being captured by the closure.

  • Instead of writing a free function, write an extension on a type or protocol (whenever you can). This helps with readability and discoverability.

One final note about our code samples throughout the book: to save space and focus on the essentials, we usually omit import statements that would be required to make the code compile. If you try out the code yourself and the compiler tells you it doesn’t recognize a particular symbol, try adding an import Foundation or import UIKit statement.

Revision History

Fourth Edition (May 2019)

Third Edition (October 2017)

Second Edition (September 2016)

First Edition (March 2016)

  • Initial release (covering Swift 2.2).

Built-In Collections

Collections of elements are among the most important data types in any programming language. Good language support for different kinds of containers has a big impact on programmer productivity and happiness. Swift places special emphasis on sequences and collections — so much of the standard library is dedicated to this topic that we sometimes have the feeling it deals with little else. The resulting model is more extensible than what you may be used to from other languages, but it’s also quite complex.

In this chapter, we’re going to take a look at the major collection types Swift ships with, with a focus on how to work with them effectively and idiomatically. In the Collection Protocols chapter later in the book, we’ll climb up the abstraction ladder and see how the collection protocols in the standard library work.

Arrays

Arrays and Mutability

Arrays are the most common collections in Swift. An array is an ordered container of elements that all have the same type, and it provides random access to each element. As an example, to create an array of numbers, we can write the following:

// The Fibonacci numbers
let fibs = [0, 1, 1, 2, 3, 5]

If we try to modify the array defined above (by using append(_:), for example), we get a compile error. This is because the array is defined as a constant, using let. In many cases, this is exactly the correct thing to do; it prevents us from accidentally changing the array. If we want the array to be a variable, we have to define it using var:

var mutableFibs = [0, 1, 1, 2, 3, 5]

Now we can easily append a single element or a sequence of elements:

mutableFibs.append(8)
mutableFibs.append(contentsOf: [13, 21])
mutableFibs // [0, 1, 1, 2, 3, 5, 8, 13, 21]

There are a couple of benefits that come with making the distinction between var and let. Constants defined with let are easier to reason about because they’re immutable. When you read a declaration like let fibs = ..., you know that the value of fibs will never change — the immutability is enforced by the compiler. This helps greatly when reading through code. However, note that this is only true for types that have value semantics. A let variable containing a reference to a class instance guarantees that the reference will never change, i.e. you can’t assign another object to that variable. However, the object the reference points to can change. We’ll go into more detail on these differences in the chapter on Structs and Classes.

Arrays, like all collection types in the standard library, have value semantics. When you assign an existing array to another variable, the array contents are copied over. For example, in the following code snippet, x is never modified:

var x = [1,2,3]
var y = x
y.append(4)
y // [1, 2, 3, 4]
x // [1, 2, 3]

The statement var y = x makes a copy of x, so appending 4 to y won’t change x — the value of x will still be [1, 2, 3]. The same thing happens when you pass an array into a function; the function receives a local copy of the array, and any changes it makes don’t affect the caller.

Compare this with the approach to mutability taken by NSArray in Foundation. NSArray has no mutating methods — to mutate an array, you need an NSMutableArray. But just because you have a non-mutating NSArray reference does not mean the array can’t be mutated underneath you:

let a = NSMutableArray(array: [1,2,3])
// I don't want to be able to mutate b.
let b: NSArray = a
// But it can still be mutated — via a.
a.insert(4, at: 3)
b // ( 1, 2, 3, 4 )

The correct way to write this is to manually create a copy upon assignment:

let c = NSMutableArray(array: [1,2,3])
// I don't want to be able to mutate d.
let d = c.copy() as! NSArray
c.insert(4, at: 3)
d // ( 1, 2, 3 )

In the example above, it’s very clear that we need to make a copy — a is mutable, after all. However, when passing around arrays between methods and functions, this isn’t always so easy to see.

In Swift, there’s just one array type, and mutability is controlled by declaring with var instead of let. But there’s no reference sharing — when you declare a second array with let, you’re guaranteed it’ll never change.

Making so many copies could be a performance problem, but in practice, all collection types in the Swift standard library are implemented using a technique called copy-on-write, which makes sure the data is only copied when necessary. So in our example, x and y shared internal storage up until the point y.append was called. In the chapter on Structs and Classes, we’ll take a deeper look at value semantics, including how to implement copy-on-write for your own types:

Array Indexing

Swift arrays provide all the usual operations you’d expect, such as isEmpty and count. Arrays also allow for direct access of elements at a specific index through subscripting, like with fibs[3]. Keep in mind that you need to make sure the index is within bounds before getting an element via subscript. Fetch the element at index 3, and you’d better be sure the array has at least four elements in it. Otherwise, your program will trap, i.e. abort with a fatal error.

Swift has many ways to work with arrays without you ever needing to calculate an index:

  • Want to iterate over the array?
    for x in array

  • Want to iterate over all but the first element of an array?
    for x in array.dropFirst()

  • Want to iterate over all but the last five elements?
    for x in array.dropLast(5)

  • Want to number all the elements in an array?
    for (num, element) in array.enumerated()

  • Want to find the location of a specific element?
    if let idx = array.firstIndex { someMatchingLogic($0) }

  • Want to transform all the elements in an array?
    array.map { someTransformation($0) }

  • Want to fetch only the elements matching a specific criterion?
    array.filter { someCriteria($0) }

Another sign that Swift wants to discourage you from doing index math is the removal of traditional C-style for loops from the language in Swift 3. Manually fiddling with indices is a rich sea of bugs to mine, so it’s often best avoided.

But sometimes you do have to use an index. And with array indices, the expectation is that when you do, you’ll have thought very carefully about the logic behind the index calculation. So to have to unwrap the value of a subscript operation is probably overkill — it means you don’t trust your code. But chances are you do trust your code, so you’ll probably resort to force-unwrapping the result, because you know the index must be valid. This is (a) annoying, and (b) a bad habit to get into. When force-unwrapping becomes routine, eventually you’re going to slip up and force-unwrap something you don’t mean to. So to avoid this habit becoming routine, arrays don’t give you the option.

While a subscripting operation that responds to an invalid index with a controlled crash could arguably be called unsafe, that’s only one aspect of safety. Subscripting is totally safe in regard to memory safety — the standard library collections always perform bounds checks to prevent unauthorized memory access with an out-of-bounds index.

Other operations behave differently. The first and last properties return an optional value, which is nil if the array is empty. first is equivalent to isEmpty ? nil : self[0]. Similarly, the removeLast method will trap if you call it on an empty array, whereas popLast will only delete and return the last element if the array isn’t empty, and otherwise it will do nothing and return nil. Which one you’d want to use depends on your use case. When you’re using the array as a stack, you’ll probably always want to combine checking for empty and removing the last entry. On the other hand, if you already know whether or not the array is empty, dealing with the optional is fiddly.

We’ll encounter these tradeoffs again later in this chapter when we talk about dictionaries. Additionally, there’s an entire chapter dedicated to Optionals.

Dictionaries

Sets

Ranges

Recap

Optionals

Sentinel Values

An extremely common pattern in programming is to have an operation that may or may not return a value.

Perhaps not returning a value is an expected outcome when you’ve reached the end of a file you were reading, as in the following C snippet:

int ch;
while ((ch = getchar()) != EOF) {
    printf("Read character %c\n", ch);
}
printf("Reached end-of-file\n");

EOF is just a #define for -1. As long as there are more characters in the file, getchar returns them. But if the end of the file is reached, getchar returns -1.

Or perhaps returning no value means “not found,” as in this bit of C++:

auto vec = {1, 2, 3};
auto iterator = std::find(vec.begin(), vec.end(), someValue);
if (iterator != vec.end()) {
    std::cout << "vec contains " << *iterator << std::endl;
}

Here, vec.end() is the iterator “one past the end” of the container; it’s a special iterator you can check against the container’s end but that you mustn’t ever actually use to access a value — similar to a collection’s endIndex in Swift. The find function uses it to indicate that no such value is present in the container.

Or maybe the value can’t be returned because something went wrong during the function’s processing. Probably the most notorious example is that of the null pointer. This innocuous-looking piece of Java code will likely throw a NullPointerException:

int i = Integer.getInteger("123")

It happens that Integer.getInteger doesn’t parse strings into integers, but rather gets the integer value of a system property named “123.” This property probably doesn’t exist, in which case getInteger returns null. When the null then gets unboxed into an int, Java throws an exception.

Or take this example in Objective-C:

[[NSString alloc] initWithContentsOfURL:url 
    encoding:NSUTF8StringEncoding error:&error];

This initializer might return nil, in which case — and only then — the error pointer should be checked. There’s no guarantee the error pointer is valid if the initializer returns non-nil.

In all of the above examples, the function returns a special “magic” value to indicate that it hasn’t returned a real value. Magic values like these are called “sentinel values.”

But this approach is problematic. The result returned looks and feels like a real value. An int of -1 is still a valid integer, but you don’t ever want to print it out. vec.end() is an iterator, but the results are undefined if you try to use it. And everyone loves seeing a stack dump when your Java program throws a NullPointerException.

Unlike Java, Objective-C allows sending messages to nil. This is “safe” insofar as the runtime guarantees that the return value from a message to nil will always be the equivalent of zero, i.e. nil for object return types, 0 for numeric types, and so on. If the message returns a struct, it’ll have all its members initialized to zero. With this in mind, consider the following snippet for finding a substring:

NSString *someString = ...
if ([someString rangeOfString:@"Swift"].location != NSNotFound) {
    NSLog(@"Someone mentioned Swift!");
}

If someString is nil, whether accidentally or on purpose, the rangeOfString: message will return a zeroed NSRange. Hence, its .location will be zero, and the inequality comparison against NSNotFound (which is defined as NSIntegerMax) will succeed. Therefore, the body of the if statement will be executed when it shouldn’t be.

Null references cause so many problems that Tony Hoare, credited with their creation in 1965, calls them his “billion-dollar mistake”:

At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Another problem with sentinel values is that they require prior knowledge. Sometimes there’s an idiom that’s widely followed in a community, as with the C++ end iterator or the error handling conventions in Objective-C. If such rules don’t exist or you’re not aware of them, you have to refer to the documentation. Moreover, there’s no way for the function to indicate it can’t fail. If a call returns a pointer, that pointer might never be nil. But there’s no way to tell except by reading the documentation, and even then, the documentation might be wrong.

Replacing Sentinel Values with Enums

A Tour of Optional Techniques

When to Force-Unwrap

Implicitly Unwrapped Optionals

Recap

Functions

Overview

To open this chapter, let’s recap some main points regarding functions. If you’re already familiar with first-class functions, feel free to skip ahead to the next section. But if you’re even slightly unsure about them, read through what’s below.

To understand functions and closures in Swift, you really need to understand three things, in roughly this order of importance:

  1. Functions can be assigned to variables and passed in and out of other functions as arguments, just as an Int or a String can be.

  2. Functions can capture variables that exist outside of their local scope.

  3. There are two ways of creating functions — either with the func keyword, or with { }. Swift calls the latter closure expressions.

Sometimes people new to the topic of closures come at it in reverse order and maybe miss one of these points, or they conflate the terms closure and closure expression — and this can cause a lot of confusion. It’s a three-legged stool, and if you miss one of the three points above, you’ll fall over when you try to sit down.

1. Functions can be assigned to variables and passed in and out of other functions as arguments.

In Swift, as in many modern languages, functions are referred to as “first-class objects.” You can assign functions to variables, and you can pass them in and out of other functions to be called later.

This is the most important thing to understand. “Getting” this for functional programming is akin to “getting” pointers in C. If you don’t quite grasp this part, everything else will just be noise.

Let’s start with a function that simply prints an integer:

func printInt(i: Int) {
    print("You passed \(i).")
}

To assign the function to a variable, funVar, we just use the function name as the value. Note the absence of parentheses after the function name:

let funVar = printInt

Now we can call the printInt function using the funVar variable. Note the use of parentheses after the variable name:

funVar(2) // You passed 2.

It’s also noteworthy that we must not include an argument label in the funVar call, whereas printInt calls require the argument label, as in printInt(i: 2). Swift only allows argument labels in function declarations; the labels aren’t included in a function’s type. This means that you currently can’t assign argument labels to a variable of a function type, though this will likely change in a future Swift version.

We can also write a function that takes a function as an argument:

func useFunction(function: (Int) -> () ) {
    function(3)
}

useFunction(function: printInt) // You passed 3.
useFunction(function: funVar) // You passed 3.

Why is being able to treat functions like this such a big deal? Because it allows you to easily write “higher-order” functions, which take functions as arguments and apply them in useful ways, as we saw in the chapter on Built-In Collections.

Functions can also return other functions:

func returnFunc() -> (Int) -> String {
    func innerFunc(i: Int) -> String {
        return "You passed \(i)."
    }
    return innerFunc
}
let myFunc = returnFunc()
myFunc(3) // You passed 3.

2. Functions can capture variables that exist outside of their local scope.

When a function references variables outside its scope, those variables are captured and stick around after they would otherwise fall out of scope and be destroyed.

To see this, let’s revisit our returnFunc function but add a counter that increases each time we call it:

func counterFunc() -> (Int) -> String {
    var counter = 0
    func innerFunc(i: Int) -> String {
        counter += i   // counter is captured
        return "Running total: \(counter)"
    }
    return innerFunc
}

Normally counter, being a local variable of counterFunc, would go out of scope just after the return statement, and it’d be destroyed. Instead, because it’s captured by innerFunc, the Swift runtime will keep it alive until the function that captured it gets destroyed. We can call the inner function multiple times, and we see that the running total increases:

let f = counterFunc()
f(3) // Running total: 3
f(4) // Running total: 7

If we call counterFunc() again, a fresh counter variable will be created and captured:

let g = counterFunc()
g(2) // Running total: 2
g(2) // Running total: 4

This doesn’t affect our first function, which still has its own captured version of counter:

f(2) // Running total: 9

Think of these functions combined with their captured variables as similar to instances of classes with a single method (the function) and some member variables (the captured variables).

In programming terminology, a combination of a function and an environment of captured variables is called a closure. So f and g above are examples of closures, because they capture and use a non-local variable (counter) that was declared outside of them.

3. Functions can be declared using the { } syntax for closure expressions.

In Swift, you can define functions in two ways. One is with the func keyword. The other way is to use a closure expression. Consider this simple function to double a number:

func doubler(i: Int) -> Int { 
    return i * 2 
}
[1, 2, 3, 4].map(doubler) // [2, 4, 6, 8]

And here’s the same function written using the closure expression syntax. Just like before, we can pass it to map:

let doublerAlt = { (i: Int) -> Int in return i*2 }
[1, 2, 3, 4].map(doublerAlt) // [2, 4, 6, 8]

Functions declared as closure expressions can be thought of as function literals in the same way that 1 and "hello" are integer and string literals. They’re also anonymous — they aren’t named, unlike with the func keyword. The only way they can be used is to assign them to a variable when they’re created (as we do here with doubler), or to pass them to another function or method.

There’s a third way anonymous functions can be used: you can call a function directly in line as part of the same expression that defines it. This can be useful for defining properties whose initialization requires more than one line. We’ll see an example of this below in the section on lazy properties.

The doubler declared using the closure expression and the one declared earlier using the func keyword are completely equivalent, apart from the differences in their handling of argument labels that we mentioned above. They even exist in the same “namespace,” unlike in some languages.

Why is the { } syntax useful then? Why not just use func every time? Well, it can be a lot more compact, especially when writing quick functions to pass into other functions, such as map. Here’s our doubler map example written in a much shorter form:

[1, 2, 3].map { $0 * 2 } // [2, 4, 6]

This looks very different because we’ve leveraged several features of Swift to make the code more concise. Here they are, one by one:

  1. If you’re passing the closure in as an argument and that’s all you need it for, there’s no need to store it in a local variable first. Think of this like passing in a numeric expression, such as 5*i, to a function that takes an Int as a parameter.

  2. If the compiler can infer a type from the context, you don’t need to specify it. In our example, the function passed to map takes an Int (inferred from the type of the array elements) and returns an Int (inferred from the type of the multiplication expression).

  3. If the closure expression’s body contains just a single expression, it’ll automatically return the value of the expression, and you can leave off the return.

  4. Swift automatically provides shorthand names for the arguments to the function — $0 for the first, $1 for the second, etc.

  5. If the last argument to a function is a closure expression, you can move the expression outside the parentheses of the function call. This trailing closure syntax is nice if you have a multi-line closure expression, as it more closely resembles a regular function definition or other block statement, such as if expr { }.

  6. Finally, if a function has no arguments other than a closure expression, you can leave off the parentheses after the function name altogether.

Using each of these rules, we can boil down the expression below to the form shown above:

[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } )
[1, 2, 3].map( { i in return i * 2 } )
[1, 2, 3].map( { i in i * 2 } )
[1, 2, 3].map( { $0 * 2 } )
[1, 2, 3].map() { $0 * 2 }
[1, 2, 3].map { $0 * 2 }

If you’re new to Swift’s syntax, and to functional programming in general, these compact function declarations might seem daunting at first. But as you get more comfortable with the syntax and the functional programming style, they’ll start to feel more natural, and you’ll appreciate the ability to remove the clutter so you can see more clearly what the code is doing. Once you get used to reading code written like this, it’ll be even easier to parse at a glance than the equivalent code written with a conventional for loop.

Sometimes, Swift needs a helping hand with inferring types. And sometimes, you may get something wrong and the types aren’t what you think they should be. If ever you get a mysterious error when trying to supply a closure expression, it’s a good idea to write out the full form (first version above), complete with types. In many cases, that will help clear up where things are going wrong. Once you have the long form compiling, take the types out again one by one until the compiler complains. And if the error was yours, you’ll have fixed your code in the process.

Swift will also insist you be more explicit sometimes. For example, you can’t completely ignore input parameters. Suppose you wanted an array of random numbers. A quick way to do this is to map a range with a function that just generates random numbers. Nonetheless, you must supply an argument. You can use _ in such a case to indicate to the compiler that you acknowledge there’s an argument but that you don’t care what it is:

(0..<3).map { _ in Int.random(in: 1..<100) } // [75, 74, 43]

When you need to explicitly type the variables, you don’t have to do it inside the closure expression. For example, try defining isEven without any types:

let isEven = { $0 % 2 == 0 }

Above, the type of isEven is inferred to be (Int) -> Bool in the same way that let i = 1 is inferred to be Int — because Int is the default type for integer literals.

This is because of a type alias, IntegerLiteralType, in the standard library:

protocol ExpressibleByIntegerLiteral {
    associatedtype IntegerLiteralType
    /// Create an instance initialized to `value`.
    init(integerLiteral value: IntegerLiteralType)
}

/// The default type for an otherwise-unconstrained integer literal.
typealias IntegerLiteralType = Int

If you were to define your own type alias, it would override the default one and change this behavior:

typealias IntegerLiteralType = UInt32
let i = 1  // i will be of type UInt32.

This is almost certainly a bad idea.

If, however, you need a version of isEven for a different type, you could type the argument and the return value inside the closure expression:

let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }

But you could also supply the context from outside the closure:

let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool

Since closure expressions are most commonly used in some context of existing input or output types, adding an explicit type isn’t often necessary, but it’s useful to know you can do this.

Flexibility through Functions

Functions as Delegates

inout Parameters and Mutating Methods

Properties

Subscripts

Key Paths

Autoclosures

The @escaping Annotation

Recap

Structs and Classes

When designing our data types, Swift lets us choose between two alternatives that seem to be similar on the surface: structs and classes. Both can have stored and computed properties, and both can have methods defined on them. Furthermore, both have initializers, we can define extensions on them, and we can conform them to protocols. Sometimes our code even keeps compiling when we change the class keyword into struct or vice versa. However, the similarities on the surface are deceptive, as structs and classes have fundamentally different behaviors.

Structs are value types, whereas classes are reference types. Even if we don’t think in these terms, we’re all familiar with the behavior of values and references in our daily work. We’ll try to leverage this implicit understanding in the next section to shine some light on the formal distinction between value types and reference types in general, and structs and classes specifically.

Value Types and Reference Types

Let’s start by looking at one of the simplest types possible: integers. Consider the following code:

var a: Int = 3
var b = a
b += 1

What’s the value of a now? It’s probably safe to say that we all expect a to still hold the value 3, even though we’ve incremented b to 4. Anything else would be a big surprise. And this is indeed correct:

a // 3
b // 4

This behavior is the essence of value types: assignment copies the value. In other words, each value type variable holds its own independent value. If a type behaves that way, it is also said to have value semantics.

Looking at the definition of Int in the standard library, we can indeed see that it is a struct (and therefore has value semantics):

public struct Int: FixedWidthInteger, SignedInteger {
    ...
}

Before we proceed, let’s take a step back and look at this behavior from a more low-level perspective.

What do we mean by the term “variable?” We can say that a variable is a name for a location in memory that contains a value of a certain type. In the example above, we use the name a to refer to a location in memory of type Int currently holding the value 3. The second variable, b, is a name for a different location in memory, equally of type Int and containing the value 3 after the initial assignment. The statement b += 1 then takes the value stored in the memory location referred to as b, increments it by one, and writes it back to the same location in memory. Thus, b now contains the value 4. Since the increment statement only modifies the value of the b variable, a is unaffected by this statement.

A value type variable is a name for a location in memory that directly contains the value.
A value type variable is a name for a location in memory that directly contains the value.

Value types are characterized by this direct relationship between variable and value: the value (also referred to as the instance of the value type) resides directly at the location in memory behind the variable. This applies to simple value types like integers, but also to more complex types like custom structs with multiple properties (on a machine code level this might not hold true due to compiler optimizations, but these are opaque to the developer so that our description is at least semantically accurate).

Mutation

Lifecycle

Deciding between Structs and Classes

Classes with Value Semantics

Structs with Reference Semantics

Copy-On-Write Optimization

Recap

Enums

Structs and classes, which we discussed in the previous chapter, are examples of record types. A record is composed of zero or more fields (properties), with each field having its own type. Tuples also fall into this category: a tuple is effectively a lightweight anonymous struct with fewer capabilities. Records are such an obvious concept that we take them for granted. Almost all programming languages allow you to define composite types of this kind (early versions of BASIC and the original Lisp are perhaps the best-known exceptions). Even assembly programmers have always used the concept of records to structure data in memory, albeit without language support.

Swift’s enumerations, or enums, belong to a fundamentally different category that’s sometimes called tagged unions or variant types. Despite being a concept equally as powerful as records, support for variants is much less widespread in mainstream programming languages. They are commonplace in functional languages, though, and have become popular in newer languages such as Rust. In our opinion, enums are one of Swift’s best features.

Overview

An enum consists of zero or more cases, with each case having an optional tuple-style list of associated values. In this chapter, we’ll sometimes use the singular term “associated value” when we talk about a single case’s associated value(s). A case can have multiple associated values, but you can think of them as a single tuple.

Here’s a simple enum without associated values for representing the alignment of a paragraph:

enum TextAlignment {
    case left
    case center
    case right
}

We saw in the Optionals chapter that Optional is a generic enum with two cases, none and some. The some case has an associated value for the boxed value:

@_frozen enum Optional<Wrapped> {
    /// The absence of a value.
    case none
    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)
}

(Ignore the @_frozen attribute for now. We’ll discuss it in Frozen and Non-Frozen Enums later on.)

The Result type, whose purpose is to represent the success or failure of an operation, has a similar shape but adds a second associated value (and corresponding generic parameter) for the failure case, enabling it to capture detailed error information:

enum Result<Success, Failure: Error> {
    /// A success, storing a `Success` value.
    case success(Success)
    /// A failure, storing a `Failure` value.
    case failure(Failure)
}

We discuss Result in detail in the Error Handling chapter, and we’ll also use it in many examples in this chapter.

You create an enum value by specifying one of its cases, plus values for the case’s associated values, if it has any:

let alignment = TextAlignment.left
let download: Result<String, NetworkError> = .success("<p>Hello world!</p>")

Notice that in the second line, we have to provide the full type annotation, including all generic parameters. An expression like Result.success(htmlText) produces an error unless the compiler can infer the concrete type of the other generic parameter, Failure, from context. Having specified the complete type once, we can then rely on type inference using the leading-dot syntax. (The definition of NetworkError is not shown here.)

Enums Are Value Types

Enums are value types, just like structs. They have almost all the same capabilities structs have:

  • Enums can have methods, computed properties, and subscripts.

  • Methods can be declared mutating or non-mutating.

  • You can write extensions for enums.

  • Enums can conform to protocols.

Enums cannot have stored properties, however. An enum’s state is fully represented by its case plus the case’s associated value. Think of the associated values as the stored properties for a particular case.

Mutating methods on enums work the same way they do on structs. We saw in the Structs and Classes chapter that inside a mutating method, self is passed inout and hence mutable. Because enums don’t have stored properties and there’s no way to mutate a case’s associated value directly, we mutate an enum by assigning a new value directly to self.

Enums don’t require explicit initializers because the usual way to initialize an enum variable is to assign it a case. However, it’s possible to add additional “convenience” initializers in the type definition or in an extension. For example, using the Locale API from Foundation, we can add an initializer to our TextAlignment enum that sets a default text alignment for a given locale:

extension TextAlignment {
    init(defaultFor locale: Locale) {
        guard let language = locale.languageCode else {
            // Default value if language is n/a.
            self = .left
            return
        }
        switch Locale.characterDirection(forLanguage: language) {
        case .rightToLeft:
            self = .right
        // Left is the default for everything else.
        case .leftToRight, .topToBottom, .bottomToTop, .unknown:
            self = .left
        @unknown default:
            self = .left
        }
    }
}

let english = Locale(identifier: "en_AU")
TextAlignment(defaultFor: english) // left
let arabic = Locale(identifier: "ar_EG")
TextAlignment(defaultFor: arabic) // right

(We’ll cover the @unknown default case in the Frozen and Non-Frozen Enums section.)

Sum Types and Product Types

An enum value contains exactly one of its cases (plus values for the case’s associated values, if any). In fact, enums were once called “oneof” and later “union” in the early days of Swift (before the first public release). More concretely, a Result value contains either a success or a failure value, but never both (and never none). In contrast, an instance of a record type contains values for all of its fields: a (String, Int) tuple contains a string and an integer. (Note that we talk about compound records with more than one field here; UInt8 is a struct too, and you might say that it constrains instances to “one of 0…255”. But that’s not what we mean.)

This ability to model “or” relationships is fairly unique and it’s what makes enums so useful. It allows us to write safer and more expressive code that takes full advantage of strong types in situations that often can’t be expressed as cleanly with structs, tuples, or classes.

We say “fairly unique” because protocols and subclassing can be used for the same purpose, albeit with very different tradeoffs and applications. A variable of a protocol type (also called an existential) can be one of any type that conforms to the protocol. Similarly, an object of type UIView on iOS can also refer to any one of UIView’s direct or indirect subclasses, such as UILabel or UIButton. When working with such an object, we can either use the common interface defined on the base type (equivalent to calling methods defined on an enum), or attempt to downcast the instance to a concrete subtype in order to access data that’s unique to that subtype (equivalent to switching over an enum).

The difference lies in which approach is more common (dynamic dispatch through the common interface for protocols and classes, switching for enums), and also in the particular capabilities and limitations the constructs have. For example, the list of cases of an enum is fixed and can’t be extended retroactively, whereas you can always conform one more type to a protocol or add another subclass (though subclassing across module boundaries is restricted unless you explicitly declare a class open). Whether this freedom is desirable or even required depends on the problem to be solved. As value types, enums are also generally more lightweight and better suited for modeling “plain old values.”

There’s a neat correspondence between the two categories of types (“or” and “and”) and the mathematical concepts of addition and multiplication. Knowing about it is not essential for being a good Swift programmer, but we find it a helpful line of thinking when designing custom types.

There are many possible definitions for the term “type.” Here’s one: a type is the set of all possible values, or inhabitants, its instances can assume. Bool has two inhabitants, false and true. UInt8 has 28 = 256 inhabitants. Int64 has 264, or about 18.4 quintillion, inhabitants. Types such as String have infinitely many inhabitants — you can always create another string by adding one more character (at least until you’ve filled up your computer’s memory).

Now consider a tuple of two Boolean fields: (Bool, Bool). How many inhabitants does this type have? The answer is four: (false, false), (true, false), (false, true), and (true, true). It’s impossible to construct any other value of this type except these four. What if we add another Bool, making it (Bool, Bool, Bool)? The number of inhabitants doubles to eight since each of the previous four inhabitants can be combined with false and true, respectively. This works not only with Bools, of course. A (Bool, UInt8) pair has 2 × 256 = 512 inhabitants because each of the 256 UInt8 inhabitants can be paired with one of the two Boolean values.

Generally speaking, the number of inhabitants of a tuple (or struct, or class) is equal to the product of the inhabitants of its members. For this reason, structs, classes, and tuples are also called product types.

Contrast this with enums. Here’s an enum with three cases:

enum PrimaryColor {
    case red
    case yellow
    case blue
}

This type has three inhabitants, one per case. It’s impossible to construct any other PrimaryColor value than .red, .yellow, or .blue. What happens if we add associated values into the mix? Let’s add a fourth case that allows us to specify a grayscale value between 0 (black) and 255 (white):

enum ExtendedColor {
    case red
    case yellow
    case blue
    case gray(brightness: UInt8)
}

The .gray case alone has 256 possible values, resulting in 3 + 256 = 259 inhabitants for the entire enum. Generally speaking, the number of inhabitants of an enum is equal to the sum of the inhabitants of its cases. This is why enums are also called sum types.

Adding a field to a struct multiplies the number of possible states, often enormously. Adding a case to an enum only adds one additional inhabitant (or, if the case has an associated value, its inhabitants). This is a very useful property for writing safe code. The Designing with Enums section later in this chapter is all about how to take advantage of this property in our code.

Pattern Matching

Designing with Enums

Raw Values

Enumerating Enum Cases

Frozen and Non-Frozen Enums

Tips and Tricks

Recap

Strings

All modern programming languages have support for Unicode strings, but that often only means that the native string type can store Unicode data — it’s not a promise that simple operations like getting the length of a string will return “sensible” results. In fact, most languages, and in turn, most string manipulation code written in those languages, exhibit a certain level of denial about Unicode’s inherent complexity. This can lead to some unpleasant bugs.

Swift’s string implementation goes to heroic efforts to be as Unicode-correct as possible. A String in Swift is a collection of Character values, where a Character is what a human reader of a text would perceive as a single character, regardless of how many Unicode scalars it’s composed of. As a result, all standard Collection operations like count or prefix(5) work on the level of user-perceived characters.

This is great for correctness, but it comes at a price, mostly in terms of unfamiliarity; if you’re used to manipulating strings with integer indices in other languages, Swift’s design will seem unwieldy at first, leaving you wondering: Why can’t I write str[999] to access a string’s one-thousandth character? Why doesn’t str[idx+1] get the next character? Why can’t I loop over a range of Character values such as "a"..."z"? It also has performance implications: String does not support random access, i.e. jumping to an arbitrary character is not an O(1) operation. It can’t be — when characters have variable width, the string doesn’t know where the nth character is stored without looking at all the characters that come before it.

In this chapter, we’ll discuss the string architecture in detail, as well as some techniques for getting the most out of Swift strings in terms of functionality and performance. But we’ll start with an overview of the required Unicode terminology.

Unicode

Things used to be so simple. ASCII strings were a sequence of integers between 0 and 127. If you stored them in an 8-bit byte, you even had a bit to spare! Since every character was of a fixed size, ASCII strings could be random access.

But ASCII wasn’t enough if you were writing in anything other than English or for a non-U.S. audience; other countries and languages needed other characters (even English-speaking Britain needed a £ sign). Most of them needed more characters than would fit into seven bits. ISO 8859 takes the extra bit and defines 16 different encodings above the ASCII range, such as Part 1 (ISO 8859-1, aka Latin-1), covering several Western European languages; and Part 5, covering languages that use the Cyrillic alphabet.

This is still limiting, though: if you want to use ISO 8859 to write in Turkish about Ancient Greek, you’re out of luck, since you’d need to pick either Part 7 (Latin/Greek) or Part 9 (Turkish). And eight bits is still not enough to encode many languages. For example, Part 6 (Latin/Arabic) doesn’t include the characters needed to write Arabic-script languages such as Urdu or Persian. Meanwhile, Vietnamese — which is based on the Latin alphabet but with a large number of diacritic combinations — only fits into eight bits by replacing a handful of ASCII characters from the lower half. And this isn’t even an option for other East Asian languages.

When you run out of room with a fixed-width encoding, you have a choice: either increase the size, or switch to variable-width encoding. Initially, Unicode was defined as a 2-byte fixed-width format, now called UCS-2. This was before reality set in and it was accepted that even two bytes (i.e. ~65,000 code points) would not be sufficient, while four would be horribly inefficient for most purposes. So today, Unicode is a variable-width format, and it’s variable in two different senses:

  • A single character (also known as an extended grapheme cluster) consists of one or more Unicode scalars.

  • A scalar is encoded by one or more code units.

To understand why, we need to clarify what these terms mean.

The basic building block of Unicode is the code point: an integer value in the Unicode code space, which ranges from 0 to 0x10FFFF (in decimal notation: 1,114,111). Every character or other unit of script that’s part of Unicode is assigned a unique code point. In Unicode 12.1 (published in May 2019), only about 138,000 of the 1.1 million available code points are currently in use, so there’s a lot of room for more emoji. Code points are commonly written in hex notation with a “U+” prefix. For example, the euro sign is at code point U+20AC (or 8364 in decimal).

Unicode scalars are almost, but not quite, the same as code points. They’re all the code points except the 2,048 surrogate code points in the range 0xD800 to 0xDFFF (which are used by the UTF-16 encoding to represent code points greater than 65,535). Scalars are represented in Swift string literals as "\u{xxxx}", where xxxx represents hex digits. So the euro sign can be written in Swift as either "€" or "\u{20AC}". The corresponding Swift type is Unicode.Scalar, which is a wrapper around a UInt32 value.

The same Unicode data (i.e. a sequence of scalars) can be encoded with different encodings, UTF-8 and UTF-16 being the most common ones. The smallest entity in an encoding is called a code unit. The UTF-8 encoding has 8-bit-wide code units and UTF-16 has 16-bit-wide code units. UTF-8 has the added benefit of being backwardly compatible with 8-bit ASCII — a feature that’s helped it overtake ASCII as the most popular encoding on the web and in file formats. Code units are different from code points or scalars because a single scalar is often encoded with multiple code units. Since there are more than a million potential code points, UTF-8 takes one to four code units (one to four bytes) to encode a single scalar, whereas UTF-16 takes either one or two code units (two or four bytes). Swift represents UTF-8 and UTF-16 code units as UInt8 and UInt16 values, respectively (aliased as Unicode.UTF8.CodeUnit and Unicode.UTF16.CodeUnit).

To represent each scalar by a single code unit, you’d need a 21-bit encoding scheme, which usually gets rounded up to 32-bit and is called UTF-32. This is what Unicode.Scalar does in Swift. But even that wouldn’t get you a fixed-width encoding: Unicode is still a variable-width format when it comes to “characters.” What a user might consider “a single character” — as displayed on the screen — might require multiple scalars composed together. The Unicode term for such a user-perceived character is an (extended) grapheme cluster.

The rules for how scalars form grapheme clusters determine how text is segmented. For example, if you hit the backspace key on your keyboard, you expect your text editor to delete exactly one grapheme cluster, even if that “character” is composed of multiple Unicode scalars, each of which may use a varying number of code units in the text’s representation in memory. Grapheme clusters are represented in Swift by the Character type, which can encode an arbitrary number of scalars, as long as they form a single user-perceived character. We’ll see some examples of this in the next section.

Grapheme Clusters and Canonical Equivalence

Combining Marks

A quick way to see how String handles Unicode data is to look at the two different ways to write é. Unicode defines U+00E9, Latin small letter e with acute, as a single value. But you can also write it as the plain letter e, followed by U+0301, combining acute accent. In both cases, what’s displayed is é, and a user probably has a reasonable expectation that two strings displayed as “résumé” would not only be equal to each other but also have a “length” of six characters, no matter which technique was used to produce the é in either one. They would be what the Unicode specification describes as canonically equivalent.

And in Swift, this is exactly the behavior you get:

let single = "Pok\u{00E9}mon" // Pokémon
let double = "Poke\u{0301}mon" // Pokémon

They both display identically:

(single, double) // ("Pokémon", "Pokémon")

And both have the same character count:

single.count // 7
double.count // 7

Consequently, they also compare equal:

single == double // true

Only if you drop down to a view of the underlying representation can you see that they’re different:

single.unicodeScalars.count // 7
double.unicodeScalars.count // 8

Compare this with NSString in Foundation: the two strings aren’t equal, and the length property — which many Objective-C programmers probably use to count the number of characters to be displayed on the screen — gives different results:

let nssingle = single as NSString
nssingle.length // 7
let nsdouble = double as NSString
nsdouble.length // 8
nssingle == nsdouble // false

Here, == is defined as the version for comparing two NSObjects:

extension NSObject: Equatable {
    static func ==(lhs: NSObject, rhs: NSObject) -> Bool {
        return lhs.isEqual(rhs)
    }
}

In the case of NSString, this will do a literal comparison on the level of UTF-16 code units rather than one accounting for equivalent but differently composed characters. Most string APIs in other languages work this way too. If you really want to perform a canonical comparison of two NSStrings, you must use NSString.compare(_:).

Of course, there’s one big benefit to just comparing code units: it’s faster! This is an effect that can still be achieved with Swift strings via the utf8 view:

single.utf8.elementsEqual(double.utf8) // false

Why does Unicode support multiple representations of the same character at all? The existence of precomposed characters is what enables the opening range of Unicode code points to be compatible with Latin-1, which already had characters like é and ñ. While they might be a pain to deal with, it makes conversion between the two encodings quick and simple.

And ditching precomposed forms wouldn’t have helped anyway, because composition doesn’t just stop at pairs; you can compose more than one diacritic together. For example, Yoruba has the character ọ́, which could be written three different ways: by composing ó with a dot, or by composing ọ with an acute, or by composing o with both an acute and a dot. And for that last one, the two diacritics can be in either order! So these are all equal:

let chars: [Character] = [
    "\u{1ECD}\u{300}",      // ọ́
    "\u{F2}\u{323}",        // ọ́
    "\u{6F}\u{323}\u{300}", // ọ́
    "\u{6F}\u{300}\u{323}"  // ọ́
]
let allEqual = chars.dropFirst().allSatisfy { $0 == chars.first } // true

In fact, some diacritics can be added ad infinitum. A famous internet meme illustrates this nicely:

let zalgo = "s̼̐͗͜o̠̦̤ͯͥ̒ͫ́ͅo̺̪͖̗̽ͩ̃͟ͅn̢͔͖͇͇͉̫̰ͪ͑"

zalgo.count // 4
zalgo.utf8.count // 68

In the above, zalgo.count (correctly) returns 4, while zalgo.utf8.count returns 68. And if your code doesn’t work correctly with internet memes, then what good is it, really?

Unicode’s grapheme-breaking rules even affect you when all strings you deal with are pure ASCII: CR+LF, the character pair of carriage return and line feed that’s commonly used as a line break on Windows, is a single grapheme:

// CR+LF is a single Character.
let crlf = "\r\n"
crlf.count // 1

Emoji

Strings containing emoji can also be surprising in various other programming languages. Many emoji are assigned Unicode scalars that don’t fit in a single UTF-16 code unit. Languages that represent strings as collections of UTF-16 code units, such as Java or C#, would say that the string "😂" is two “characters” long. Swift handles this case correctly:

let oneEmoji = "😂" // U+1F602
oneEmoji.count // 1

Notice that the important thing is how the string is exposed to the program, not how it’s stored in memory. Swift uses UTF-8 as its internal encoding for non-ASCII strings, but that’s an implementation detail. The public API is based on grapheme clusters.

Other emoji are composed of multiple scalars. An emoji flag is a combination of two regional indicator symbols that correspond to an ISO country code. Swift treats the flag correctly as one Character:

let flags = "🇧🇷🇳🇿"
flags.count // 2

To inspect the Unicode scalars a string is composed of, use the unicodeScalars view. Here, we format the scalar values as hex numbers in the common format for code points:

flags.unicodeScalars.map {
    "U+\(String($0.value, radix: 16, uppercase: true))"
}
// ["U+1F1E7", "U+1F1F7", "U+1F1F3", "U+1F1FF"]

Skin tones combine a base character such as 👧 with one of five skin tone modifiers (e.g., 🏽, or the type-4 skin tone modifier) to yield the final emoji (e.g. 👧🏽). Again, Swift handles this correctly:

let skinTone = "👧🏽" // 👧 + 🏽
skinTone.count // 1

Emoji depicting families and couples, such as 👨‍👩‍👧‍👦 and 👩‍❤️‍👩, present another challenge to the Unicode standards body. Due to the countless possible combinations of gender and the number of people in a group, providing a separate code point for each variation is problematic. Combine this with a distinct skin tone for each person and it becomes impossible. Unicode solves this by specifying that these emoji are actually sequences of multiple emoji, combined with the invisible zero-width joiner (ZWJ) character (U+200D). So the family 👨‍👩‍👧‍👦 is really man 👨 + ZWJ + woman 👩 + ZWJ + girl 👧 + ZWJ + boy 👦. The ZWJ serves as an indicator to the operating system that it should use a single glyph if available.

You can verify that this is really what’s going on:

let family1 = "👨‍👩‍👧‍👦"
let family2 = "👨\u{200D}👩\u{200D}👧\u{200D}👦"
family1 == family2 // true

And once again, Swift is smart enough to treat such a sequence as a single Character:

family1.count // 1
family2.count // 1

New emoji for professions introduced in 2016 are ZWJ sequences too. For example, the female firefighter 👩‍🚒 is composed of woman 👩 + ZWJ + fire engine 🚒, and the male health worker 👨‍⚕️ is a sequence of man 👨 + ZWJ + staff of Aesculapius ⚕.

Rendering these sequences into a single glyph is the task of the operating system. On Apple platforms in 2019, the OS includes glyphs for the subset of sequences the Unicode standard lists as “recommended for general interchange” (RGI), i.e. the ones “most likely to be widely supported across multiple platforms.” When no glyph is available for a syntactically valid sequence, the text-rendering system falls back to rendering each component as a separate glyph. Notice that this can cause a mismatch “in the other direction” between user-perceived characters and what Swift sees as a grapheme cluster; all examples up until now were concerned with programming languages overcounting characters, but here we see the reverse. As an example, family sequences containing skin tones are currently not part of the RGI subset. But even if the operating system renders such a sequence as multiple glyphs, Swift still counts it as a single Character because the Unicode text segmentation rules are not concerned with rendering:

// Family with skin tones is rendered as multiple glyphs
// on most platforms in 2019.
let family3 = "👱🏾\u{200D}👩🏽\u{200D}👧🏿\u{200D}👦🏻"
// But Swift still counts it as a single Character.
family3.count // 1

Microsoft can already render this and other variations as single glyphs, by the way, and the other OS vendors will almost certainly follow soon. But the point still stands: no matter how carefully a string API is designed, text is so complicated that it may never catch all edge cases.

In the past, Swift had trouble keeping up with Unicode changes. Swift 3 handled skin tones and ZWJ sequences incorrectly because its grapheme-breaking algorithm was based on an old version of the Unicode standard. As of Swift 4, Swift uses the operating system’s ICU library. As a result, your programs will automatically adopt new Unicode rules as users update their OSes. The other side of the coin is of course that you can’t rely on users seeing the same behavior you see during development. For example, when you deploy server-side Swift code on Linux, the code might behave differently because the Linux distribution might ship with a different ICU version than your development machine does.

In the examples we discussed in this section, we treated the length of a string as a proxy for all sorts of things that can go wrong when a language doesn’t take the full complexity of Unicode into account. Just think of the gibberish a simple task such as reversing a string can produce in a programming language that doesn’t process strings by grapheme clusters when the string contains composed character sequences. This isn’t a new problem, but the emoji explosion has made it much more likely that bugs caused by sloppy text handling will come to the surface, even if your user base is predominantly English-speaking. And the magnitude of errors has increased as well: whereas a decade ago a botched accented character would cause an off-by-one error, messing up a modern emoji can easily cause results to be off by 10 or more “characters.” For example, a four-person family emoji is 11 (UTF-16) or 25 (UTF-8) code units long:

family1.count // 1
family1.utf16.count // 11
family1.utf8.count // 25

It’s not that other languages don’t have Unicode-correct APIs at all — most do. For instance, NSString has the enumerateSubstrings method that can be used to walk through a string by grapheme clusters. But defaults matter; Swift’s priority is to do the correct thing by default. And if you ever need to drop down to a lower level of abstraction, String provides views that let you operate directly on Unicode scalars or code units. We’ll say more about those below.

Strings and Collections

As we’ve seen, String is a collection of Character values. In Swift’s first three years of existence, String went back and forth between conforming and not conforming to the Collection protocol. The argument for not adding the conformance was that programmers would expect all generic collection-processing algorithms to be completely safe and Unicode-correct, which wouldn’t necessarily be true for all edge cases.

As a simple example, you might assume that if you concatenate two collections, the resulting collection’s length would be the sum of the lengths of the two source collections. But this doesn’t hold for strings if a suffix of the first string forms a grapheme cluster with a prefix of the second string:

let flagLetterJ = "🇯"
let flagLetterP = "🇵"
let flag = flagLetterJ + flagLetterP // 🇯🇵
flag.count // 1
flag.count == flagLetterJ.count + flagLetterP.count // false

To this end, String itself was not made a Collection in Swift 2 and 3; a collection-of-characters view was moved to a property, characters, which put it on a footing similar to the other collection views: unicodeScalars, utf8, and utf16. Picking a specific view prompted you to acknowledge you were moving into a collection-processing mode and that you should consider the consequences of the algorithm you were about to run.

In practice, the loss in usability and learnability caused by this change turned out to vastly outweigh the gain in correctness for a few edge cases that are rarely relevant in real code (unless you’re writing a text editor). So String was made a Collection again in Swift 4.

Bidirectional, Not Random Access

However, for reasons that should be clear from the examples in the previous section, String is not a random-access collection. How could it be, when knowing where the nth character of a particular string is involves evaluating just how many Unicode scalars precede that character? For this reason, String conforms only to BidirectionalCollection. You can start at either end of the string, moving forward or backward, and the code will look at the composition of the adjacent characters and skip over the correct number of bytes. However, you need to iterate up and down one character at a time.

Keep the performance implications of this in mind when writing string-processing code. Algorithms that depend on random access to maintain their performance guarantees aren’t a good match for Unicode strings. Consider this String extension for generating a list of a string’s prefixes, which works by generating an integer range from zero to the string’s length and then mapping over the range to create the prefix for each length:

extension String {
    var allPrefixes1: [Substring] {
        return (0...count).map(prefix)
    }
}

let hello = "Hello"
hello.allPrefixes1 // ["", "H", "He", "Hel", "Hell", "Hello"]

As simple as this code looks, it’s very inefficient. It first walks over the string once to calculate the length, which is fine. But then each of the n + 1 calls to prefix is another O(n) operation because prefix always starts at the beginning and has to work its way through the string to count the desired number of characters. Running a linear process inside another linear loop means this algorithm is accidentally O(n2) — as the length of the string increases, the time this algorithm takes increases quadratically.

If possible, an efficient string algorithm should walk over a string only once and then operate on string indices to denote the substrings it’s interested in. Here’s a better version of the same algorithm:

extension String {
    var allPrefixes2: [Substring] {
        return [""] + indices.map { index in self[...index] }
    }
}

hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]

This code also has to iterate over the string once to generate the indices collection. But once that’s done, the subscripting operation inside map is O(1). This makes the whole algorithm O(n).

Range-Replaceable, Not Mutable

String also conforms to RangeReplaceableCollection. Here’s an example of how you’d replace part of a string by first identifying the appropriate range in terms of string indices and then calling replaceSubrange. The replacement string can have a different length or could even be empty (which would be equivalent to calling removeSubrange):

var greeting = "Hello, world!"
if let comma = greeting.firstIndex(of: ",") {
    greeting[..<comma] // Hello
    greeting.replaceSubrange(comma..., with: " again.")
}
greeting // Hello again.

As always, keep in mind that results may be surprising if parts of the replacement string form new grapheme clusters with adjacent characters in the original string.

One collection-like feature that strings do not provide is that of MutableCollection. This protocol adds one feature to a collection — that of the single-element subscript set — in addition to get. This isn’t to say strings aren’t mutable — as we’ve just seen, they have several mutation methods. But what you can’t do is replace a single character using the subscript operator. The reason comes back to variable-length characters. Most people can probably intuit that a single-element subscript update would happen in constant time, as it does for Array. But since a character in a string may be of variable width, updating a single character could take linear time in proportion to the length of the string: changing the width of a single element would require shuffling all the later elements up or down in memory. Moreover, indices that come after the replaced index would become invalid through the shuffling, which is equally unintuitive. For these reasons, you have to use replaceSubrange, even if the range you pass in is only a single character.

String Indices

Most programming languages use integers for subscripting strings, e.g. str[5] would return the sixth “character” of str (for whatever that language’s idea of a “character” is). Swift doesn’t allow this. Why? The answer should sound familiar to you by now: subscripting is supposed to take constant time (intuitively as well as per the requirements of the Collection protocol), and looking up the nth Character is impossible without looking at all the bytes that come before it.

String.Index, the index type used by String and its views, is an opaque value that essentially stores a byte offset into the string’s in-memory representation (usually UTF-8). It’s still an O(n) operation if you want to compute the index for the nth character and have to start at the beginning of the string, but once you have a valid index, subscripting the string with it will only take O(1) time. And crucially, finding the next index after an existing index is also fast because you can start at the existing index’s byte offset — you don’t need to go back to the beginning again. This is why iterating over the characters in a string in order (forward or backward) is efficient.

String index manipulation is based on the same Collection APIs you’d use with any other collection. It’s easy to miss this equivalence since the collections we use by far the most — arrays — use integer indices, and we usually use simple arithmetic to manipulate those. The index(after:) method returns the index of the next character:

let s = "abcdef"
let second = s.index(after: s.startIndex)
s[second] // b

You can automate iterating over multiple characters in one go via the index(_:offsetBy:) method:

// Advance 4 more characters.
let sixth = s.index(second, offsetBy: 4)
s[sixth] // f

If there’s a risk of advancing past the end of the string, you can add a limitedBy: parameter. The method returns nil if it hits the limit before reaching the target index:

let safeIdx = s.index(s.startIndex, offsetBy: 400, limitedBy: s.endIndex)
safeIdx // nil

This is undoubtedly more code than simple integer indices would require, but again, that’s the point. If Swift allowed integer subscripting of strings, the temptation to accidentally write horribly inefficient code (e.g. by using integer subscripting inside a loop) would be too big.

Nevertheless, to someone used to dealing with fixed-width characters, working with strings in Swift seems challenging at first — how will you navigate without integer indices? And indeed, some seemingly simple tasks like extracting the first four characters of a string can turn into monstrosities like this one:

s[..<s.index(s.startIndex, offsetBy: 4)] // abcd

But thankfully, being able to access the string via the Collection interface also means you have several helpful techniques at your disposal. Most of the methods that operate on Array also work on String. Using the prefix method, the same thing looks much clearer:

s.prefix(4) // abcd

(Note that either expression returns a Substring; you can convert it back into a String by wrapping it in a String.init. We’ll talk more about substrings in the next section.)

As a slightly more complex example, extracting the month from a date string can be accomplished entirely without any subscripting operations into the string:

let date = "2019-09-01"
date.split(separator: "-")[1] // 09
date.dropFirst(5).prefix(2) // 09

For finding a specific character, you can use firstIndex(of:):

var hello = "Hello!"
if let idx = hello.firstIndex(of: "!") {
    hello.insert(contentsOf: ", world", at: idx)
}
hello // Hello, world!

The insert(contentsOf:at:) method inserts another collection of the same element type (e.g. Character for strings) before a given index. This doesn’t have to be another String; you could insert an array of characters into a string just as easily.

Of course, there are also tasks that cannot be accomplished just by using the Collection APIs on a string: parsing a CSV file is a good example of this. We cannot simply split a line on comma characters, because commas can also appear within values that are wrapped in quotes. To solve tasks like this, we can iterate over the string, character by character, while keeping track of some state. Essentially, we’re writing a very simple parser:

func parse(csv: String) -> [[String]] {
    var result: [[String]] = [[]]
    var currentField = ""
    var inQuotes = false
    for c in csv {
        switch (c, inQuotes) {
        // ...
        }
    }
    return result
}

First, we create a result as an array of arrays of strings. Each line is represented by an array of strings, and the CSV string can contain many lines. The currentField variable acts as a buffer to collect the characters of one field while we iterate over the string. Finally, the inQuotes Boolean keeps track of whether or not we’re currently within a quoted string. It’s the only piece of state we need for this simple parser.

Now we have to fill in the cases for the switch statement:

  • (",", false) — a comma outside of quotes ends the current field

  • ("\n", false) — a newline outside of quotes ends the current line

  • ("\"", _) — a quote toggles the inQuotes Boolean

  • default — in all other cases, we append the current character to currentField

func parse(csv: String) -> [[String]] {
    // ...
    for c in csv {
        switch (c, inQuotes) {
        case (",", false):
            result[result.endIndex-1].append(currentField)
            currentField.removeAll()
        case ("\n", false):
            result[result.endIndex-1].append(currentField)
            currentField.removeAll()
            result.append([])
        case ("\"", _):
            inQuotes = !inQuotes
        default:
            currentField.append(c)
        }
    }
    result[result.endIndex-1].append(currentField)
    return result
}

(We’re creating a temporary tuple to switch over two values at once. You may remember this technique from the Enums chapter.)

After the for loop, we still have to append currentField one last time before returning the result, because the CSV string might not end on a newline.

Let’s try the CSV parser out with an example:

let csv = #"""
    "Values in quotes","can contain , characters"
    "Values without quotes work as well:",42
    """#

parse(csv: csv)
/*
[["Values in quotes", "can contain , characters"],
 ["Values without quotes work as well:", "42"]]
*/

The string literal above is using the extended delimiters syntax (enclosing the string literal in # characters), which allows us to write quotes within the string literal without having to escape them.

Being able to write small parsers like this one significantly enhances your string-handling skills. In this way, tasks that are very difficult or impossible to accomplish with Collection APIs or even regular expressions often become simple.

The CSV parser above is not complete, but it’s already useful. It’s very short because we don’t have to track a lot of state; there’s just a single Boolean variable. With a little bit of extra work, we could ignore empty lines, ignore whitespace around quoted fields, and support escaping of quotes within quoted fields (by using two quote characters). Instead of using a single Boolean for tracking the parser’s state, we would then use an enum to distinguish all possible states unambiguously.

However, the more state we add to the parser, the easier it becomes to make mistakes in its implementation. Therefore, this approach of parsing within a single loop is only advisable for very simple parsing tasks. If we have to keep track of more state, we’d have to change the strategy from writing everything in a single loop to breaking the parser up into multiple functions.

Substrings

Like all collections, String has a specific slice or SubSequence type named Substring. A substring is much like an ArraySlice: it’s a view of a base string with different start and end indices. Substrings share the text storage of their base strings. This has the huge benefit that slicing a string is a very cheap operation. Creating the firstWord variable in the following example requires no expensive copies or memory allocation:

let sentence = "The quick brown fox jumped over the lazy dog."
let firstSpace = sentence.firstIndex(of: " ") ?? sentence.endIndex
let firstWord = sentence[..<firstSpace] // The
type(of: firstWord) // Substring

Slicing being cheap is especially important in loops where you iterate over the entire (potentially long) string to extract its components. Tasks like finding all occurrences of a word in a text or parsing a CSV file come to mind. A very useful string processing operation in this context is splitting. The split method is defined on Collection and returns an array of subsequences (i.e. [Substring]). Its most common variant is defined like so:

extension Collection where Element: Equatable {
    public func split(separator: Element, maxSplits: Int = Int.max,
        omittingEmptySubsequences: Bool = true) -> [SubSequence]
}

You can use it like this:

let poem = """
    Over the wintry
    forest, winds howl in rage
    with no leaves to blow.
    """
let lines = poem.split(separator: "\n")
// ["Over the wintry", "forest, winds howl in rage", "with no leaves to blow."]
type(of: lines) // Array<Substring>

This can serve a function similar to the components(separatedBy:) method String inherits from NSString, with added configurations for whether or not to drop empty components. Again, no copies of the input string are made. And since there’s another variant of split that takes a closure, it can do more than just compare characters. Here’s an example of a primitive word wrap algorithm, where the closure captures a count of the length of the line thus far:

extension String {
    func wrapped(after maxLength: Int = 70) -> String {
        var lineLength = 0
        let lines = self.split(omittingEmptySubsequences: false) { character in
            if character.isWhitespace && lineLength >= maxLength {
                lineLength = 0
                return true
            } else {
                lineLength += 1
                return false
            }
        }
        return lines.joined(separator: "\n")
    }
}

sentence.wrapped(after: 15)
/*
 The quick brown
 fox jumped over
 the lazy dog.
*/

Or, consider writing a version that takes a sequence of multiple separators:

extension Collection where Element: Equatable {
    func split<S: Sequence>(separators: S) -> [SubSequence]
        where Element == S.Element
    {
        return split { separators.contains($0) }
    }
}

This way, you can write the following:

"Hello, world!".split(separators: ",! ") // ["Hello", "world"]

StringProtocol

Substring has almost the same interface as String. This is achieved through a common protocol named StringProtocol, which both types conform to. Since almost the entire string API is defined on StringProtocol, you can mostly work with a Substring as you would with a String. At some point, though, you’ll have to turn your substrings back into String instances; like all slices, substrings are only intended for short-term storage, in order to avoid expensive copies during an operation. When the operation is complete and you want to store the results or pass them on to another subsystem, you should create a new String. You can do this by initializing a String with a Substring, as we do in this example:

func lastWord(in input: String) -> String? {
    // Process the input, working on substrings.
    let words = input.split(separators: [",", " "])
    guard let lastWord = words.last else { return nil }
    // Convert to String for return.
    return String(lastWord)
}

lastWord(in: "one, two, three, four, five") // Optional("five")

The rationale for discouraging long-term storage of substrings is that a substring always holds on to the entire original string. A substring representing a single character of a huge string will hold the entire string in memory, even after the original string’s lifetime would normally have ended. Long-term storage of substrings would therefore effectively cause memory leaks because the original strings have to be kept in memory even when they’re no longer accessible.

By working with substrings during an operation and only creating new strings at the end, we defer copies until the last moment and make sure to only incur the cost of those copies that are actually necessary. In the example above, we split the entire (potentially long) string into substrings, but we only pay the cost for a single copy of one short substring at the end. (Ignore for a moment that this algorithm isn’t efficient anyway; iterating backward from the end until we find the first separator would be the better approach.)

Encountering a function that only accepts a Substring when you want to pass a String is less common — most functions should either take a String or any StringProtocol-conforming type. But if you do need to pass a Substring, the quickest way is to subscript the string with the range operator ... without specifying any bounds:

// Substring with identical start and end index as the base string.
let substring = sentence[...]

We already saw an example of this in the chapter on Collection Protocols when we defined the Words collection.

The Substring type was introduced in Swift 4. In Swift 3, String.CharacterView used to be its own slice type. This had the advantage that users only had to understand a single type, but it meant that a stored substring would keep the entire original string buffer alive even after it normally would’ve been released. By introducing Substring, Swift traded a small loss in convenience for cheap slicing operations and predictable memory usage.

You may be tempted to take full advantage of the existence of StringProtocol and convert all your APIs to take StringProtocol instances rather than plain Strings. But the advice of the Swift team is not to do that:

Our general advice is to stick with String. Most APIs would be simpler and clearer just using String rather than being made generic (which itself can come at a cost), and user conversion on the way in on the few occasions that’s needed isn’t much of a burden.

APIs that are extremely likely to be used with substrings, and at the same time aren’t further generalizable to the Sequence or Collection level, are an exception to this rule. An example of this in the standard library is the joined method. The standard library provides an overload for sequences with StringProtocol-conforming elements:

extension Sequence where Element: StringProtocol {
    /// Returns a new string by concatenating the elements of the sequence,
    /// adding the given separator between each element.
    public func joined(separator: String = "") -> String
}

This lets you call joined directly on an array of substrings (which you got from a call to split, for example) without having to map over the array and copy every substring into a new string. This is more convenient and much faster.

The number type initializers that take a string and convert it into a number also take StringProtocol values. Again, this is especially handy if you want to process an array of substrings:

let commaSeparatedNumbers = "1,2,3,4,5"
let numbers = commaSeparatedNumbers.split(separator: ",")
    .compactMap { Int($0) }
numbers // [1, 2, 3, 4, 5]

Since substrings are intended to be shortlived, it’s generally not advisable to return one from a function unless we’re talking about Sequence or Collection APIs that return slices. If you write a similar function that only makes sense for strings, having it return a substring tells readers that it doesn’t make a copy. Functions that create new strings requiring memory allocations, such as uppercased(), should always return String instances.

If you want to extend String with new functionality, placing the extension on StringProtocol is a good idea to keep the API surface between String and Substring consistent. StringProtocol is explicitly designed to be used whenever you would’ve previously extended String. If you want to move existing extensions from String to StringProtocol, the only change you should have to make is to replace any passing of self into an API that takes a concrete String with String(self).

Keep in mind, though, that StringProtocol is not intended as a conformance target for your own custom string types. The documentation explicitly warns against it:

Do not declare new conformances to StringProtocol. Only the String and Substring types of the standard library are valid conforming types.

Code Unit Views

Strings and Foundation

Unicode Properties

Internal Structure of String and Character

String Literals

String Interpolation

Custom String Descriptions

Text Output Streams

Recap

Generics

Generic programming is a technique for writing reusable code while maintaining type safety. For example, the standard library uses generic programming to make the sort method take a custom comparator function while making sure the types of the comparator’s parameters match up with the element type of the sequence being sorted. Likewise, an array is generic over the kind of elements it contains in order to provide a type-safe API for accessing and mutating the array’s contents.

When we talk about generic programming in Swift, we usually mean programming with generics (signified by angle brackets in Swift’s syntax, e.g. Array<Int>). However, it’s helpful to see the broader context in which generics exist. Generics are a form of polymorphism. Polymorphism means using a single interface or name that works with multiple types.

There are at least four different concepts that can all be grouped under polymorphic programming:

  • We can define multiple methods with the same name but different types. For example, in the Functions chapter, we defined three different functions named sortDescriptor, all of which had different parameter types. This is called overloading, or more technically, ad hoc polymorphism.

  • When a function or method expects a class C, we can also pass in a subclass of C. This is called subtype polymorphism.

  • When a function has a generic parameter (in angle brackets), we call it a generic function (and likewise for generic types, or generic methods). This is called parametric polymorphism. The generic parameters are also called generics.

  • We can define protocols and make multiple types conform to them. This is another (more structured) form of ad hoc polymorphism, which we’ll discuss in the Protocols chapter.

Which concept is used to solve a particular problem is often a matter of taste. In this chapter, we’ll talk about the third technique, parametric polymorphism. Generics are often used together with protocols to specify constraints on generic parameters. We’ll see examples of this in the Protocols chapter, but this chapter focuses on just generics.

Generic Types

The most generic function we can write is the identity function, i.e. the function that returns its input unchanged:

func identity<A>(_ value: A) -> A {
    return value
}

The identity function has a single generic type: for any A we choose, it has the type (A) -> A. However, the identity function has an unlimited number of concrete types (types with no generic parameters, such as Int, Bool, and String). For example, if we choose A to be Int, the concrete type is (Int) -> Int; if we choose A to be (String -> Bool), then its type is ((String) -> Bool) -> (String) -> Bool; and so on.

Functions and methods aren’t the only generic types. We can also have generic structs, classes, and enums. For example, here’s the definition of Optional:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

We say that Optional is a generic type. When we choose a value for Wrapped, we get a concrete type. For example, Optional<Int> or Optional<UIView> are both concrete types. We can think of Optional as a type constructor: given a concrete type (e.g. Int), it constructs a different concrete type (e.g. Optional<Int>).

When we look at the standard library, we see there are many concrete types, but also many generic types (for example: Array, Dictionary, and Result). The Array type has a single generic parameter, Element. This means we can pick any concrete type and use it to create an array. We can also create our own generic types. For example, here’s an enum that describes a binary tree with values at the nodes:

enum BinaryTree<Element> {
    case leaf
    indirect case node(Element, l: BinaryTree<Element>, r: BinaryTree<Element>)
}

BinaryTree is a generic type with a single generic parameter, Element. To create a concrete type, we have to choose a concrete type for Element. For example, we can pick Int:

let tree: BinaryTree<Int> = .node(5, l: .leaf, r: .leaf)

When we want to turn a generic type into a concrete type, we have to choose exactly one concrete type for each generic parameter. You might already be familiar with this limitation when creating an Array. For example, when we create an empty array, we’re required to provide an explicit type; otherwise, Swift doesn’t know what concrete type to use for its elements:

// Type annotation is required.
var emptyArray: [String] = []

Likewise, Swift doesn’t allow you to put values with different concrete types in an array unless you explicitly choose a type for the Array’s Element. By default, the compiler suggests choosing Any:

let multipleTypes: [Any] = [1, "foo", true]

We’ll discuss Any in the section on generic functions.

Generics vs. Any

Designing with Generics

Recap

Protocols

When we work with generic types, we often want to constrain their generic parameters. There are many different reasons to use generics with protocols. Here are some common examples:

  • You can use a protocol to build an algorithm that depends on a type being a number (regardless of the concrete numeric type) or a collection. By programming against a protocol, all conforming types receive the new functionality.

  • You can use a protocol to abstract over different “backends” for your code. You can program against a protocol which is then implemented by different types. For example, a drawing program could use a Drawable protocol and be implemented by an SVG renderer and a Core Graphics renderer. Likewise, cross-platform code could use a Platform protocol with specific instances for Linux, macOS, and iOS.

  • You can use protocols to make code testable. More specifically, when you write code that uses a protocol rather than a concrete type, you can replace one concrete implementation with a different one in your tests.

A protocol in Swift declares a formal set of requirements. For example, the Equatable protocol requires that a conforming type implements the == operator. These requirements can consist of methods, initializers, associated types, properties, and inherited protocols. Some protocols also have additional requirements that can’t be expressed in Swift’s type system. For example, the Collection protocol expects that accessing an element through a subscript is O(1). (You’re allowed to deviate from O(1) as long as you document it.)

Let’s run through a number Swift’s major protocol features. Throughout the chapter, we’ll discuss each of these features in depth.

Protocols in Swift can be extended with more functionality. The simplest example is Equatable: it requires the == operator to be implemented, but then it adds the != operator, which uses the definition of ==. Likewise, the Sequence protocol has few requirements (you need to provide a way to make an iterator) but adds a lot of methods through extensions.

Protocols can also have conditional extensions to add APIs that require additional constraints. For example, the Collection protocol has a method, max(), that only exists for collections with an Element type that conforms to Comparable.

Protocols can inherit from other protocols. For example, Hashable specifies that any types conforming to it must be Equatable as well. Likewise, RangeReplaceableCollection inherits from Collection, which in turn inherits from Sequence. In other words, we can form protocol hierarchies.

Additionally, protocols can be composed together. For example, Codable is a type alias from the standard library that composes Encodable and Decodable.

In some cases, protocol conformances are dependent on other conformances. For example, an array conforms to Equatable if and only if its Element type conforms to Equatable. This is called conditional conformance: the conformance of Array to Equatable is conditional on its elements conforming to Equatable.

Protocols can declare associated types, and a conforming type is required to specify concrete types for these associated types. For example, the IteratorProtocol protocol defines an associated type, Element, and every type that conforms to IteratorProtocol has defined what its Element type is.

All the features above aren’t limited to the standard library; we can use them in our own protocols as well. While protocol-oriented programming is indispensable in Swift, we want to give a word of warning as well. Each protocol adds a layer of abstraction, and sometimes, this can lead to code that’s difficult to understand. At the same time, using a protocol can sometimes greatly simplify code. Finding a good balance here takes some experience.

Protocol Witnesses

Designing with Protocols

Protocols with Associated Types

Existentials

Type Erasers

Retroactive Conformance

Recap

Collection Protocols

We mentioned in the chapter on Built-In Collections that Swift’s collection types — like Array, Dictionary, and Set — are implemented on top of a rich set of abstractions for processing sequences of elements. This chapter is all about the Sequence and Collection protocols, which form the cornerstones of this model. We’ll cover how these protocols work, why they work the way they do, and how you can write your own sequences and collections. It can help to see the collection protocols in an inheritance diagram:

The collection protocol hierarchy in the standard library.
The collection protocol hierarchy in the standard library.
  • Sequence provides iteration. It allows you to create an iterator, but there are no guarantees about whether the sequence is single-pass (e.g. reading from standard input) or multi-pass (iterating over an array).

  • Collection extends Sequence. It guarantees that the sequence is multi-pass, and it allows you to look up elements by their indices. It also adds slicing capabilities via its SubSequence type, which is a collection itself.

  • MutableCollection adds the ability to mutate an element through a subscript in constant time. It does not allow you to add or remove elements. Array is a MutableCollection, but notably, String is not because it cannot guarantee constant-time mutation, as characters don’t have a fixed width.

  • RangeReplaceableCollection adds the ability to replace a contiguous range of elements in a collection. By extension, this also adds methods like append, remove, and so on. Many mutable collections are range-replaceable as well, but there are exceptions. Most notably, Set and Dictionary don’t conform, but types like String and Array do.

  • BidirectionalCollection adds the ability to iterate backward through a collection. For example, a Dictionary doesn’t allow reverse iteration and doesn’t conform, but a String does. Backward iteration is critical for some algorithms.

  • RandomAccessCollection extends BidirectionalCollection and adds the ability to compute with indices more efficiently: it requires that measuring the distance between indices and moving indices by a certain distance takes constant time. For example, an Array is a random-access collection, but a String is not: computing the distance between two string indices takes linear time.

  • LazySequenceProtocol models a sequence that computes its elements lazily while it is being iterated. This is mostly useful for writing algorithms in a functional style: you can take an infinite sequence and filter it, and then take the first element, all without incurring the (infinite) cost of computing elements the subsequent code doesn’t need.

  • LazyCollectionProtocol is the same as LazySequenceProtocol, but for collections.

In this chapter, we’ll look at each of these protocols in detail. When you write your own algorithms, it can be very useful to keep this protocol hierarchy in mind: if you can write an algorithm on one of the protocols toward the root of the hierarchy, more types will be able to take advantage of the algorithm.

Sequences

The Sequence protocol stands at the base of the hierarchy. A sequence is a series of values of the same type that lets you iterate over the values. The most common way to traverse a sequence is a for loop:

for element in someSequence {
    doSomething(with: element)
}

This seemingly simple capability of enumerating elements forms the foundation for a large number of useful operations Sequence provides to adopters of the protocol. We already saw many of them in the previous chapters. Whenever you come up with a common operation that depends on sequential access to a series of values, you should consider implementing it on top of Sequence too.

The requirements of the Sequence protocol are fairly small. All a conforming type must do is provide a makeIterator() method that returns an iterator:

protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol

    func makeIterator() -> Iterator
    // ...
}

We can learn two things from this (simplified) definition of Sequence: a Sequence has an associated Element type, and it knows how to make an iterator. So let’s first take a closer look at iterators.

Iterators

Sequences provide access to their elements by creating an iterator. The iterator produces the values of the sequence one at a time and keeps track of its own iteration state as it traverses through the sequence. The only method defined in the IteratorProtocol protocol is next(), which must return the next element in the sequence on each subsequent call, or nil when the sequence is exhausted:

protocol IteratorProtocol {
    associatedtype Element
    mutating func next() -> Element?
}

Most protocols don’t end in Protocol, but there are a few exceptions in the standard library: IteratorProtocol, StringProtocol, Keyed[En|De]CodingContainerProtocol, and Lazy[Collection|Sequence]Protocol. This is done to avoid name clashes with associated or concrete types that use the suffix-less names. The API Design Guidelines suggest that protocols should either be nouns or have a suffix of -able, -ible, or -ing, depending on the protocol’s role.

The associated Element type specifies the type of the values the iterator produces. For example, the element type of the iterator for String is Character. By extension, the iterator also defines its sequence’s element type. This is done through a constraint on Sequence’s associated Iterator type — Iterator.Element == Element ensures that both element types are the same:

protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol
       where Iterator.Element == Element
    // ...
}

You normally only have to care about iterators when you implement one for a custom sequence type. Other than that, you rarely need to use iterators directly, because a for loop is the idiomatic way to traverse a sequence. In fact, this is how a for loop works under the hood: the compiler creates a fresh iterator for the sequence and calls next on that iterator repeatedly until nil is returned. The for loop example we showed above is essentially shorthand for the following:

var iterator = someSequence.makeIterator()
while let element = iterator.next() {
    doSomething(with: element)
}

Iterators are single-pass constructs; they can only be advanced, and they can never be reversed or reset. To restart iteration, you create a new iterator (in fact, that’s exactly what Sequence allows through makeIterator()). While most iterators will produce a finite number of elements and eventually return nil from next(), nothing stops you from vending an infinite series that never ends. As a matter of fact, the simplest iterator imaginable — short of one that immediately returns nil — is one that just returns the same value over and over again:

struct ConstantIterator: IteratorProtocol {
    typealias Element = Int
    mutating func next() -> Int? {
        return 1
    }
}

The explicit type alias for Element is optional (but it’s often useful for documentation purposes, especially in larger protocols). If we omit it, the compiler infers the concrete type of Element from the return type of next():

struct ConstantIterator: IteratorProtocol {
    mutating func next() -> Int? {
        return 1
    }
}

Notice that the next() method is declared as mutating. This isn’t strictly necessary in this simplistic example because our iterator has no mutable state. In practice, though, iterators are inherently stateful. Almost any useful iterator requires mutable state to keep track of its position in the sequence.

We can create a new instance of ConstantIterator and loop over the sequence it produces in a while loop, printing an endless stream of ones:

var iterator = ConstantIterator()
while let x = iterator.next() {
    print(x)
}

Let’s look at a more elaborate example. FibsIterator produces the Fibonacci sequence. It keeps track of the current position in the sequence by storing the upcoming two numbers. The next method then returns the first number and updates the state for the following call. Like the previous example, this iterator also produces an “infinite” stream; it keeps generating numbers until it reaches integer overflow, and then the program crashes:

struct FibsIterator: IteratorProtocol {
    var state = (0, 1)
    mutating func next() -> Int? {
        let upcomingNumber = state.0
        state = (state.1, state.0 + state.1)
        return upcomingNumber
    }
}

Conforming to Sequence

An example of an iterator that produces a finite sequence is the following PrefixIterator, which generates all non-empty prefixes of a string (including the string itself). For example, for the string "abc", it generates "a", "ab", and "abc". It starts at the beginning of the string, and with each call of next, it increments the slice of the string it returns by one character until it reaches the end:

struct PrefixIterator: IteratorProtocol {
    let string: String
    var offset: String.Index

    init(string: String) {
        self.string = string
        offset = string.startIndex
    }

    mutating func next() -> Substring? {
        guard offset < string.endIndex else { return nil }
        string.formIndex(after: &offset)
        return string[..<offset]
    }
}

(string[..<offset] is a slicing operation that returns the substring between the start and the offset — we saw the partial range notation in the Built-In Collections chapter, and we’ll talk more about slicing later.)

With PrefixIterator in place, defining the accompanying PrefixSequence type is easy. Again, it isn’t necessary to specify the associated Iterator or Element types explicitly because the compiler can infer them from the return type of the makeIterator method:

struct PrefixSequence: Sequence {
    let string: String
    func makeIterator() -> PrefixIterator {
        return PrefixIterator(string: string)
    }
}

Now we can use a for loop to iterate over all the prefixes:

for prefix in PrefixSequence(string: "Hello") {
    print(prefix)
}
/*
 H
 He
 Hel
 Hell
 Hello
*/

Or we can perform any other operation provided by Sequence:

PrefixSequence(string: "Hello").map { $0.uppercased() }
// ["H", "HE", "HEL", "HELL", "HELLO"]

We can create sequences for ConstantIterator and FibsIterator in the same way. We’re not showing them here, but you may want to try this yourself. Just keep in mind that these iterators create infinite sequences. Use a construct like for i in fibsSequence.prefix(10) to slice off a finite piece.

Collections

Indices

Subsequences

Specialized Collections

Lazy Sequences

Recap

Error Handling

As programmers, we constantly have to deal with things going wrong: the network connection may go down, a file we expected to exist may not be there, and so on. Handling failures well is one of those intangible factors that distinguishes good programs from bad ones, and yet we often tend to see error handling as a subordinate task — something to be added later (and which then often gets cut as a deadline looms).

And we get it: error handling can be messy, and coding for the happy path is usually more fun. It’s all the more important then that a programming language provides a good error handling architecture that supports programmers in this task. Here are some of the things we think such an architecture should provide:

  • Conciseness. The code for throwing errors should not overwhelm the code for the happy path.

  • Propagation. An error should not have to be handled where it occurred. It’s often better if the logic for recovering from an error is far removed from the place where the error originated. An error handling architecture should make it easy to communicate errors up the call stack to the appropriate level. Bonus points for making it easy for intermediate functions (functions that call throwing functions, but neither throw nor handle errors themselves) to pass errors through without requiring big syntax changes.

  • Documentation. Make it easy for programmers to determine where errors can occur and possibly which errors can occur.

  • Safety. Make it impossible for programmers to ignore errors accidentally.

  • Universality. Specify a single error throwing and handling mechanism that can be used in all situations.

As we’ll see in this chapter, Swift’s native error handling architecture with throw, try, and catch scores 3.5 out of 5 on this scale — it doesn’t encode which errors a function can throw, and it isn’t universal because it doesn’t work well with callback-based asynchronous APIs. In comparison, the Result type, which is an alternative error handling approach, does have the capabilities throws is missing, but it fares somewhat worse on conciseness, propagation, and safety (only throwing functions require try).

Error Categories

The terms “error” and “failure” can mean all sorts of things. Let’s try to come up with some categories of “things that can go wrong,” differentiated by how we commonly handle them in code:

Expected errors are failures the programmer expects (or should expect) to happen during normal operation. These include things like network issues (a network connection is never 100 percent reliable), or when a string the user has entered is malformed. We can further segment expected errors by the complexity of the failure reason:

  • Trivial errors. Some operations have exactly one expected failure condition. For example, when you look up a key in a dictionary, the key is either present (success) or absent (failure). In Swift, we tend to return optionals from functions that have a single clear and commonly used “not found” or “invalid input” error condition. Returning a rich error value wouldn’t give the caller more information than what’s already present in the optional value.

    Assuming the failure reason is obvious to the caller, optionals perform very well in terms of conciseness (partly thanks to syntactic sugar for optionals), safety (we have to unwrap the value before we can use it), documentation (functions have optional return types), propagation (optional chaining), and universality (optionals are ubiquitous).

  • Rich errors. Networking and file system operations are examples of tasks that require more substantial error information than “something went wrong.” There are many different things that can fail in these situations, and programmers will regularly want to react differently depending on the type of failure (e.g. a program may want to retry a request when it times out but display an error to the user if a URL doesn’t exist). Errors of this type are the main focus of this chapter.

    While most failable standard library APIs return trivial errors (i.e. optionals), the Codable system uses rich errors. Encoding and decoding have many different error conditions, and precise error information is very valuable for clients to figure out what went wrong. The methods for encoding and decoding are annotated with throws to tell callers to prepare for handling errors.

Unexpected errors. A condition that the programmer didn’t anticipate occurred and that makes it difficult or impossible to continue. This usually means that an assumption the programmer made (“this can never happen”) turned out to be false. Examples where the standard library follows this pattern include accessing an array with an out-of-bounds index, creating a range with an upper bound that’s smaller than the lower bound, and dividing by zero.

The usual way to deal with an unexpected error in Swift is to let the program crash, because continuing with an unknown program state would be unsafe. Moreover, these situations are considered programmer errors that should be caught in testing — it would be inappropriate to handle them e.g. by displaying an error to the user.

In code, we use assertions (i.e. assert, precondition, or fatalError) to validate our expectations and trap if an assumption doesn’t hold. We looked at these functions in the Optionals chapter. Assertions are a great tool for identifying bugs in your code. Used correctly, they show you at the earliest possible moment when your program is in a state you didn’t expect. They’re also a useful documentation tool: every assert or precondition call makes the author’s (usually implicit) assumptions about program state visible to other readers of the code.

Assertions should never be used to signal expected errors — doing so would make graceful handling of these errors impossible because programs can’t recover from assertions. The opposite — using optionals or throwing functions to point out programmer errors — should also be avoided, because it’s better to catch a wrong assumption at the source than let it permeate through other layers of the program.

The Result Type

Throwing and Catching

Typed and Untyped Errors

Non-Ignorable Errors

Error Conversions

Chaining Errors

Errors in Asynchronous Code

Cleaning Up Using defer

Rethrowing

Bridging Errors to Objective-C

Recap

Encoding and Decoding

Serializing a program’s internal data structures into some kind of data interchange format and vice versa is one of the most common programming tasks. Swift calls these operations encoding and decoding.

The Codable system (named after its base “protocol,” which is really a type alias) is a standardized design for encoding and decoding data that all custom types can opt into. It’s designed around three central goals:

  • Universality — It should work with structs, enums, and classes.

  • Type safety — Interchange formats such as JSON are often weakly typed, whereas your code should work with strongly typed data structures.

  • Reducing boilerplate — Developers should have to write as little repetitive “adapter code” as possible to let custom types participate in the system. The compiler should generate this code for you automatically.

Types declare their ability to be (de-)serialized by conforming to the Encodable and/or Decodable protocols. Each of these protocols has just one requirement — Encodable defines an encode(to:) method in which a value encodes itself, and Decodable specifies an initializer for creating an instance from serialized data:

/// A type that can encode itself to an external representation.
public protocol Encodable {
    /// Encodes this value into the given encoder.
    public func encode(to encoder: Encoder) throws
}

/// A type that can decode itself from an external representation.
public protocol Decodable {
    /// Creates a new instance by decoding from the given decoder.
    public init(from decoder: Decoder) throws
}

Because most types that adopt one will also adopt the other, the standard library provides the Codable type alias as a shorthand for both:

public typealias Codable = Decodable & Encodable

All basic standard library types — including Bool, the number types, and String — are codable out of the box, as are optionals, arrays, dictionaries, sets, and ranges containing codable elements. Lastly, many common data types used by Apple’s frameworks — including Data, Date, URL, CGPoint, and CGRect — have adopted Codable.

Once you have a value of a codable type, you can create an encoder and tell it to serialize the value into the target format, e.g. JSON. In the reverse direction, a decoder takes serialized data and turns it back into an instance of the originating type. On the surface, the corresponding Encoder and Decoder protocols aren’t much more complex than Encodable and Decodable. The central task of an encoder or decoder is to manage a hierarchy of containers that store the serialized data. While you rarely have to interact with the Encoder and Decoder protocols directly unless you’re writing your own coders, understanding this structure and the three kinds of containers is necessary when you want to customize how your own types encode themselves. We’ll see many examples of this below.

A Minimal Example

The Encoding Process

The Synthesized Code

Manual Conformance

Common Coding Tasks

Recap

Interoperability

One of Swift’s strengths is the low friction when interoperating with C and Objective-C. Swift can automatically bridge Objective-C types to native Swift types, and it can even bridge with many C types. This allows us to use existing libraries and provide a nice interface on top.

In this chapter, we’ll create a wrapper around the C reference implementation of CommonMark. CommonMark is a formal specification for Markdown, a popular syntax for formatting plain text. If you’ve ever written a post on GitHub or Stack Overflow, you’ve probably used Markdown. After this practical example, we’ll take a look at the tools the standard library provides for working with memory, and we’ll see how they can be used to interact with C code.

Wrapping a C Library

Swift’s ability to call into C code allows us to take advantage of the abundance of existing C libraries. C APIs are often clunky, and memory management is tricky, but writing a wrapper around an existing library’s interface in Swift is often much easier and involves less work than building something from scratch; meanwhile, users of our wrapper will see no difference in terms of type safety or ease of use compared to a fully native solution. All we need to start is the dynamic library and its C header files.

Our example, the CommonMark C library, is a reference implementation of the CommonMark spec that’s both fast and well tested. We’ll take a layered approach in order to make CommonMark accessible from Swift. First we’ll create a very thin Swift class around the opaque types the library exposes. Then we’ll wrap this class with Swift enums in order to provide a more idiomatic API.

An Overview of Low-Level Types

Closures as C Callbacks

Recap

Final Words

We hope you enjoyed this journey through Swift with us.

Despite its young age, Swift is already a complex language. It’d be a daunting task to cover every aspect of it in one book, let alone expect readers to remember it all. But even if you don’t immediately put everything you’ve learned to practical use, we’re confident that having a better understanding of your language makes you a more accomplished programmer.

If you take one thing away from this book, we hope it is that Swift’s many advanced aspects are there to help you write better, safer, and more expressive code. While you can write Swift code that feels not much different from Objective-C, Java, or C#, we hope to have convinced you that features like enums, generics, and first-class functions can greatly improve your code.

Swift 5’s headline feature was reaching a stable ABI (application binary interface) on Apple platforms. Going forward, any library compiled with Swift 5 will be usable from future versions of Swift, and the runtime and standard library are now included in the OS, rather than within each app bundle.

Nailing down all the internal data formats that are required for a stable (and futureproof) ABI took up a lot of the Swift team’s resources over the past few years. Now that this work is done, we can expect a renewed focus on improving the public-facing parts of Swift. Not having to worry about keeping the binary size of the standard library small (because it’s now shared among all processes, both on disk and in memory) also opens up the potential for adding significantly more functionality to the standard library.

Swift is still improving rapidly. While the era of large-scale source-breaking changes is behind us, there are several areas where we expect major enhancements in the coming years:

  • An explicit memory ownership model will allow developers to annotate function arguments with ownership requirements. The goal is to give the compiler all the information it needs to avoid unnecessary copies when passing values to functions. We’re already seeing the first pieces of this with the gradual introduction of compiler-enforced exclusive memory access in Swift 4 and 5.

  • Discussions about adding first-class concurrency support to Swift are still in their infancy, and this is a project that will take several years to complete. Still, there’s a good chance we’ll get something like the coroutine-based async/await model that’s popular in other languages in the not-too-distant future.

  • The compiler bakes a lot of metadata about types and their properties into the binaries. This information is already being used by debugging tools, but there aren’t any public APIs to access it yet. The existence of this data opens the door for more powerful reflection and introspection capabilities that go way beyond what the current Mirror type can do.

If you’re interested in shaping how these and other features turn out, remember that Swift is being developed in the open. Consider joining the Swift Forums and adding your perspective to the discussions.

Finally, we’d like to encourage you to take advantage of the fact that Swift is open source. When you have a question the documentation doesn’t answer, the source code can often give you the answer. If you made it this far, you’ll have no problem finding your way through the standard library source files. Being able to check how things are implemented in there was a big help for us when writing this book.