Swift Gems

Download as pdf or txt
Download as pdf or txt
You are on page 1of 206

Swift Gems

100+ tips to take your Swift code


to the next level

Natalia Panferova
2024
ii
Contents

Introduction 1

Pattern matching and control flow 3


Match a single case of an enum with if case let . . . . . . . . . . . . 4
Explicitly handle potential future enum cases . . . . . . . . . . . . . 5
Match continuous data without precise boundaries using one-sided
ranges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Overload the pattern matching operator for custom matching behavior 9
Combine switch statements with tuples for complex conditions . . . 11
Switch on multiple optional values simultaneously . . . . . . . . . . 12
Simplify optional unwrapping with the new shorthand syntax . . . . 13
Modify unwrapped optionals directly within the if statement block . 14
Leverage map to transform optional values . . . . . . . . . . . . . . 15
Execute loop operations on non-nil elements . . . . . . . . . . . . . 16
Set a constant based on conditions . . . . . . . . . . . . . . . . . . 17
Iterate over items and indices in collections . . . . . . . . . . . . . . 19
Filter elements within the loop structure using where clause . . . . . 21
Label loop statements to control execution of nested loops . . . . . . 23

Functions, methods, and closures 25


Pass a varying number of input values to a function . . . . . . . . . 26
Define a set of configurations with OptionSet . . . . . . . . . . . . . 27
Enable direct modification of function arguments . . . . . . . . . . 29
Optimize function arguments with autoclosures . . . . . . . . . . . 31
Handle different parameter types or numbers with method overloads 33
Ignore return values without generating a warning . . . . . . . . . . 35
Indicate to the compiler that a function will never return . . . . . . . 37

iii
iv CONTENTS

Simplify generic signatures using opaque types . . . . . . . . . . . . 39


Assign descriptive names to complex closure types with typealias . . 41
Utilize implicit self references in closures . . . . . . . . . . . . . . . 43
Embrace higher-order functions to enhance code flexibility . . . . . 45
Optimize error handling in higher-order functions with rethrows . . 47

Custom types: structs, classes, enums 49


Enhance your types with custom string representation . . . . . . . . 50
Allow custom types to be initialized directly with literal values . . . . 52
Preserve memberwise initializer when adding custom inits to structs 54
Bind custom types to specific raw values . . . . . . . . . . . . . . . 56
Transform your types into callable entities with callAsFunction() . . 58
Leverage Comparable to create ranges from custom types . . . . . . 60
Implement copy-on-write for efficient memory usage . . . . . . . . 62
Prevent misuse of irrelevant inherited functionalities in subclasses . 65
Model hierarchical data with recursive enumerations . . . . . . . . . 67
Simplify enum comparisons with automatic Comparable conformance 69
Generate a collection of all cases in an enum . . . . . . . . . . . . . 71
Utilize enum cases as factory methods . . . . . . . . . . . . . . . . 73

Advanced property management strategies 75


Set dynamic default values for properties using closures . . . . . . . 76
Avoid unnecessary initialization of resource-heavy properties . . . . 78
Leverage computed properties to synchronize related data . . . . . . 80
Activate property observers during initialization . . . . . . . . . . . 82
Prevent unauthorized modifications of properties with private(set) . 84
Provide default property values in protocol extensions . . . . . . . . 86
Perform asynchronous operations within property getters . . . . . . 88
Throw errors in property getters to manage failures . . . . . . . . . 91
Handle mutable static properties in generic types . . . . . . . . . . . 94
CONTENTS v

Encapsulate property-specific behavior in property wrappers . . . . 96

Protocols and generics 99


Limit protocol adoption to class types . . . . . . . . . . . . . . . . . 100
Define type-specific protocol requirements with the Self keyword . . 102
Build versatile protocols with associated types . . . . . . . . . . . . 104
Provide default method implementations in protocol extensions . . . 106
Constrain protocol extensions to types that meet specific criteria . . 108
Instantiate protocol-conforming types using static factory methods . 110
Implement hierarchical interfaces with protocol inheritance . . . . . 112
Conditionally add methods to generic types in extensions . . . . . . 114
Accurately identify the dynamic type of values in generic contexts . . 115
Create clear semantic distinctions with phantom types . . . . . . . . 117

Collection transformations and optimizations 119


Sort arrays using comparison operators as closures . . . . . . . . . . 120
Change a range of array values in a single operation . . . . . . . . . 122
Simplify array mapping with key paths . . . . . . . . . . . . . . . . 124
Instantiate arrays with a specified capacity without default values . . 125
Specify default values in dictionary subscripts . . . . . . . . . . . . 126
Refine dictionary data retrieval with type-safe generic subscripts . . 127
Safely modify dictionary values with optional chaining . . . . . . . . 129
Capture previous values on dictionary updates . . . . . . . . . . . . 130
Transform dictionary values efficiently with mapValues() . . . . . . 132
Extend collections whose elements conform to certain protocols . . . 133
Expose collection data through custom read-only subscripts . . . . . 135
Accumulate collection elements into a single value in a memory-
efficient way . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Leverage lazy collections for efficient use of resources . . . . . . . . 138

String manipulation techniques 141


vi CONTENTS

Manipulate strings using string indices . . . . . . . . . . . . . . . . 142


Avoid unwanted newlines in multiline strings . . . . . . . . . . . . . 144
Bypass the need to escape special characters using raw strings . . . . 145
Craft visually expressive strings with emoji . . . . . . . . . . . . . . 146
Take advantage of unicode normalization for string comparisons . . 148
Utilize dynamic string concatenation techniques . . . . . . . . . . . 149
Define custom string interpolation behavior . . . . . . . . . . . . . 150
Leverage collection capabilities to transform strings . . . . . . . . . 152

Asynchronous programming and error handling 153


Bridge async/await and completion handlers . . . . . . . . . . . . . 154
Run main-actor-isolated functions within the main dispatch queue . 156
Allow execution of other tasks during long operations using yield() . 158
Utilize task cancellation mechanisms to stop unnecessary work . . . 159
Manage the execution priority of concurrent tasks . . . . . . . . . . 161
Delay asynchronous tasks using modern clock APIs . . . . . . . . . 163
Handle errors in asynchronous code . . . . . . . . . . . . . . . . . 164
Rethrow errors with added context . . . . . . . . . . . . . . . . . . 166
Explicitly define the scope of try with infix operators . . . . . . . . . 169
Transform success or error values of the Result type . . . . . . . . . 171
Convert Result into a throwing expression . . . . . . . . . . . . . . 173

Logging and debugging 175


Catch logical errors in development phase with assertions . . . . . . 176
Enrich debug logs with contextual information . . . . . . . . . . . . 177
Implement custom nil-coalescing for optionals in debug prints . . . 179
Customize debug output with CustomDebugStringConvertible . . . . 180
Introspect the properties and values of instances at runtime . . . . . 182
Utilize the dump() function for in-depth debugging . . . . . . . . . 184

Code organization 187


CONTENTS vii

Facilitate access to shared resources using type subscripts . . . . . . 188


Encapsulate context-specific utilities within a parent type or extension190
Organize related functionalities using enums as namespaces . . . . . 192
Draw attention to code that needs review or modification . . . . . . 193
Tackle framework availability with compiler directives . . . . . . . . 194
Disambiguate standard library types from custom declarations . . . 196
viii CONTENTS
Introduction

Thank you so much for purchasing a copy of “Swift Gems”. This book con-
tains a collection of concise, easily digestible tips and techniques designed for
experienced developers looking to advance their Swift expertise.

As an enthusiastic Swift developer with extensive experience in creating robust


applications across multiple platforms, I have had the privilege to witness
and contribute to the evolution of Swift. This journey has provided me with
invaluable insights and advanced techniques that I am eager to share with the
community. Swift’s versatility allows it to be used effectively for both small-
scale applications and complex enterprise systems, making it a top choice
for developers looking to push the boundaries of what’s possible in software
development.

Recognizing that many experienced developers are constantly seeking ways to


refine their skills and expand their toolkit, this book is designed to serve as a
resource for enhancing the quality and efficiency of your Swift code. Whether
you are developing for iOS, macOS, watchOS, or tvOS, or even writing Swift on
the server, the tips and techniques compiled here will provide you with new
perspectives and approaches to tackle everyday coding challenges and innovate
in your projects.

“Swift Gems” aims to bridge the gap between intermediate knowledge and ad-
vanced mastery, offering a series of easily implementable, bite-sized strategies
that can be directly applied to improve your current projects. Each chapter is
crafted with precision, focusing on a specific aspect of Swift development, from
pattern matching and asynchronous programming to data management and
beyond. Practical examples and concise explanations make complex concepts
accessible and actionable.

Furthermore, to ensure that you can immediately put these ideas into practice,
each concept is accompanied by code snippets and examples that illustrate how

1
2 INTRODUCTION

to integrate these enhancements effectively. The book is structured to facilitate


quick learning and application, enabling you to integrate advanced features
and optimize your development workflow efficiently.

Whether you’re looking to optimize performance, streamline your coding pro-


cess, or simply explore new features in Swift, this book will provide you with
the tools and knowledge necessary to elevate your coding skills and enhance
your development projects.

The content of the book is copyright Nil Coalescing Limited. You can’t share
or redistribute it without a prior written permission from the copyright
owner. If you find any issues with the book, have any general feedback
or have suggestions for future updates, you can send us a message to
[email protected]. I will be updating the book when there are
changes to the APIs and to incorporate important feedback from readers. You
can check back on books.nilcoalescing.com/swift-gems to see if there have
been new releases and view the release notes.

I hope you enjoy the book!


Pattern matching and control flow

Let’s explore some useful techniques that can be applied in pattern matching
and control flow in Swift.

We’ll delve into the powerful capabilities of Swift’s pattern matching, a feature
that goes beyond the basic switch-case structure familiar in many program-
ming languages. We’ll look into how to use various Swift constructs to elegantly
handle conditions and control flows, including working with enums and op-
tional values. We’ll see how to perform operations on continuous data ranges
and manage optional values with precision. The techniques we’ll cover can
simplify our code and enhance its robustness and readability.

This chapter is designed for experienced Swift developers who are looking to
refine their skills in efficiently managing program flow and handling data based
on specific patterns. By mastering these techniques, you will be equipped to
write cleaner, more expressive, and more efficient Swift code, harnessing the
full potential of pattern matching and advanced control flow to tackle real-world
programming challenges more effectively.

3
4 PATTERN MATCHING AND CONTROL FLOW

Match a single case of an enum with if case let


The if case let syntax provides a concise way to extract values from an enum
case for further processing. It’s a great alternative to the switch statement
when dealing with just one case.

Let’s consider an enum Activity with multiple cases.


enum Activity {
case blogging(String)
case running(Int)
case talking(String)
case sleeping
}

Each case of Activity can hold different types of associated values.

If we want to check if the activity is blogging and also capture the associated
blog topic, we can do it in the following way with if case let.
let currentActivity = Activity.blogging("Swift Enums")

if case let .blogging(topic) = currentActivity {


print("Blogging about \(topic)")
} else {
print("Not blogging")
}

In our example, if case let checks if currentActivity is of the blogging case


and binds the associated value to the topic. If the match is successful, it executes
the code inside the block.

Using if case let is particularly beneficial when we are only interested in


one case and want to avoid the verbosity of a switch statement. It makes the
intention of our code clearer when dealing with a single case.
PATTERN MATCHING AND CONTROL FLOW 5

Explicitly handle potential future enum cases

Enums in Swift are heavily used to represent a fixed set of related values. How-
ever, when enums are likely to evolve over time, such as those exposed by
libraries or frameworks, it becomes crucial to handle new cases effectively.
Swift’s @unknown default is an enhanced version of the traditional default
case in switch statements, designed specifically to improve safety and main-
tainability when dealing with such evolving enums.

The @unknown default keyword provides a mechanism to handle future enum


cases that aren’t currently recognized by our code. Unlike a regular default
case, which passively covers all unspecified cases, @unknown default actively
prompts the compiler to issue warnings when new enum cases are introduced
and not explicitly handled. This feature is essential for keeping our code robust
and future-proof, especially when using enums from external libraries that may
be updated independently of our application.

Consider using @unknown default with the Calendar.Component enum, which


could be expanded with new components in future versions of Foundation.

import Foundation

func displayName(for component: Calendar.Component) -> String {


switch component {
case .year:
return "Year"
case .month:
return "Month"
case .day:
return "Day"
// Explicitly handle other known cases...
@unknown default:
// Log and handle any unexpected cases gracefully
print("A new Calendar.Component case has been introduced.")
return "Unknown Component"
}
}

In this implementation, each known case is handled explicitly, providing clear


and specific functionality. The @unknown default case serves as a safety net,
6 PATTERN MATCHING AND CONTROL FLOW

ensuring that the function remains operational even when new enum cases are
added, and prompting a review of the handling code.

The compiler warnings triggered by @unknown default help developers identify


unhandled cases promptly after libraries or the Swift language itself is updated.
This proactive feature significantly aids in maintaining source compatibility
and functionality.

Using @unknown default in Swift is a best practice for handling enums from
external sources or even within large projects where enums might evolve over
time. It offers a strategic advantage by alerting developers to changes that could
impact the application, thereby fostering code that is both safe and adaptable
to future updates.
PATTERN MATCHING AND CONTROL FLOW 7

Match continuous data without precise boundaries using one-


sided ranges

Swift’s pattern matching capabilities allow us to write clear and concise code,
particularly useful when handling continuous or broad sets of data. A practical
example of this is using one-sided ranges in switch statements, which effectively
matches a wide range of values without the need to specify a precise endpoint.
This approach is especially beneficial in scenarios where the data can vary
significantly but still needs to be categorized distinctly.

Consider the case of developing a weather application where we need to provide


clothing recommendations based on temperature. Here’s how we might im-
plement this using one-sided ranges in a switch statement. For simplicity in
this example we’ll just have an integer for the temperature and assume it’s in
degrees Celsius.

let temperature = 28

switch temperature {
case ..<0:
print("It's freezing! Wear a heavy coat.")
case 0..<15:
print("It's cool. Wear a jacket.")
case 15..<25:
print("Mild weather. A sweater should be fine.")
case 25...:
print("It's hot! T-shirt weather.")
default:
print("Temperature out of range.")
}

In our code, ..<0 matches any temperature below 0 degrees Celsius, efficiently
handling the lower boundary of our range. Similarly, 25... covers any tem-
perature above 25 degrees Celsius, ensuring that higher temperatures are also
addressed without explicitly setting an upper limit.

Using one-sided ranges like this not only simplifies the code but also ensures
comprehensive coverage of possible values, minimizing the risk of gaps in the
logic. This technique proves particularly useful in applications dealing with
8 PATTERN MATCHING AND CONTROL FLOW

ranges of values such as temperatures, scores, or ages, where each category


can span a broad interval and precise boundaries might not be necessary or
practical.
PATTERN MATCHING AND CONTROL FLOW 9

Overload the pattern matching operator for custom matching


behavior
Swift’s pattern matching is an exceptionally flexible technique predominantly
used in switch statements to accommodate a wide range of patterns. In this
context, an expression pattern within a switch case represents the value of
an expression. The core of this functionality hinges on the pattern matching
operator (~=), which Swift utilizes behind the scenes to assess whether a pattern
corresponds with the value. Typically, ~= performs comparisons between two
values of the same type using ==. However, the pattern matching operator can
be overloaded to enable custom matching behaviors, offering enhanced control
and adaptability in how data is evaluated and handled.

Let’s consider a custom type Circle and demonstrate how to implement custom
pattern matching for it. We’ll define a simple Circle struct and overload the
~= operator to match a Circle with a specific radius. This overload will allow
us to use a Double in a switch statement case to match against a Circle.
struct Circle {
var radius: Double
}

func ~= (pattern: Double, value: Circle) -> Bool {


return value.radius == pattern
}

let myCircle = Circle(radius: 5)

switch myCircle {
case 5:
print("Circle with a radius of 5")
case 10:
print("Circle with a radius of 10")
default:
print("Circle with a different radius")
}

We can add as many overloads as we need. For example, we can define custom
logic to check whether the Circle’s radius falls within a specified range. The
switch statement will now be able to match myCircle against Double values
10 PATTERN MATCHING AND CONTROL FLOW

and ranges, thanks to our custom implementations of the ~= operator.


func ~= (pattern: ClosedRange<Double>, value: Circle) -> Bool {
return pattern.contains(value.radius)
}

switch myCircle {
case 0:
print("Radius is 0, it's a point!")
case 1...10:
print("Small circle with a radius between 1 and 10")
default:
print("Circle with a different radius")
}

Custom pattern matching in Swift opens up a lot of possibilities for handling


complex types more elegantly. By overloading the ~= operator, we can tailor
the pattern matching process to suit our custom types. As with any powerful
tool, we should use it wisely to enhance our code without compromising on
readability.
PATTERN MATCHING AND CONTROL FLOW 11

Combine switch statements with tuples for complex conditions


Swift’s pattern matching capabilities, especially when combined with tuples in
switch statements, offer a powerful way to simplify complex conditional logic
while enhancing code readability. This approach allows for handling multiple
variables and conditions in a single, concise statement.

Consider a scenario in a game where player actions depend on both their energy
level and the time of day. Instead of evaluating these conditions separately, we
can create a tuple right within the switch statement to check both simultan-
eously.
let energy = 80
let timeOfDay = "evening"

switch (energy, timeOfDay) {


case (80...100, "morning"):
print("You're full of energy! Great time for a workout.")
case (50...79, "morning"):
print("You're doing okay. Maybe a light jog?")
case (0...49, "morning"):
print("Take it easy, maybe stretch a little.")
case (80...100, "evening"):
print("You're full of energy! Perfect for some evening training.")
case (50...79, "evening"):
print("You have some energy left. How about some yoga?")
case (0...49, "evening"):
print("Not much energy left. Time to rest.")
default:
print("Check your energy level and time of day, something's off.")
}

In this example, by combining the energy level and timeOfDay into a tuple
directly within the switch statement, we can streamline the process of determ-
ining the appropriate action for the player. This method significantly reduces
the complexity of handling multiple conditions, making the decision-making
process in our code more straightforward and easier to manage.
12 PATTERN MATCHING AND CONTROL FLOW

Switch on multiple optional values simultaneously


Utilizing tuple patterns in switch statements offers a robust way to handle
multiple optional values simultaneously, allowing for clean and concise man-
agement of various combinations of those values.

Consider a scenario where we have two optional integers, optionalInt1 and


optionalInt2. Depending on their values, we might want to execute different
actions. Here’s how we can use a tuple pattern to elegantly address each possible
combination of these optional integers.
var optionalInt1: Int? = 1
var optionalInt2: Int? = nil

switch (optionalInt1, optionalInt2) {


case let (value1?, value2?):
print("Both have values: \(value1) and \(value2)")
case let (value1?, nil):
print("First has a value: \(value1), second is nil")
case let (nil, value2?):
print("First is nil, second has a value: \(value2)")
case (nil, nil):
print("Both are nil")
}

In this example, the switch statement checks the tuple (optionalInt1,


optionalInt2). The first case matches when both elements in the tuple are
non-nil. Here, each value is unwrapped and available for use within the case
block. The second and third cases handle the scenarios where one of the
optionals is nil, and the other contains a value. The final case addresses the
situation where both optionals are nil, allowing us to define a clear action for
this scenario.

By structuring the switch this way, each combination of presence and absence
of values is handled explicitly, making the code both easier to follow and main-
tain. This method significantly reduces the complexity that typically arises from
nested if-else conditions and provides a straightforward approach to branching
logic based on multiple optional values.
PATTERN MATCHING AND CONTROL FLOW 13

Simplify optional unwrapping with the new shorthand syntax


In earlier versions of Swift, unwrapping optionals often introduced redundancy,
especially when using the if let syntax. Traditionally, to unwrap an optional
and check for its presence simultaneously, we had to declare a new variable
with the same name as the optional in our conditional statement. For instance,
with the old syntax, we would repeat bookTitle twice.
var bookTitle: String?

if let bookTitle = bookTitle {


print("The title of the book is \(bookTitle)")
}

This redundancy can clutter code, especially in more complex conditional


checks.

Swift 5.7 introduced a shorthand syntax for optional unwrapping that elimin-
ates this redundancy. Now, we can omit the right-hand side of the assignment
in the if let statement when the unwrapped variable is intended to have the
same name as the optional.

The updated syntax looks like this.


var bookTitle: String?

if let bookTitle {
print("The title of the book is \(bookTitle)")
}

This shorthand syntax is applicable not only in if let statements but also with
guard let and while let, making it a versatile improvement across different
types of flow control structures. By eliminating the need to repeat the variable
name, this feature helps in reducing boilerplate code and enhancing the overall
readability.
14 PATTERN MATCHING AND CONTROL FLOW

Modify unwrapped optionals directly within the if statement


block
When working with optionals in Swift, unwrapping them safely is a common
task. The if let syntax is widely used for this purpose. However, Swift also
allows the use of var instead of let in these constructs. This enables us to not
only unwrap the optional but also mutate the unwrapped value directly within
the control flow block.

Using var in this way makes the variable mutable inside the block, but since
optionals are value types in Swift, the mutation won’t affect the original variable
outside the block. This behavior is crucial to understand to prevent unexpected
side effects.

Here’s an example to illustrate the use of var with an optional in an if state-


ment.
var numberString: String = "2"

if var number = Int(numberString) {


number *= number

// Prints `Squared value: 4`


print("Squared value: \(number)")
}

// Here, 'number' is not accessible outside the 'if' block

This approach can be particularly useful when we need to perform temporary,


isolated modifications to a value coming from an optional, without affecting
the original state of our data structures.
PATTERN MATCHING AND CONTROL FLOW 15

Leverage map to transform optional values


One of the lesser-known ways to unwrap an optional value in Swift is to use the
map() method. This method provides a succinct and expressive approach to
transforming optional values without the need for verbose conditional checks.

Consider a scenario where we need to create a URLRequest from a string repres-


entation of a URL. Since initializing a URL from a string can fail, returning nil if
the string is not valid, the map() method becomes particularly useful.
func createURLRequest(from urlString: String) -> URLRequest? {
URL(string: urlString).map { URLRequest(url: $0) }
}

In the example above, the function attempts to create a URL object from a string,
which results in an Optional<URL>. When map() is called on this optional URL,
it executes a provided closure if, and only if, the URL isn’t nil. The closure
{ URLRequest(url: $0) } is used to construct a URLRequest from the non-nil
URL.

The map() method executes the transformation closure only when the optional
contains a value. If URL(string: urlString) is nil, map() skips the closure
execution and returns nil.

The method reduces the need for explicit unwrapping using if let or guard
let, making the code less verbose and more straightforward.
16 PATTERN MATCHING AND CONTROL FLOW

Execute loop operations on non-nil elements


When working with arrays of optional values in Swift, it’s common to encounter
situations where we only want to execute loop operations on non-nil elements.
Using case let in a for-in loop is an efficient and elegant way to avoid the need
for nested conditionals or explicit unwrapping within the loop body, making
our code cleaner and more efficient. This technique leverages Swift’s pattern
matching capabilities to selectively execute the loop body only for non-nil
elements, automatically unwrapping them in the process.

Here’s a practical example to illustrate it.


let optionalNames: [String?] = [
"Alice", nil, "Bob", "Charlie", nil, "Diana"
]

for case let name? in optionalNames {


// This code will only execute for non-nil elements
print(name)
}

In the above snippet, for case let name? in optionalNames iterates over
each element in optionalNames. The case let name? pattern matches only
non-nil values and unwraps them, assigning the unwrapped value to name. The
loop body is executed only for these non-nil, unwrapped values.

This method is especially useful for bypassing nil values in an array without
needing a separate conditional check inside the loop. It not only simplifies
the code by reducing the need for explicit unwrapping and nil checks but also
enhances readability, making it clear that the loop only concerns itself with valid,
non-nil data. This approach is a testament to Swift’s powerful pattern matching
framework, providing a succinct and expressive way to handle optional values
within collections.
PATTERN MATCHING AND CONTROL FLOW 17

Set a constant based on conditions

Typically, when we declare a variable with var, we can set or change its value
at a later point in our code. With let, however, we need to be more deliberate.
Swift’s compiler enforces a strict rule: every constant must be initialized before
use, and in every possible code path. This might seem restrictive, but it’s a
powerful feature that prevents runtime errors and ensures our code behaves
predictably.

Here’s a common scenario: we want to set a constant based on conditions,


such as the result of a function or a switch statement. How do we do this
without declaring the constant as a var and assigning an arbitrary default
value? The answer lies in understanding Swift’s control flow and ensuring that
every possible path of execution provides a value for the constant.

Consider the following example where we want to set a let constant based on
a condition.
func weatherNotification(for temperature: Int) -> String {
let message: String

if temperature > 30 {
message = "It's hot outside."
} else if temperature < 0 {
message = "Freezing temperatures!"
} else {
message = "Mild weather."
}

let detailedMessage = message + " Take necessary precautions."


return detailedMessage
}

In this example, message is a constant whose value depends on the temperature.


Notice how message is set in every branch of the if-else statement. This
satisfies the compiler’s requirement that message must have a value no matter
what the outcome of the temperature check is.

In more complex scenarios, especially when dealing with switch statements


or nested conditions, ensuring that every code path sets the constant can be
18 PATTERN MATCHING AND CONTROL FLOW

challenging. However, Swift’s compiler is our ally here. It will refuse to compile
code if it detects any path where the constant might not be set.
PATTERN MATCHING AND CONTROL FLOW 19

Iterate over items and indices in collections

Iterating over both the items and their indices in a collection is a common re-
quirement in Swift programming. While the enumerated() method might seem
like the obvious choice for this task, it’s crucial to understand its limitations.
The integer it produces starts at zero and increments by one for each item,
which is perfect for use as a counter but not necessarily as an index, especially
if the collection isn’t zero-based or directly indexed by integers.

Here’s a typical example using enumerated().


var ingredients = ["potatoes", "cheese", "cream"]

for (i, ingredient) in ingredients.enumerated() {


// The counter helps us display the sequence number, not the index
print("ingredient number \(i + 1) is \(ingredient)")
}

For a more accurate way to handle indices, especially when working with
collections that might be subsets or have non-standard indexing, we can use
the zip() function. This method pairs each element with its actual index, even
if the collection has been modified.
// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]

// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()

for (i, ingredient) in zip(


doubleIngredients.indices, doubleIngredients
) {
// Correctly use the actual indices of the subsequence
doubleIngredients[i] = "\(ingredient) x 2"
}

This approach ensures that we are using the correct indices corresponding to
the actual positions in the modified collection.

For a better interface over zip(), we can also use the indexed() method
from Swift Algorithms. It’s equivalent to zip(doubleIngredients.indices,
doubleIngredients) but might be more clear.
20 PATTERN MATCHING AND CONTROL FLOW

import Algorithms

// Array<String>
var ingredients = ["potatoes", "cheese", "cream"]

// Array<String>.SubSequence
var doubleIngredients = ingredients.dropFirst()

for (i, ingredient) in doubleIngredients.indexed() {


// Do something with the index
doubleIngredients[i] = "\(ingredient) x 2"
}
PATTERN MATCHING AND CONTROL FLOW 21

Filter elements within the loop structure using where clause

To selectively iterate over elements that meet specific conditions within a for-
in loop, Swift provides the where clause, a powerful tool for filtering elements
directly within the loop structure. When iterating through a collection or
sequence, the where clause enables us to filter the elements and iterate only over
those that satisfy particular criteria. This allows us to process or manipulate
the desired subset of elements within the loop.

For example, let’s consider a TransportationEvent enum representing different


types of events in a transportation system.
enum TransportationEvent {
case busArrival(
busNumber: Int,
passengers: Int
)
case trainArrival(
trainNumber: Int,
passengers: Int,
cargoLoad: Int
)
case bicycleArrival
}

Suppose we have an array of transportation events, and we want to iterate over


only the busArrival events where the number of passengers is greater than 10.
We can achieve this with the following code.
let transportationEvents: [TransportationEvent] = [
.busArrival(busNumber: 1, passengers: 5),
.busArrival(busNumber: 2, passengers: 15),
.trainArrival(trainNumber: 10, passengers: 50, cargoLoad: 100),
.busArrival(busNumber: 3, passengers: 20),
.bicycleArrival
]

for case let .busArrival(busNumber, passengers)


in transportationEvents where passengers > 10 {
print("Bus \(busNumber) arrived with \(passengers) passengers.")
}

We use the pattern case let .busArrival(busNumber, passengers) within


22 PATTERN MATCHING AND CONTROL FLOW

the for-in loop to match elements of the transportationEvents array that


are of type busArrival. The where clause then filters these matched events
further, ensuring that the loop body is executed only for bus arrivals where the
passenger count exceeds 10.

The where clause effectively reduces the need for additional if statements
inside the loop, making our code cleaner and more readable. It allows us to
directly specify the conditions for the elements we want to process, improving
the clarity and efficiency of our data handling.
PATTERN MATCHING AND CONTROL FLOW 23

Label loop statements to control execution of nested loops

One of the lesser-known yet incredibly useful features in Swift is the concept
of named loops. This feature enhances the control flow in our code, making
complex loop structures more manageable and readable.

Named loops are not a separate type of loop but rather a way of labeling loop
statements. In Swift, we can assign a name to loops (for, while, or repeat-
while) and use this name to specifically control the flow of the program. This
is particularly useful when dealing with nested loops and we need to break out
of or continue an outer loop from within an inner loop.

The syntax for naming a loop is straightforward. We simply precede the loop
with a label followed by a colon. Let’s consider a practical example. Suppose
we are working with a two-dimensional array and want to search for a specific
value. Once the value is found, we’d typically want to break out of both loops.
Here’s how we can do it with named loops.
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]

let valueToFind = 5
searchValue: for row in matrix {
for num in row {
if num == valueToFind {
print("Value \(valueToFind) found!")
break searchValue
}
}
}

In this example, searchValue is the label for the outer loop. When valueToFind
is found, break searchValue terminates the outer loop, preventing unnecessary
iterations.

In nested loops, it’s often unclear which loop the break or continue statement is
affecting. Named loops remove this ambiguity, making our code more readable
24 PATTERN MATCHING AND CONTROL FLOW

and maintainable.
Functions, methods, and closures

Let’s delve into the world of functions, methods, and closures in Swift.

In this chapter, we explore the sophisticated mechanisms of Swift’s functions


and closures, key to crafting flexible and reusable code. You’ll learn about
enhancing function definitions, handling complex closures, and using advanced
techniques to make your functions more expressive and robust. We will also
touch on how certain features can simplify your code’s structure and increase
its maintainability, such as using OptionSet for configuration or employing
type aliases to clarify complex closures.

Designed for experienced Swift developers, this chapter aims to deepen your
understanding of function and closure mechanisms. By mastering these ad-
vanced techniques in functions, methods, and closures, you will significantly
enhance the flexibility and efficiency of your Swift code.

25
26 FUNCTIONS, METHODS, AND CLOSURES

Pass a varying number of input values to a function


Swift provides a convenient way to handle functions that require an indefinite
number of arguments through the use of variadic parameters. These para-
meters allow you to pass a flexible number of inputs by appending three dots
(...) after the parameter type, effectively converting these inputs into an array
within the function’s scope. This feature is invaluable for functions that need
to process a series of items where the count might not be known in advance.

Here’s an example to demonstrate variadic parameters.


func printNumbers(numbers: Int...) {
for number in numbers {
print(number)
}
}

printNumbers(numbers: 1, 2, 3)

In the code above, the printNumbers() function is capable of accepting any


number of integer arguments. Inside the function, these integers are accessible
as an array named numbers, which the function iterates over to print each
number. This technique is versatile and not limited to integers but can be
applied to any data type, allowing for broad functionality.

Variadic parameters are typically placed at the end of a function’s parameter list
to ensure clarity in function calls, especially when the function accepts various
types of parameters. This strategic placement helps maintain the readability
and usability of your function calls, making it straightforward to pass multiple
arguments seamlessly.
FUNCTIONS, METHODS, AND CLOSURES 27

Define a set of configurations with OptionSet

In Swift, managing a group of related configuration options for a method can


be elegantly handled using the OptionSet. OptionSet is a protocol used to
represent a collection of options. Each option is a unique value, and multiple
options can be combined. It’s particularly useful for configurations where
options are not mutually exclusive.

Imagine we are writing a function to fetch data from a server. We might have
several options for this fetch operation, like whether to use cache, whether to
retry on failure, or whether to run the operation in the background.

First, let’s define an OptionSet to represent these options.


struct FetchOptions: OptionSet {
let rawValue: Int

static let useCache = FetchOptions(rawValue: 1 << 0)


static let retryOnFailure = FetchOptions(rawValue: 1 << 1)
static let background = FetchOptions(rawValue: 1 << 2)
}

Each option is assigned a unique value, a power of two, which allows them to
be uniquely combined using bitwise operations without overlap.

Now, we can use this OptionSet in our method definition. When calling the
method, we can specify a single option or a combination of options.
func fetchData(options: FetchOptions) {
if options.contains(.useCache) {
// Implement cache logic
}
if options.contains(.retryOnFailure) {
// Implement retry logic
}
if options.contains(.background) {
// Implement background execution logic
}

// Rest of the fetch operation


}

fetchData(options: [.useCache, .retryOnFailure])


28 FUNCTIONS, METHODS, AND CLOSURES

Using an OptionSet to pass configurations to a method can make our code


more readable and expressive. We can easily combine different options without
cluttering our method signatures with multiple Boolean parameters.
FUNCTIONS, METHODS, AND CLOSURES 29

Enable direct modification of function arguments

Managing data mutation within functions often requires careful consideration.


Sometimes, it’s not sufficient to pass values into a function and have it operate
on them independently. Instead, there are situations where you need the
function to directly modify the original variables passed to it.

In Swift development, managing mutable state within functions can be eleg-


antly handled using inout parameters. They provide a way for a function to
modify its parameters directly, rather than working with copies. This can
be particularly handy when dealing with complex data structures or when
performance optimization is a concern.

Imagine a scenario where we’re tasked with designing a function to update user
details. These details, such as name and age, may need to be modified based on
various requirements.

struct User {
var name: String
var age: Int
}

func updateUserDetails(
user: inout User, newName: String, newAge: Int
) {
user.name = newName
user.age = newAge
}

var currentUser = User(name: "Alice", age: 30)

updateUserDetails(
user: &currentUser, newName: "Bob", newAge: 25
)

// Prints `User(name: "Bob", age: 25)`


print(currentUser)

In this example, the updateUserDetails() function takes an inout parameter


user, along with new name and age values. This allows the function to directly
modify the properties of the User instance passed to it.
30 FUNCTIONS, METHODS, AND CLOSURES

By avoiding unnecessary copying of data, inout parameters can improve per-


formance and memory usage, particularly when dealing with large data struc-
tures.
FUNCTIONS, METHODS, AND CLOSURES 31

Optimize function arguments with autoclosures


When we define a function parameter as an @autoclosure in Swift, we allow the
caller to pass in an expression as if it is a direct value. Swift then automatically
wraps this expression in a closure. The key benefit is that the expression
inside the autoclosure is not evaluated at the point of the function call, but
rather at the point of execution within the function. It’s useful for optimizing
performance, especially in situations where the evaluation of an argument is
resource-intensive.

To illustrate the use of autoclosures, let’s consider a logging function.


enum LogLevel: Comparable {
case debug, info, warning, error
}

var currentLogLevel: LogLevel = .info

func logMessage(
level: LogLevel,
message: @autoclosure () -> String
) {
if level >= currentLogLevel {
print(message())
}
}

func expensiveStringComputation() -> String {


// Simulate an expensive operation
return "Expensive Computed String"
}

currentLogLevel = .debug
logMessage(
level: .debug,
message: "Debug: \(expensiveStringComputation())"
)

In this example, the logMessage() function takes a message parameter marked


with @autoclosure. This means the expensiveStringComputation() is not ex-
ecuted when the logMessage() function is called. Instead, it’s wrapped in
a closure and only executed when the if condition evaluates to true. This
approach avoids unnecessary computation when the log level is lower than
32 FUNCTIONS, METHODS, AND CLOSURES

debug.

Autoclosures in Swift offer a sophisticated yet straightforward way to optim-


ize function arguments, especially when dealing with potentially expensive
computations. By automatically wrapping arguments in closures, they enable
deferred execution, leading to more efficient and cleaner code.
FUNCTIONS, METHODS, AND CLOSURES 33

Handle different parameter types or numbers with method


overloads

Method overloading in Swift allows us to define multiple method with the same
name but different parameter types or numbers. This feature can enhance the
flexibility and clarity of our API interfaces by allowing them to handle various
data inputs more gracefully.

When we overload a method, we can tailor each version to effectively work with
a specific type of data, improving the method’s usability and reducing the need
for type checks or conversions within the method body.

Here’s an example demonstrating method overloading to handle different types


of user input in a simple application setting.
class InputHandler {
// Handles integer input
func handleInput(input: Int) {
print("Handling integer input: \(input)")
}

// Handles string input


func handleInput(input: String) {
print("Handling string input: \(input)")
}

// Handles boolean input


func handleInput(input: Bool) {
print("Handling boolean input: \(input)")
}
}

let inputHandler = InputHandler()

// Prints `Handling integer input: 42`


inputHandler.handleInput(input: 42)

// Prints `Handling string input: Hello`


inputHandler.handleInput(input: "Hello")

// Prints `Handling boolean input: true`


inputHandler.handleInput(input: true)

In this example, the InputHandler class has three versions of the handleInput()
34 FUNCTIONS, METHODS, AND CLOSURES

method, each designed to manage a different type of data: integers, strings,


and booleans. This design lets each method implementation focus on the
specific processing logic appropriate for its data type, keeping the code clear
and concise.

Method overloading is particularly useful in scenarios where a method’s core


logic varies significantly based on the type of its input, or when we want to
ensure type safety without imposing extra runtime checks or casting. This
approach also simplifies future modifications to the logic for a particular data
type, as changes are contained within the designated method variant.
FUNCTIONS, METHODS, AND CLOSURES 35

Ignore return values without generating a warning

Functions that return a value typically require that value to be used or explicitly
ignored, as unused return values can often signify a programming oversight or
a potential bug. However, in some cases, a function might return a value that
is useful only under certain conditions, and not using the return value should
not necessarily trigger a warning. To handle such scenarios, Swift provides
the @discardableResult attribute. This attribute can be applied to functions
to indicate that the caller may choose to ignore the returned value without the
compiler generating a warning.

For example, consider a logging function that returns a Boolean status indicat-
ing whether the log entry was successfully recorded. While this information can
be crucial in some debugging or error-handling scenarios, in many cases, the
developer might simply want to perform the logging action without checking
the result
@discardableResult
func log(message: String) -> Bool {
// Logic to log the message
print("Log: \(message)")
return true
}

// Use the result


let success = log(message: "User performed an action")
if !success {
print("Logging failed")
}

// Ignore the result


log(message: "User performed an action")

In the code above, the log() function is decorated with the @discardableResult
attribute, allowing it to be called without handling the Boolean result. This
functionality is particularly useful because it provides the flexibility to either
monitor the success of the logging operation or to simply execute the logging
without cluttering the code with unnecessary result checks.

By using @discardableResult, we can design our APIs to be both flexible and


36 FUNCTIONS, METHODS, AND CLOSURES

convenient, accommodating various use cases without compromising the clarity


or safety of the application.
FUNCTIONS, METHODS, AND CLOSURES 37

Indicate to the compiler that a function will never return


When we need to explicitly inform the compiler that a certain point in the
program should not be reached, we should use the Never return type. This
special return type is used to indicate that the function will end the program’s
execution or throw an error, thus it will never return to its caller

Let’s consider an example to understand how Never can be used effectively.


func assertUnreachableCode(
file: String = #file,
line: Int = #line,
function: String = #function
) -> Never {
print("""
Fatal error: Unreachable code reached \
in \(file) at line \(line), function \(function)
""")

fatalError("Unreachable code reached")


}

enum UserCommand {
case start, stop, pause, resume, unknown
}

func processCommand(_ command: UserCommand) -> String {


switch command {
case .start: return "Started"
case .stop: return "Stopped"
case .pause: return "Paused"
case .resume: return "Resumed"
case .unknown:
// This case should logically never occur
assertUnreachableCode()
}
}

print(processCommand(.start))
print(processCommand(.unknown))

In the above code, we define UserCommand, an enumeration that represents dif-


ferent user commands within an application. The function processCommand()
processes these commands, returning a string response for each valid com-
mand. Notably, the unknown case is included to handle any undefined command
38 FUNCTIONS, METHODS, AND CLOSURES

inputs, which ideally should never occur if the application logic is correctly
implemented.

The assertUnreachableCode() function is strategically used within the unknown


case of the switch statement. Marked with a Never return type, it provides a
clear indication of an anomaly if this case is ever triggered. When executed,
it not only terminates the application but also provides detailed debugging
information about where and why the termination occurred.

The compiler understands that after calling assertUnreachableCode(), the


function will not continue its normal execution flow. And for anyone reading
the code, it becomes immediately clear that reaching this point in the code is
abnormal and should be investigated.
FUNCTIONS, METHODS, AND CLOSURES 39

Simplify generic signatures using opaque types

In Swift programming, managing the complexity of type relationships is cru-


cial for creating clear and maintainable APIs. Generics offer flexibility and
reusability that are essential in modular and layered architectures but can
sometimes lead to complex type signatures. Swift’s opaque types help simplify
these signatures while still retaining the benefits of generics and ensuring type
safety. This feature is particularly useful in functions where abstracting the
specific type can enhance API usability and maintainability.

Consider a scenario in a sports equipment manufacturing system where each


piece of equipment can undergo multiple enhancements. We’ll define a system
that models how a soccer ball can be both customized and quality-checked.

protocol Equipment {
var description: String { get }
}

struct SoccerBall: Equipment {


var description: String {
"Soccer ball"
}
}

struct CustomizedEquipment<T: Equipment>: Equipment {


var baseEquipment: T
var description: String {
"Customized \(baseEquipment.description)"
}
}
40 FUNCTIONS, METHODS, AND CLOSURES

struct QualityCheckedEquipment<T: Equipment>: Equipment {


var baseEquipment: T
var description: String {
"Quality checked \(baseEquipment.description)"
}
}

func produceHighQualityCustomizedBall(
) -> CustomizedEquipment<QualityCheckedEquipment<SoccerBall>> {
let qualityCheckedBall = QualityCheckedEquipment(
baseEquipment: SoccerBall()
)
return CustomizedEquipment(baseEquipment: qualityCheckedBall)
}

In this example, the function produceHighQualityCustomizedBall() returns


a type CustomizedEquipment<QualityCheckedEquipment<SoccerBall>>. This
type signature explicitly reveals the process the soccer ball undergoes - first
quality checking, then customization. While descriptive, this signature is quite
complex and tightly couples the function’s users to its specific implementation.

To abstract away the complexity of our function’s return type while maintaining
its strict type safety and benefits, we can use Swift’s opaque types. Here’s how
we can refactor the produceHighQualityCustomizedBall() function to use an
opaque type.
func produceHighQualityCustomizedBall() -> some Equipment {
let qualityCheckedBall = QualityCheckedEquipment(
baseEquipment: SoccerBall()
)
return CustomizedEquipment(baseEquipment: qualityCheckedBall)
}

The function now returns some Equipment. This means it will return something
that conforms to the Equipment protocol, but the exact type is hidden. The use
of some ensures consistency, the function must always return the same type.
At the same time, it allows the internal details about how the equipment is
enhanced to remain obscured from the function’s consumers.

This approach simplifies the API and decouples the implementation details
from the type signature.
FUNCTIONS, METHODS, AND CLOSURES 41

Assign descriptive names to complex closure types with


typealias

In Swift, closures are a core feature that make our code more concise and
adaptable. Yet, as closures grow in complexity, they can clutter our codebase,
especially when dealing with repeated patterns such as completion handlers
in asynchronous operations. These handlers often utilize the Result type to
denote success or failure, leading to verbose and repetitive code that can be
challenging to manage.
var completionHandler: ((Result<String, Error>) -> Void)?

While this is a standard pattern in Swift, it can become quite verbose, especially
if we have multiple such handlers with similar signatures. It can make our code
harder to read and maintain.

To address this, Swift offers the typealias keyword, which lets us assign a
more readable name to complex type signatures. We can use it to simplify the
closure syntax and make our code more expressive.
typealias CompletionHandler = (Result<String, Error>) -> Void

var completionHandler: CompletionHandler?

This refactor makes the code cleaner and clarifies the role of completionHandler.
It simplifies the syntax and enhances the expressiveness of the code, making it
easier to read and maintain.

Moreover, should the structure of our completion handler need to change in


the future, only the typealias definition requires updating. This centralized
change automatically propagates throughout the code, ensuring that all ref-
erences to CompletionHandler reflect the new structure. This method is not
only more efficient but also reduces the risk of errors compared to manually
updating multiple closure definitions.

However, it’s important to use typealias judiciously. Applying it to overly


simple closures or types might lead to unnecessary abstraction and potentially
confuse readers. Reserve typealias for instances where it truly enhances
42 FUNCTIONS, METHODS, AND CLOSURES

readability and maintainability, helping you manage complexity effectively in


your Swift projects.
FUNCTIONS, METHODS, AND CLOSURES 43

Utilize implicit self references in closures

When we capture self in a closure in Swift, we are essentially including a


reference to the instance of the class within the closure’s context. This is
commonly done to access instance properties or methods from within the
closure. However, if not handled carefully, capturing self can lead to retain
cycles, where the closure and the class instance hold strong references to each
other, preventing deallocation and potentially causing memory leaks.

In earlier versions of Swift, explicitly referencing self within closures was


necessary to avoid unintentional retain cycles. However, this requirement
often resulted in verbose and cluttered code.

Swift 5.8 introduced a notable improvement that allows for implicit self within
closures, provided self is captured weakly and safely unwrapped. This change
simplifies code by reducing verbosity without compromising on clarity or safety.

Consider an example involving a Timer within a class that periodically performs


an action. It’s crucial here to capture self weakly to prevent retain cycles
between the class and the timer.
44 FUNCTIONS, METHODS, AND CLOSURES

import Foundation

class PeriodicTaskScheduler {
var timer: Timer?
var interval: TimeInterval

init(interval: TimeInterval) {
self.interval = interval
}

func start() {
timer = Timer.scheduledTimer(
withTimeInterval: interval, repeats: true
) { [weak self] _ in
guard let self else { return }
performRegularTask()
}
}

private func performRegularTask() {


// Perform the task
print("Task performed")
}

deinit {
timer?.invalidate()
print("Scheduler deinitialized")
}
}

Notice how, after unwrapping self, we can directly call performRegularTask()


without the self prefix.

It’s important to note that for implicit self to be used, the unwrapped optional
must be assigned to a variable named self. If renamed, such as to strongSelf,
then it must be referenced explicitly within the closure.

This new capability streamlines syntax while maintaining explicit control over
memory management, enhancing both code readability and safety.
FUNCTIONS, METHODS, AND CLOSURES 45

Embrace higher-order functions to enhance code flexibility

Higher-order functions are a fundamental concept in Swift that can enhance


the modularity and reusability of our code. These functions either take other
functions as parameters or return them, providing a flexible way to abstract
and manipulate behaviors in our applications.

By using higher-order functions, we can design more generic and reusable


components that can be adapted to a wide range of tasks, simplifying complex
operations and reducing redundancy. They are particularly useful in scenarios
involving filtering, mapping, or accumulating results.

Here’s an example that demonstrates creating a higher-order function to apply


different transformations to a given number.
func transformNumber(
_ number: Int,
using transformation: (Int) -> Int
) -> Int {
return transformation(number)
}

// 8
let doubled = transformNumber(4, using: { $0 * 2 })

// 16
let squared = transformNumber(4, using: { $0 * $0 })

// 5
let incremented = transformNumber(4, using: { $0 + 1 })

In this example, the transformNumber() function is a higher-order function


because it takes another function transformation as a parameter, which it ap-
plies to number. This design allows transformNumber() to be extremely flexible,
capable of performing any operation on number that conforms to the (Int) ->
Int signature.

The use of higher-order functions can increase the expressiveness and power
of our code. By passing different functions as parameters, we can customize
the behavior of our functions without altering their implementation. This
46 FUNCTIONS, METHODS, AND CLOSURES

technique is invaluable for tasks that involve applying various operations to data
structures, particularly when combined with Swift’s functional programming
features like map, filter, and reduce.
FUNCTIONS, METHODS, AND CLOSURES 47

Optimize error handling in higher-order functions with


rethrows

Managing errors efficiently is crucial for writing robust and reliable applica-
tions. Swift’s rethrows keyword revolutionizes error handling in higher-order
functions, which are functions that take other functions as arguments. This
feature is especially beneficial for creating flexible and maintainable code by
enabling these functions to handle errors more dynamically.

The rethrows keyword allows a higher-order function to pass along any errors
thrown by its function arguments without necessarily being a throwing func-
tion itself. This capability is crucial for maintaining clean and concise code,
particularly when the functions used as arguments vary between throwing and
non-throwing types. It eliminates the need for do-catch blocks when used with
non-throwing functions, simplifying the calling code.

Consider a function designed to apply a transformation to each element in an ar-


ray. The use of rethrows in this context highlights its utility by accommodating
both simple and complex transformations.
func transform<T>(
_ items: [T],
using transformFunction: (T) throws -> T
) rethrows -> [T] {
var transformedItems = [T]()
for item in items {
transformedItems.append(try transformFunction(item))
}
return transformedItems
}

In the following scenario, since square() is a non-throwing function, the call


to transform() does not require a do-catch block, simplifying its usage.
func square(_ number: Int) -> Int {
return number * number
}

let numbers = [1, 2, 3, 4, 5]


let squaredNumbers = transform(numbers, using: square)
print(squaredNumbers)
48 FUNCTIONS, METHODS, AND CLOSURES

When using squareWithLimit(), which can throw an error, transform()


propagates that error, necessitating a do-catch block for handling it
appropriately.
enum CalculationError: Error {
case resultExceedsMaximumAllowed
}

func squareWithLimit(_ number: Int, maxLimit: Int) throws -> Int {


let result = number * number
guard result <= maxLimit else {
throw CalculationError.resultExceedsMaximumAllowed
}
return result
}

let numbers = [3, 10, 5]


let maxLimit = 100
do {
let squaredNumbers = try transform(numbers) {
try squareWithLimit($0, maxLimit: maxLimit)
}
print(squaredNumbers)
} catch CalculationError.resultExceedsMaximumAllowed {
print("""
Error: A number's square exceeds \
the maximum allowed limit.
""")
}

The rethrows keyword in Swift is a powerful tool for developers leveraging


higher-order functions in their applications. It allows for more robust, clean,
and maintainable error handling by adapting to the error-throwing character-
istics of functions used as arguments. This adaptability makes it invaluable for
building advanced functional programming constructs without compromising
on safety and simplicity.
Custom types: structs, classes, enums

In Swift, the power to define and manipulate custom types—such as structs,


enums, and classes—is fundamental to building robust and efficient applica-
tions. This chapter dives into advanced techniques for defining and enhancing
custom types, aimed at improving their functionality and efficiency in your
applications. You’ll explore how to make your types more expressive, such
as by customizing their string representations or enabling initialization from
literal values, which makes them as straightforward to use as built-in types.

We’ll cover key strategies for balancing custom behavior with automated fea-
tures, like preserving memberwise initializers while adding custom ones. Effi-
ciency is another major focus, with techniques like implementing copy-on-write
to manage memory effectively and ensuring types use only the necessary re-
sources.

Additionally, you’ll learn how to use enumerations for modeling complex data
structures and managing instances with precision, such as through factory
methods or by simplifying comparisons with automatic protocol conformance.

By mastering these techniques, you’ll be able to create sophisticated, efficient


custom types that leverage Swift’s type system, enhancing both the performance
and clarity of your code.

49
50 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Enhance your types with custom string representation

In Swift, the CustomStringConvertible protocol allows custom types to define


how they should be represented as strings, making it possible to provide a more
meaningful and user-friendly description of an instance. Implementing this
protocol is particularly useful for debugging and logging purposes, where a
clear and readable output can significantly ease the process of understanding
and using custom types.

Let’s consider a Coordinates struct that holds geographic coordinates. By con-


forming to CustomStringConvertible, we can customize how these coordinates
are represented when converted to a string, thereby enhancing clarity and read-
ability.
struct Coordinates: CustomStringConvertible {
var latitude: Double
var longitude: Double

var description: String {


let latDirection = latitude >= 0 ? "N" : "S"
let lonDirection = longitude >= 0 ? "E" : "W"

return """
Coordinates: \(abs(latitude))°\(latDirection), \
\(abs(longitude))°\(lonDirection)
"""
}
}

let location = Coordinates(latitude: 37.7749, longitude: -122.4194)

// Prints `Coordinates: 37.7749°N, 122.4194°W`


print(location)

The description property allows us to define a custom string that represents


the instance. In the Coordinates example, the description includes the absolute
values of the latitude and longitude, along with their respective directions
(North, South, East, West). This approach is not only more informative but
also aligns with common representations in mapping and navigation systems.

Implementing CustomStringConvertible makes debugging more straightfor-


CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 51

ward as objects can be printed directly with meaningful information. This re-
duces the need for manually appending properties or writing separate functions
to decipher object states. For types used extensively throughout an application,
providing a clear string representation ensures that logs, error messages, and
debug outputs are accessible and helpful to developers and users alike.
52 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Allow custom types to be initialized directly with literal values

Enhancing custom types with the ability to be initialized directly from literals
can significantly simplify our code, making it more concise and readable. This
functionality is achieved by conforming custom types to specific Swift literal pro-
tocols, such as ExpressibleByIntegerLiteral, ExpressibleByStringLiteral,
ExpressibleByArrayLiteral, and others. These protocols allow our types to
integrate seamlessly with Swift’s type inference system, providing a natural
and intuitive way to create instances of our types.

To implement literal support, we should first choose the appropriate literal


protocol that matches the type of data our custom type will handle. Each literal
protocol requires implementing a specific initializer that accepts the respective
literal type as an argument. For instance, if we wanted our custom type to be ini-
tialized with a string literal, we would conform to ExpressibleByStringLiteral
and implement init(stringLiteral value: String).

Consider a Temperature struct that we want to initialize using integer literals,


where the integer represents degrees Celsius.
struct Temperature: ExpressibleByIntegerLiteral {
var celsius: Double

init(integerLiteral value: Int) {


self.celsius = Double(value)
}
}

let currentTemperature: Temperature = 23

In this implementation, the Temperature struct conforms to ExpressibleByIntegerLite


The required initializer init(integerLiteral value: Int) converts the re-
ceived integer literal to a Double and assigns it to the celsius property.

Literal initialization makes the code more natural and easier to understand at a
glance, especially when the type and the literal value logically correspond, such
as a temperature value in degrees. Conforming to literal protocols allows cus-
tom types to behave more like native Swift types, making their usage consistent
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 53

with built-in types.

When adding literal support, it’s essential to ensure that the initializer can
handle all potential values of the literal appropriately. This is crucial for main-
taining the type’s integrity and safety, preventing runtime errors and ensuring
that our type’s constraints are respected.
54 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Preserve memberwise initializer when adding custom inits to


structs

In Swift, struct types automatically receive a memberwise initializer, which is


exceptionally handy for initializing properties directly. However, introducing
a custom initializer in the struct’s main definition suppresses this automatic
provision. To circumvent this limitation and preserve both the memberwise
and custom initializers, we can define the custom initializer in an extension.

Consider a Person struct with properties for name and age.


struct Person {
var name: String
var age: Int
}

Swift inherently provides a memberwise initializer for Person, enabling us to


instantiate it as follows:
let person1 = Person(name: "Alice", age: 30)

To introduce a custom initializer that calculates age based on a birth year while
retaining the memberwise initializer, we can define the custom initializer in an
extension of the struct.
extension Person {
init(name: String, birthYear: Int) {
let currentYear = Calendar.current
.component(.year, from: Date())
self.init(name: name, age: currentYear - birthYear)
}
}

This extension adds a new initializer that accepts a name and birthYear, calcu-
lates the age, and utilizes the memberwise initializer to complete the construc-
tion of the Person instance.

Now, with this setup, both initializers can be used seamlessly.


let person2 = Person(name: "Bob", birthYear: 1990)
let person3 = Person(name: "Charlie", age: 25)

This approach works because extensions in Swift can add functionality to types,
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 55

including initializers, without overriding the default functionality provided by


the language. By leveraging extensions to define custom initializers, we main-
tain the utility of Swift’s default memberwise initializer, thus enhancing the
flexibility of our struct’s initialization options without sacrificing convenience.
56 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Bind custom types to specific raw values

The RawRepresentable protocol in Swift allows us to create custom types that


have a direct relationship with a basic, underlying data type, commonly known
as a “raw” value. It’s typically associated with enums, but it can also be useful
for custom structs or classes.

Consider a scenario where we need a system for handling unique identifiers,


such as database keys or user session IDs. Using a simple string might work,
but it can be error-prone and confusing when strings are used for different
types of data throughout our application.

By creating a struct named Identifier that conforms to RawRepresentable,


we can ensure that each identifier is not just a string, but a string that follows
specific rules. This not only prevents mix-ups but also makes our code safer
and clearer to other developers who might work on it.
struct Identifier: RawRepresentable {
var rawValue: String

init?(rawValue: String) {
guard
!rawValue.isEmpty,
rawValue.allSatisfy(
{ $0.isLetter || $0.isNumber || $0 == "-" }
)
else {
return nil
}
self.rawValue = rawValue
}
}

In our code, Identifier is a struct that takes a string as its raw value. The
initializer checks that the string is not empty and contains only letters, numbers,
or dashes. If the string doesn’t meet these criteria, the initializer fails and
returns nil, preventing invalid identifiers from being created.

As RawRepresentable is a standard protocol, conforming to it means


Identifier can seamlessly integrate with Swift’s standard library and
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 57

third-party libraries expecting RawRepresentable types.

Converting between the custom type and its raw value is straightforward, which
can be helpful in debugging and logging.
if let id = Identifier(rawValue: "abc-123-abc") {
print("Stored ID: \(id.rawValue)")
}

By using such structured types, we not only improve the safety of our code but
also make it more intuitive and user-friendly for anyone who interacts with it
in the future.
58 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Transform your types into callable entities with callAsFunction()

The callAsFunction() method in Swift unlocks the capability to invoke a type


as if it were a function. This feature can enhance the readability and the ex-
pressiveness of the code, making certain types behave more like functions,
which can be particularly useful in scenarios where a type’s primary role is to
perform an operation or transformation.

We can implement the callAsFunction() method in any Swift type, such as


classes, structs, or enums. This method can take any number of parameters
and return a value, just like a regular function. When we add this method to a
type, instances of that type can be called as if they were functions, providing a
syntactically clean and intuitive way to execute their code.

Consider a struct named Greeter that is designed to generate greeting messages.


By implementing callAsFunction(), we can use instances of Greeter directly
to produce greetings, simplifying how the struct is used.
struct Greeter {
var greeting: String

func callAsFunction(name: String) -> String {


return "\(greeting), \(name)!"
}
}

let friendlyGreeter = Greeter(greeting: "Hello")

// Prints `Hello, World!`


print(friendlyGreeter(name: "World"))

In this setup, the Greeter struct possesses a greeting property and a


callAsFunction(name: String) method that constructs a greeting using
the greeting property and a provided name. This method allows the
friendlyGreeter instance to be invoked like a function, enhancing the
structural elegance of the code.

Using callAsFunction() can reduce boilerplate by eliminating the need for


additional method calls and improve code expressiveness by allowing instances
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 59

to encapsulate functionality in a function-like manner. It also adds flexibility


by supporting multiple callAsFunction() methods with different parameters,
enabling parameter-based overloading.

This method is ideal for scenarios where objects perform a primary action
repeatedly. For example, objects that transform data can apply transforma-
tions directly using callAsFunction(), command pattern objects can execute
commands directly, and API wrappers can provide a more fluent interface by
allowing function-like invocation.
60 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Leverage Comparable to create ranges from custom types

Creating ranges from custom types in Swift can significantly enhance the way
we handle and manipulate domain-specific data, particularly when dealing
with intervals such as time slots or geographical boundaries. For custom types
to support this functionality, they need to conform to the Comparable protocol.
This conformance involves implementing the necessary comparison operators
that define how instances of the custom type can be ordered and compared.

To illustrate, let’s consider the example of a custom type called TimeSlot, which
we’ll use in a time management application. This type encapsulates hours and
minutes and includes comparison logic that determines the order based on
time

struct TimeSlot: Comparable {


var hour: Int
var minute: Int

// Implement the Comparable protocol

static func < (lhs: TimeSlot, rhs: TimeSlot) -> Bool {


(lhs.hour < rhs.hour) ||
(lhs.hour == rhs.hour && lhs.minute < rhs.minute)
}

static func == (lhs: TimeSlot, rhs: TimeSlot) -> Bool {


(lhs.hour == rhs.hour && lhs.minute == rhs.minute)
}
}
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 61

// Create ranges with the TimeSlot

let morningStart = TimeSlot(hour: 9, minute: 0)


let morningEnd = TimeSlot(hour: 12, minute: 0)

let morningShift = morningStart..<morningEnd

// Check if a specific time slot falls within the morning shift

let timeSlot1 = TimeSlot(hour: 10, minute: 30)

if morningShift.contains(timeSlot1) {
print("Time slot 1 is within the morning shift.")
}

In this setup, TimeSlot is designed with a Comparable implementation that


compares TimeSlot instances first by hour and then by minute if the hours are
the same. This comparison logic is essential for constructing meaningful ranges
of time slots. The range morningShift, created using TimeSlot, represents the
interval from 9:00 AM to 12:00 PM.

This approach allows us to leverage the power of range operations in Swift,


such as checking if a particular time slot falls within a specified interval,
morningShift.contains(timeSlot1) for example. It demonstrates how
custom types can be tailored to handle specific data more effectively, ensuring
that operations such as inclusion checks are both intuitive and efficient.

Using custom ranges with types like TimeSlot not only improves the clarity of
the code by abstracting complex comparisons into the type’s logic, but it also
enhances the adaptability of the application. This enables us to manage data
that is inherently interval-based, such as schedules or geographic regions, in a
way that is both scalable and easy to maintain.
62 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

Implement copy-on-write for efficient memory usage

Copy-on-write (COW) is a sophisticated programming technique that optimizes


the use of resources such as memory, particularly for types that can encapsulate
large amounts of data. This technique delays the copying of data until it is
necessary, which is particularly useful in Swift, a language that emphasizes
value types and immutability.

Swift extensively uses value types, such as structs and enums, which are copied
on assignment or when passed to a function. This behavior ensures data encap-
sulated within these types remains immutable and local to a specific context.
However, this could lead to performance bottlenecks due to excessive copy-
ing, especially with large data structures. Swift’s standard library addresses
this challenge using COW, ensuring structures like Array, Dictionary, Set, and
String are efficient even when they hold large amounts of data.

Though Swift’s standard library types already utilize COW, understanding its
implementation can be beneficial. Here’s an example demonstrating COW
with a custom type.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 63

struct LargeData {
var data: [String: String] {
get {
storage.data
}
set {
storageForWriting.data = newValue
}
}

private var storage: Storage

private var storageForWriting: LargeData.Storage {


mutating get {
if !isKnownUniquelyReferenced(&storage) {
self.storage = storage.copy()
}
return storage
}
}

init(data: [String: String]) {


storage = Storage(data: data)
}

private class Storage {


var data: [String: String]

init(data: [String: String]) {


self.data = data
}

func copy() -> LargeData.Storage {


print("Making a copy")
return LargeData.Storage(data: data)
}
}
}

var largeData = LargeData(data: ["color": "orange"])

// No copy is made
var copyOfDataData = largeData

// Prints `Making a copy`


copyOfDataData.data["color"] = "blue"

We encapsulate the actual data within a private inner class Storage inside the
64 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

struct. This class holds the mutable data dictionary and provides a method for
copying itself. The storageForWriting computed property checks if the storage
instance is uniquely referenced. If not, it performs a copy before returning the
storage to ensure that subsequent modifications do not affect other references.
The data property getter and setter provide external access to the dictionary,
using storageForWriting to ensure changes are isolated when necessary. This
ensures the struct maintains value semantics, while still allowing efficient
modifications.

Copy-on-write is a powerful mechanism in Swift that allows developers to


maintain the immutability and efficiency of value types while minimizing per-
formance costs associated with data copying. Implementing COW in custom
types can help harness these benefits, particularly when dealing with large data
structures within value types.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 65

Prevent misuse of irrelevant inherited functionalities in sub-


classes

Subclassing allows a class to inherit methods, properties, and initializers from


a superclass. While inheritance is a powerful feature, not all functionalities
of the superclass may be relevant or safe for the subclass to use. Misusing
these inherited functionalities can lead to bugs and unintended behaviors,
undermining the robustness of our applications.

The @available(*, unavailable) attribute in Swift is a useful tool to manage


inheritance more safely. It allows us to explicitly mark inherited methods or
initializers as unavailable in subclasses. This prevents their use, enhances
the safety and clarity of the subclass, and ensures that the subclass’s behavior
remains aligned with its intended purpose.

Consider a scenario involving different types of membership, where a


LifetimeMembership should not implement the same renew() method as a
general Membership.

class Membership {
func renew() {
// Generic renewal process
}
}

class LifetimeMembership: Membership {


@available(
*, unavailable,
message: "Lifetime memberships do not require renewal."
)
override func renew() {
super.renew()
}
}

let lifetimeMember = LifetimeMembership()

// Causes a compile-time error with a clear message


lifetimeMember.renew()

In this example, marking the renew() method as unavailable in LifetimeMembership


66 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

protects the class from being utilized in ways that contradict its lifetime nature.

In UIKit, dealing with custom UIView subclasses often requires implementing


init(coder:) due to NSCoding protocol conformance. However, if a view is not
meant to be loaded from a storyboard, this initializer becomes redundant.
final class CustomView: UIView {
@available(
*, unavailable,
message: """
CustomView is not designed \
to be initialized from a storyboard.
"""
)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

init(customParameter: [Int]) {
// Custom initialization logic
super.init(frame: .zero)
// Additional setup using customParameter
}
}

In the CustomView case, marking init(coder:) as unavailable discourages its


use and directs developers towards the proper initialization method, ensuring
that the view is initialized correctly under the expected circumstances.

Using @available(*, unavailable) in explicitly blocks the use of inherited


functionalities that do not apply to the subclass, preventing errors and pro-
moting safer coding practices. By marking methods as unavailable, it clearly
communicates to other developers how a subclass should and shouldn’t be used.
It helps enforce architectural and design decisions at compile time, making it
easier to maintain and evolve the codebase.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 67

Model hierarchical data with recursive enumerations

Recursive enumerations in Swift are a powerful feature for modeling data


structures where a particular type can recur within its own definition, such as
in nested or hierarchical systems. The ability to include instances of the same
enumeration as associated values of its cases makes recursive enums ideal
for representing complex, nested relationships in an intuitive and type-safe
manner.

To enable recursion within an enumeration, Swift requires the use of the


indirect keyword. Placing indirect before the enum keyword allows Swift
to handle the enumeration’s memory in a way that supports nested instances
of itself.

Consider a file system that consists of files and folders, where folders can
contain other files or folders, creating a potentially deep hierarchy. Here’s how
we might represent such a structure using a recursive enumeration.
indirect enum FileSystemItem {
case file(name: String)
case folder(name: String, items: [FileSystemItem])
}

let imageFile = FileSystemItem.file(name: "[email protected]")


let textFile = FileSystemItem.file(name: "notes.txt")
let documentsFolder = FileSystemItem.folder(
name: "Documents",
items: [imageFile, textFile]
)
let desktopFolder = FileSystemItem.folder(
name: "Desktop",
items: [documentsFolder]
)

In this example, the folder case represents a folder, which contains a name
and an array of FileSystemItem, allowing it to store other files or folders. This
structure allows each folder case to encapsulate an arbitrary number of files
and subfolders, mirroring the recursive nature of real file systems. The hier-
archy of the file system is expressed clearly, with each level of the structure
cleanly represented using the same FileSystemItem enum. This makes the
68 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

data structure easy to understand and maintain.

Recursive enumerations are not just theoretical constructs but have practical
applications in many areas, including UI components and organizational struc-
tures. They simplify the modeling of complex hierarchical data by using a
consistent, clear, and safe structure. This makes them an invaluable tool in the
toolkit of any Swift developer looking to manage nested data effectively.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 69

Simplify enum comparisons with automatic Comparable con-


formance

The introduction of automatic Comparable conformance for enumerations in


Swift 5.3 significantly streamlined the process of comparing enum cases, elim-
inating the need for custom comparison logic in many scenarios. This feature
is particularly useful for enums that represent ordered stages or levels, such as
developmental stages of a plant or priority levels in task management.

Consider an enumeration that represents the growth stages of a plant. With


Swift’s automatic Comparable conformance, we can directly compare these
stages based on their declaration order in the enum.

enum PlantGrowth: Comparable {


case seed
case sprout
case flowering
case fruiting
}

let currentStage = PlantGrowth.sprout


let nextStage = PlantGrowth.flowering

// Use the automatically derived comparison logic


if currentStage < nextStage {
print("The plant is still growing.")
}

In this example, the Comparable protocol allows for a natural and intuitive
comparison of growth stages, reflecting their sequential nature.

Automatic conformance also extends to enums with associated values, provided


these values are Comparable. This enables more nuanced comparisons that can
take into account additional context or details.
70 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

enum TaskPriority: Comparable {


case low
case medium
case high

// Associated value that is also `Comparable`


case critical(level: Int)
}

let task1 = TaskPriority.high


let task2 = TaskPriority.critical(level: 5)
let task3 = TaskPriority.critical(level: 10)

// Compare tasks with different priorities


if task2 > task1 {
print("Task 2 has a higher priority than task 1.")
}

// Compare tasks within the same category but with different levels
if task3 > task2 {
print("Task 3 has a higher priority than task 2.")
}

This setup not only categorizes tasks by general priority levels but also allows
for precise gradation among tasks deemed critical, enhancing decision-making
processes based on priority.

Automatic Comparable conformance reduces boilerplate code and simplifies


comparisons of enum cases, making code easier to write and understand. It
supports natural ordering based on the enum declaration or the logical order
of associated values, which is especially useful in domains modeling processes
or hierarchies.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 71

Generate a collection of all cases in an enum

When developing in Swift, enums serve as a core feature for representing a


collection of related values in a type-safe way. To further extend their utility,
Swift offers the CaseIterable protocol, which is invaluable for cases where you
need to work with all possible values of an enum systematically. This feature
is particularly useful for UI development, testing, or any situation where you
need to present or evaluate all cases from an enumeration.

Implementing CaseIterable is straightforward for enums without associated


values. By conforming to this protocol, Swift automatically synthesizes an
allCases property for our enum, providing an array of all the enum’s cases. This
automatic synthesis removes the need for manual updates to case collections,
ensuring that the allCases array is always current with the enum definition.

enum CompassDirection: CaseIterable {


case north
case south
case east
case west
}

/* Prints `[
CompassDirection.north, CompassDirection.south,
CompassDirection.east, CompassDirection.west
]` */
print(CompassDirection.allCases)

For enums with associated values, the automatic synthesis of allCases is not
possible because Swift cannot infer all potential combinations of the associ-
ated values and their cases. In these instances, we must manually implement
the allCases property, defining each combination that makes sense for our
application’s logic.

This approach could work in situations where the associated values are finite
and predictable, like boolean flags.
72 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

enum FeatureToggle: CaseIterable {


case darkMode(isEnabled: Bool)
case logging(isEnabled: Bool)

static var allCases: [FeatureToggle] {


return [
.darkMode(isEnabled: true),
.darkMode(isEnabled: false),
.logging(isEnabled: true),
.logging(isEnabled: false)
]
}
}

/* Prints `[
FeatureToggle.darkMode(isEnabled: true),
FeatureToggle.darkMode(isEnabled: false),
FeatureToggle.logging(isEnabled: true),
FeatureToggle.logging(isEnabled: false)
]` */
print(FeatureToggle.allCases)

This approach ensures that we have full control over the enum cases, especially
when the combinations of associated values are critical to our application’s
functionality.

The CaseIterable protocol significantly enhances the functionality of enums


in Swift by providing a systematic way to access all cases. This feature sim-
plifies tasks such as populating UI elements, conducting thorough tests, or
applying configurations across a range of options, ensuring that no enum case
is overlooked.
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 73

Utilize enum cases as factory methods

In Swift, enum cases can be remarkably versatile when they are designed with
associated values. This feature allows enum cases to act very much like factory
methods. Essentially, when an enum case has associated values, the case
name itself becomes a function. This function takes the associated values as
parameters and returns an instance of the enum, thereby functioning as a
factory method.

Let’s apply this concept to a Theme enum that manages different styling config-
urations for a user interface.
enum Theme {
case light
case dark
case custom(textColor: String, font: String)
}

let summerTheme = Theme.custom(textColor: "Yellow", font: "Arial")

Here, the Theme.custom case is essentially a function with the signature


(String, String) -> Theme. This means when we provide a text color and a
font, Theme.custom returns a new Theme instance configured with those specific
attributes.

One of the powerful features of these enum factory methods is their ability to
interact seamlessly with higher-order functions.
let configs = [("Red", "Helvetica"), ("Blue", "Verdana")]
let themes = configs.map(Theme.custom)

By passing Theme.custom directly to the map() function, we efficiently transform


each tuple in configs into a Theme instance. The map() function iterates over
each element of configs, and for each element, it invokes Theme.custom with
the color and font specified in the tuple.

Using enum cases as factory methods provides a robust, type-safe way to in-
stantiate enum members. This method not only simplifies object creation but
also enhances code clarity by encapsulating configuration detail. Moreover,
74 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS

integrating these factory methods with higher-order functions like map() allows
for efficient and elegant batch processing of configurations, making our code
more functional and expressive.
Advanced property management strategies

This chapter explores advanced techniques for effective property management


in Swift, providing you with strategies to enhance code robustness and effi-
ciency. It covers essential topics such as dynamic property initialization, access
control, and the use of property wrappers to encapsulate behavior. You will
learn to optimize property initialization to prevent resource wastage and en-
sure that properties maintain a consistent state through computed values and
property observers.

The chapter also delves into modern Swift features like asynchronous property
getters and error handling in property contexts, equipping you with the skills
to handle complex scenarios and improve the maintainability of your code.

Through these insights, you will master the nuanced management of properties
in Swift, enabling you to build more reliable and scalable applications.

75
76 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Set dynamic default values for properties using closures

Setting default values for properties in Swift typically involves straightforward,


static assignments. However, for dynamic initialization that requires logic or
computation, closures can offer a robust and elegant solution. This approach
is particularly advantageous for properties whose default values depend on
conditions or configurations that aren’t known at compile time.

Closures allow us to include the initialization logic within the property declara-
tion itself, keeping related code organized and easily accessible.

Consider a GameSettings class where the game’s difficulty level automatically


adjusts based on the time of day. This dynamic setting illustrates how a closure
can be used to initialize a property based on runtime conditions.
import Foundation

class GameSettings {
var difficulty: String = {
let hour = Calendar.current.component(.hour, from: Date())
switch hour {
case 6..<12:
return "Easy"
case 12..<18:
return "Medium"
default:
return "Hard"
}
}()

var gameMode: String

init(gameMode: String) {
self.gameMode = gameMode
}
}

In this example, the difficulty property is dynamically initialized using a


closure that evaluates the current hour to set an appropriate difficulty level.
The closure is wrapped in braces {}, followed by () to signify that the closure
should be executed immediately as part of the property’s initialization. This
method allows for a flexible and context-sensitive setup of the game’s difficulty
ADVANCED PROPERTY MANAGEMENT STRATEGIES 77

level without requiring additional input from the developer or user at the time
of initialization.

Properties can be initialized based on the current state or environment, offering


more flexibility than static default values. This technique is invaluable in
scenarios where property values need to be contextually relevant, such as in
user settings, environment-sensitive configurations, or as illustrated, in game
settings based on the time of day.
78 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Avoid unnecessary initialization of resource-heavy properties

Efficient management of resource-heavy properties is key to optimizing ap-


plication performance and responsiveness. In Swift, lazy properties are an
excellent tool for delaying the initialization of intensive resources until they are
actually required. This approach not only mitigates the initial burden on system
resources but also enhances application responsiveness by loading heavy assets
only when necessary.

Lazy properties are instantiated only upon their first actual use. This function-
ality allows us to include properties in our classes that, while potentially heavy
on memory or processing, do not impact the app’s performance until they are
actively accessed.

This delayed initialization is particularly beneficial in scenarios where certain


properties of an object may never be utilized during its lifecycle. By using lazy
properties, unnecessary resource consumption is avoided, which can lead to
significant savings in terms of processing time and memory usage.

Consider a UserProfile class that includes a method to load a user’s photo, a


process that is typically resource-intensive due to the size of image files and
the operations required to fetch them from a remote server or database.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 79

class UserProfile {
var userID: String

lazy var profilePhoto: Data = {


return loadImage(for: userID)
}()

init(userID: String) {
self.userID = userID
}

private func loadImage(for: String) -> Data {


print("Loading image data ...")

// Simulated operation to load image data


return Data()
}
}

// At this point, the photo is not loaded yet


let user = UserProfile(userID: "test-id")

// Prints `Loading image data ...`


let imageData = user.profilePhoto

// Does not trigger re-initialization


let imageDataAgain = user.profilePhoto

In this example, the profilePhoto property of UserProfile is defined as a lazy


property. The image data is loaded only on the first access to profilePhoto, and
subsequent accesses do not incur additional loads or processing, as evidenced by
the absence of further print statements. This demonstrates the lazy property’s
efficacy in conserving resources by avoiding redundant operations.

Utilizing lazy properties can dramatically improve the efficiency and perform-
ance of applications, particularly those requiring the handling of heavy data
operations. It ensures that costly processes are only executed when absolutely
necessary, preserving system resources and keeping the application nimble
and responsive.
80 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Leverage computed properties to synchronize related data


Computed properties in Swift are a powerful feature, often used to calculate
and return a value rather than storing it directly. While getters are commonly
written to retrieve the computed value, setters can also be incredibly useful.
They allow us to monitor and respond to changes, ensuring that our object’s
state remains consistent.

Setters in computed properties are particularly useful when we need to maintain


a relationship between several properties of an object. They help ensure that
when one property changes, other related properties are updated accordingly
to maintain consistency within the object’s state.

Consider a Circle class where the dimensions of the circle—radius, diameter,


circumference, and area—are interrelated. By using a setter in the computed
property for the diameter, any adjustment to the diameter can be directly used
to update the radius, and indirectly, the area and circumference.
class Circle {
var radius: Double

init(radius: Double) {
self.radius = radius
}

// Computed property for diameter with getter and setter


var diameter: Double {
get {
return radius * 2
}
set(newDiameter) {
radius = newDiameter / 2
}
}

var area: Double {


return Double.pi * radius * radius
}

var circumference: Double {


return Double.pi * diameter
}
}
ADVANCED PROPERTY MANAGEMENT STRATEGIES 81

The diameter is calculated by doubling the radius, and when set, it adjusts the
radius accordingly. This ensures that changes to the diameter are reflected
across all related properties. Area and circumference are dynamically calculated
based on the current value of the radius and diameter, ensuring their values are
always correct based on the latest measurements of the circle. The user of the
Circle class can modify the diameter without worrying about inconsistent or
incorrect circle measurements. The internal mechanics of how these properties
interact are encapsulated within the class, providing a clean and easy-to-use
interface.

By centrally managing how changes in one property affect others, computed


setters ensure that the object always remains in a valid state.
82 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Activate property observers during initialization

Property observers, such as willSet and didSet are not invoked when a prop-
erty is initially set within an initializer. This behavior is intentional to ensure
that the object is fully and consistently initialized before any additional logic
is executed. However, there are times when triggering behaviors similar to
property observers during initialization is necessary, such as when setting up
the object’s initial state requires additional operations or notifications.

One effective method to circumvent this limitation is by invoking a separate


setup method post-initialization to modify the property, thus triggering the
observers.
class MyClass {
var myProperty: String {
willSet {
print("Will set myProperty to \(newValue)")
}
didSet {
print("""
Did set myProperty to \(myProperty), \
previously \(oldValue)
""")
}
}

init(value: String) {
myProperty = "Initial value"
setupPropertyValue(value: value)
}

private func setupPropertyValue(value: String) {


myProperty = value
}
}

let myObject = MyClass(value: "New value")

This approach ensures that the property observers are activated, albeit after the
initial state is set, allowing for any associated side effects or validations to occur
as they would during normal property assignments outside of initialization.

Another strategy involves using a defer block within the initializer to delay the
ADVANCED PROPERTY MANAGEMENT STRATEGIES 83

assignment of the property until just before the initializer completes, ensuring
property observers are triggered.
class MyClass {
var myProperty: String {
willSet {
print("Will set myProperty to \(newValue)")
}
didSet {
print("""
Did set myProperty to \(myProperty), \
previously \(oldValue)
""")
}
}

init(value: String) {
defer { myProperty = value }
myProperty = "Initial value"
}
}

let myObject = MyClass(value: "New value")

The defer block here is a neat trick to ensure that the property is set within the
scope of initialization while still triggering the didSet and willSet observers. It
guarantees that the observers fire immediately after the initial property setup,
closely mimicking their behavior outside of initialization.

Both methods offer valid ways to ensure property observers are triggered during
initialization, each with its own advantages. The separate method approach
is clear and explicit, making the code easy to understand and maintain. The
defer block approach provides a more succinct alternative that encapsulates
the observer-triggering logic within the initializer itself.

Using these techniques, we can effectively leverage property observers even


during the initialization phase, enabling more flexible object configuration
right from the start.
84 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Prevent unauthorized modifications of properties with


private(set)

Swift’s private(set) keyword is a powerful feature for managing access levels,


particularly when we need to expose a property for reading but keep its modi-
fication controlled and restricted. It prevents unauthorized modifications to
the property, ensuring it only changes in controlled and predictable ways.

Consider an Account class where it’s important that the balance is visible to
external entities but should only be modified through well-defined methods
like deposits and withdrawals. This control ensures that the balance changes
in a predictable manner and prevents errors or security issues that could arise
from improper access.
class Account {
private(set) var balance: Double = 0.0

func deposit(amount: Double) {


if amount > 0 {
balance += amount
}
}

func withdraw(amount: Double) -> Bool {


if amount <= balance {
balance -= amount
return true
}
return false
}
}

let account = Account()


account.deposit(amount: 100)

// Prints `100.0`
print(account.balance)

// � Cannot assign to property: 'balance' setter is inaccessible


account.balance = 50

In this example, the balance property is publicly readable, allowing external


entities to see the account balance. However, modifications to balance are
ADVANCED PROPERTY MANAGEMENT STRATEGIES 85

restricted to the methods deposit() and withdraw() within the Account class,
preventing any unauthorized changes.

Using private(set) in Swift is an excellent strategy for safeguarding the integ-


rity of our data while still providing necessary visibility. It can reduce the risk
of bugs or vulnerabilities related to improper handling of critical data elements.
86 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Provide default property values in protocol extensions

Swift’s protocol extensions are a powerful feature that help streamline code
and reduce boilerplate, particularly through default implementations. While
default methods are commonly discussed, providing default property values
in protocol extensions is equally beneficial, especially for ensuring consistent
behavior across conforming types.

Consider the scenario of developing multiple network services that usually


share common configurations such as timeout intervals or base URLs. By
utilizing protocol extensions to provide default property values, we can signi-
ficantly simplify configuration management across the entire network layer of
an application.

protocol NetworkService {
var baseURL: URL { get }
var timeoutInterval: TimeInterval { get }
}

extension NetworkService {
var baseURL: URL {
return URL(string: "https://api.example.com")!
}

var timeoutInterval: TimeInterval {


return 30.0 // seconds
}
}

With this setup, any type conforming to NetworkService automatically inherits


these default values, eliminating the need for repetitive configuration code.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 87

struct ProductService: NetworkService {}


struct UserService: NetworkService {}

let productService = ProductService()

// Prints `https://api.example.com`
print(productService.baseURL)

let userService = UserService()

// Prints `30.0`
print(userService.timeoutInterval)

The real strength of using protocol extensions in this way is the ease with which
defaults can be overridden to accommodate specific needs without altering the
protocol itself or other conforming types.
struct SpecialUserService: NetworkService {
var baseURL: URL {
return URL(string: "https://special.api.example.com")!
}
}

let specialUserService = SpecialUserService()

// Prints `https://special.api.example.com`
print(specialUserService.baseURL)

In this example, SpecialUserService overrides the default baseURL while still


benefiting from the default timeoutInterval. This approach allows for flexibil-
ity and specificity when needed, while maintaining simplicity and reusability
for standard cases.

Using default property values in protocol extensions allows us to ensure consist-


ency across different implementations of a protocol, reduce code duplication
by eliminating the need to repeat common configurations, and enhance flex-
ibility by making it easy to override defaults when unique configurations are
necessary.
88 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Perform asynchronous operations within property getters

Asynchronous properties are a part of the concurrency model introduced in


Swift 5.5, which includes async/await. These properties allow us to write
code that can perform asynchronous operations while waiting for values to be
computed or retrieved.

An async property looks like a regular property but with the addition of the
async keyword. When we access an async property, we use await to pause the
execution until the property is ready to provide its value. This integrates seam-
lessly with Swift’s concurrency model, ensuring that the UI remains responsive
while the app performs background tasks.

Imagine a UserProfile class that needs to fetch profile data from a network
resource. Using an async property, we can simplify how this data is accessed
and handled, making our code more readable and maintainable.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 89

class UserProfile {
var userID: String

init(userID: String) {
self.userID = userID
}

// Async property
var profileData: Profile? {
get async {
return await fetchDataForUser(userID)
}
}

struct Profile {
var name: String
var email: String
}
}

// Mock function to simulate fetching data


func fetchDataForUser(
_ userID: String
) async -> UserProfile.Profile? {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return .init(
name: "John Doe",
email: "[email protected]"
)
}

let user = UserProfile(userID: "12345")


Task {
if let data = await user.profileData {
print("Name: \(data.name), Email: \(data.email)")
} else {
print("Profile data not found.")
}
}

The profileData property includes an async getter. This means when


profileData is accessed, the await keyword must be used, signaling that the
execution should wait until the data is ready, thus not blocking the main
thread.

Async properties reduce complexity by handling asynchronous operations


90 ADVANCED PROPERTY MANAGEMENT STRATEGIES

within property accessors, avoiding the need for additional asynchronous hand-
ling in the business logic layer. By using async properties, long-running tasks
such as data fetching do not block the main thread, ensuring that the user
interface remains responsive.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 91

Throw errors in property getters to manage failures

While properties typically return a value directly, there are cases where a prop-
erty’s computation involves complex calculations, data transformations, or
dependencies on external data sources that can fail. To manage such uncertain-
ties safely and effectively, Swift allows property getters to throw errors. This
capability ensures that any failures in property computation can be gracefully
handled, enhancing the robustness and reliability of our applications.

Imagine a UserProfile class that includes an age property, calculated based


on a user’s date of birth. This calculation could potentially fail, for instance, if
the birth date is not set or if the date calculation is faulty due to incorrect data
formats or logic errors. In such cases, using a throwing getter can prevent the
propagation of incorrect or invalid data.
92 ADVANCED PROPERTY MANAGEMENT STRATEGIES

import Foundation

class UserProfile {
var birthDate: Date?

var age: Int {


get throws {
guard let birthDate else {
throw DateError.noData
}

let ageComponents = Calendar.current.dateComponents(


[.year], from: birthDate, to: Date()
)

guard let age = ageComponents.year else {


throw DateError.calculationFailed
}

return age
}
}

init(birthDate: Date) {
self.birthDate = birthDate
}
}

enum DateError: Error {


case noData
case calculationFailed
}

By allowing property getters to throw errors, we can explicitly catch and handle
these errors where the property is accessed. This explicit error handling path-
way ensures that issues can be dealt with immediately, preventing erroneous
data from affecting the downstream logic.

Properties that can fail clearly document their failure conditions through er-
rors. This makes debugging easier because the source of an issue can be traced
through the error types and messages, rather than through obscure or mislead-
ing runtime behaviors. This approach is particularly useful when properties
depend on volatile or external data sources where issues such as network fail-
ures, data format changes, or resource unavailability might occur.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 93

While powerful, throwing getters should be used judiciously. Overuse can


lead to overly complex calling code, where every property access needs to be
wrapped in try-catch blocks.
94 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Handle mutable static properties in generic types

In Swift, adding static mutable properties directly to generic types is not sup-
ported, which can pose challenges when trying to manage state that should
be shared across all instances of a generic type. The Swift compiler expli-
citly prohibits this with an error: Static stored properties not supported
in generic types. However, there is a practical workaround to this limita-
tion that involves using an external storage mechanism and computed static
properties.

To circumvent the limitation of direct static mutable properties in generic types,


we can use a combination of a global dictionary and computed properties. The
dictionary will hold the values associated with each specific instantiation of
the generic type, using ObjectIdentifier to differentiate between different
specializations.
// Private dictionary to store values for each type specialization
fileprivate var _gValues: [ObjectIdentifier: Any] = [:]

struct MyValue<T> {
static var a: Int {
get {
_gValues[ObjectIdentifier(Self.self)] as? Int ?? 42
}

set {
_gValues[ObjectIdentifier(Self.self)] = newValue
}
}
}

We use ObjectIdentifier(Self.self) to obtain a unique identifier for each


type specialization of MyValue<T>. ObjectIdentifier provides a stable and
hashable reference to the class, struct, or enum type, allowing us to use it as
a key in our dictionary. The _gValues dictionary holds the values for each
type specialization. This dictionary is external to the generic type and thus
not affected by the constraints on static properties within generic types. The a
property acts as an interface to get and set values stored in _gValues. It appears
and behaves like a static property but uses the dictionary as its storage backend,
ADVANCED PROPERTY MANAGEMENT STRATEGIES 95

circumventing Swift’s restriction.

This approach allows us to effectively manage mutable static-like properties for


generic types. It is particularly useful in scenarios where we need to maintain
state that is consistent across all instances of a particular type specialization
but varies between different specializations.
MyValue<Int>.a = 100

// Prints `100`
print(MyValue<Int>.a) // 100
// Prints `42` (the default value)
print(MyValue<String>.a)

MyValue<String>.a = 50
// Prints `50`
print(MyValue<String>.a)

By leveraging this pattern, we can extend the functionality of generic types in


Swift, making them more flexible and powerful while adhering to the language’s
type safety and restrictions.
96 ADVANCED PROPERTY MANAGEMENT STRATEGIES

Encapsulate property-specific behavior in property wrappers

Property wrappers in Swift are a powerful feature designed to encapsulate


shared logic, thereby enhancing code efficiency and maintainability. They
offer a modular approach to manage common behaviors and transformations
applied to properties, such as input validation, synchronization, or lifecycle
management, across your codebase.

A property wrapper abstracts the logic needed to read and write a property’s
value while adding additional behavior or side effects. This allows us to reuse
code effectively and maintain consistency in property management throughout
an application.

To implement a property wrapper, we can define a structure, class, or enu-


meration with the @propertyWrapper annotation. The property wrapper itself
manages how a property is stored and defines what actions are taken when the
property is accessed or modified.

Consider a use case where it’s crucial to ensure that all string inputs in a user
model are free from unwanted whitespace. Instead of manually trimming these
strings each time they’re set, we can create a SafeString property wrapper to
handle this automatically.
ADVANCED PROPERTY MANAGEMENT STRATEGIES 97

import Foundation

@propertyWrapper struct SafeString {


var wrappedValue: String {
didSet {
wrappedValue = wrappedValue
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

init(wrappedValue: String) {
self.wrappedValue = wrappedValue
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

struct User {
@SafeString var username: String
}

var user = User(username: " John Doe ")

// Prints `John Doe`


print(user.username)

Property wrappers reduce repetitive code by handling common transformations


or checks in one place, thus simplifying the property declarations in our models
or view controllers. Encapsulating property-specific behavior in wrappers helps
keep our business logic clean and focused by separating the concerns of data
storage and data transformation.
98 ADVANCED PROPERTY MANAGEMENT STRATEGIES
Protocols and generics

This chapter explores the sophisticated use of protocols and generics in Swift,
which are essential for developing flexible and secure software components.
You’ll learn how to customize protocols for specific class types and set precise
requirements for them. Additionally, you’ll discover how to boost protocol
capabilities with associated types and streamline your code with default imple-
mentations in protocol extensions.

These techniques enhance the simplicity and organization of your code while
maintaining its adaptability. You’ll also learn to build strong and organized
interfaces through protocol inheritance and the use of factory methods, which
simplify the creation of types that conform to protocols.

With practical examples to guide you, this chapter will arm you with the skills
to effectively use protocols and generics, greatly improving the strength and
ease of maintenance of your Swift applications.

99
100 PROTOCOLS AND GENERICS

Limit protocol adoption to class types


There are situations where a protocol should be restricted to class types, typ-
ically when object identity is crucial, such as when using identity comparison
(===). To achieve this, Swift provides the AnyObject constraint, which ensures
that only class types can conform to a specific protocol.

To limit protocol adoption to class types, add the AnyObject constraint to the
protocol’s definition. This makes the protocol ineligible for adoption by struc-
tures or enumerations, which do not support reference semantics.
protocol IdentifiableClass: AnyObject {
var id: Int { get }
}

Here, IdentifiableClass is constrained to class types by inheriting from


AnyObject. This is particularly useful for functionalities like identity
comparison, which is only meaningful for reference types.
class MyClass: IdentifiableClass {
var id: Int
init(id: Int) {
self.id = id
}
}

func areSameInstance(
_ lhs: IdentifiableClass, _ rhs: IdentifiableClass
) -> Bool {
return lhs === rhs
}

let object1 = MyClass(id: 1)


let object2 = MyClass(id: 1)
let object3 = object1

// Prints `false`
print(areSameInstance(object1, object2))

// Prints `true`
print(areSameInstance(object1, object3))

Reference semantics allow for more nuanced control over how objects are stored
and referenced, which is beneficial in complex applications where managing
PROTOCOLS AND GENERICS 101

object lifecycles is critical.

In versions of Swift prior to 5.4, the class keyword was used in place of
AnyObject for the same purpose. The modern and recommended approach is
to use AnyObject to clarify that the protocol is tied to class behavior, aligning
with Swift’s emphasis on clear and expressive code.
102 PROTOCOLS AND GENERICS

Define type-specific protocol requirements with the Self


keyword

In Swift, leveraging the Self keyword in protocol definitions allows for methods
that return a type specific to the implementing class or struct. This technique
greatly enhances protocol flexibility by enabling type-specific behaviors while
maintaining strict type safety. By using Self, protocols can define methods
that guarantee to return an instance of the implementing type, rather than a
more generic or unrelated type.

Using Self in protocols allows each conforming type to maintain its unique
identity and operations, facilitating the creation of reusable and modular code
that adheres to specific behaviors dictated by the protocol.

Consider a scenario where different types of documents and data structures


need a cloning functionality that duplicates an instance and returns a new
instance of the same type.

protocol Clonable {
func clone() -> Self
}

class Document: Clonable {


var content: String

required init(content: String) {


self.content = content
}

// Preserves the Document type


func clone() -> Self {
return Self(content: self.content)
}
}
PROTOCOLS AND GENERICS 103

class Spreadsheet: Clonable {


var data: [[String]]

required init(data: [[String]]) {


self.data = data
}

// Preserves the Spreadsheet type


func clone() -> Self {
return Self(data: self.data)
}
}

The Clonable protocol defines a clone() method that uses Self to specify that
the return type should match the caller’s type. Both Document and Spreadsheet
conform to Clonable. Each class implements clone() in a way that returns a
new instance of its own type, adhering to the protocol while preserving type
specificity.

Protocols using Self enable methods that return a specific, concrete type rather
than a generic protocol type. This is crucial for operations like cloning, where
the exact type needs to be preserved. Such protocols can be adopted by any
class or struct, making them extremely versatile and reusable across different
parts of a codebase without losing the benefits of type safety.

Designing protocols with type-specific methods using the Self keyword in


Swift is a powerful pattern for building flexible yet type-safe abstractions. This
approach allows protocols to be both generic in their application and precise
in their implementation, supporting robust and maintainable software devel-
opment practices.
104 PROTOCOLS AND GENERICS

Build versatile protocols with associated types

Swift’s protocols with associated types provide a robust framework for building
adaptable and modular components. This feature allows protocols to declare a
placeholder for a type that each conforming type can then define, enabling us
to write generic code that works across many data types. This approach not
only increases the reusability of our code but also maintains strong type safety,
making our applications easier to manage and scale.

Let’s look at an example with a Container protocol that is defined with an


associated type ItemType. This associated type acts as a placeholder that each
conforming type specifies, allowing the protocol to be used with a variety of
different data types.

protocol Container {
associatedtype ItemType
mutating func append(_ item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}

struct IntStack: Container {


var items = [Int]()

mutating func append(_ item: Int) {


items.append(item)
}

var count: Int {


return items.count
}

subscript(i: Int) -> Int {


return items[i]
}
}
PROTOCOLS AND GENERICS 105

struct StringQueue: Container {


var items = [String]()

mutating func append(_ item: String) {


items.append(item)
}

var count: Int {


return items.count
}

subscript(i: Int) -> String {


return items[i]
}
}

In our example, the IntStack struct conforms to Container with ItemType


specified as Int. It implements all required methods and properties, handling
a collection of integers. The append(_:) method adds an integer to the stack,
the count property provides the number of items in the stack, and the subscript
provides indexed access to its elements.

Similarly, the StringQueue struct specifies ItemType as String and conforms


to the Container protocol. It manages a queue of strings, implementing the
same functionalities as IntStack but tailored for string handling.

Using associated types with protocols allows for significant flexibility in how
protocols are implemented across various types, fostering code reusability
without sacrificing type safety. This technique is particularly useful in creating
data structures, where operations such as adding items, counting, or item re-
trieval need to be implemented differently depending on the data type handled.
By leveraging associated types, we can create more generic and modular APIs
that are easier to extend and maintain.
106 PROTOCOLS AND GENERICS

Provide default method implementations in protocol extensions

Protocol extensions in Swift offer a robust way to enhance code reusability and
modularity by providing default implementations for methods and properties
defined in protocols. This feature significantly simplifies the management of
complex codebases.

Using protocol extensions, we can define default behaviors that all conforming
types inherit, which is ideal for protocols with common functionalities that
would otherwise require repetitive implementation across multiple types. How-
ever, these extensions also retain the flexibility for any type to provide a more
specific implementation if the default does not suit its particular needs.

Let’s take a closer look at an example involving a Drawable protocol. This


protocol requires a draw() method, which we implement in a protocol extension
to provide a basic default action. Here’s how we can set it up.

protocol Drawable {
func draw()
}

extension Drawable {
func draw() {
print("Default drawing")
}
}

struct Circle: Drawable {


// Will use default implementation
}

struct Square: Drawable {


// Provide custom implementation of draw()
func draw() {
print("Drawing a square")
}
}

let shapes: [Drawable] = [Circle(), Square()]

// Prints `Default drawing` and `Drawing a square`


shapes.forEach { $0.draw() }
PROTOCOLS AND GENERICS 107

In this implementation, Circle uses the default draw() method provided by


the protocol extension, outputting “Default drawing.” In contrast, Square
overrides this with a custom method, reflecting its unique requirements by
printing “Drawing a square.” This example underscores the dual benefits of
protocol extensions: they provide a foundational behavior that eliminates the
need for code duplication, yet they allow for customization where necessary to
accommodate specific functionalities.

This blend of conformity and flexibility makes protocol extensions an essential


tool in Swift for creating clean, efficient, and manageable code.
108 PROTOCOLS AND GENERICS

Constrain protocol extensions to types that meet specific cri-


teria

The ability to add constraints to protocol extensions is a powerful tool that


ensures these extensions are applied selectively and appropriately, enhancing
both the safety and the utility of your code.

By adding constraints to a protocol extension, we can specify that the extension


should only apply to types that meet certain conditions, such as conforming to
another protocol or being a specific class. This allows us to tailor the behavior
of the extension to scenarios where it is meaningful and safe, avoiding compile-
time errors and enforcing a clear contract about what types the extension can
be used with.

Suppose we have a Discountable protocol that models items capable of having


a discount percentage. To leverage this in a broader context, such as calculating
the total discount across a collection of items, we can extend the Collection
type but restrict this extension to only those collections whose elements con-
form to Discountable.
protocol Discountable {
var discountPercentage: Double { get }
}

extension Collection where Element: Discountable {


func totalDiscount() -> Double {
return self
.map { $0.discountPercentage }
.reduce(0, +)
}
}

The where Element: Discountable constraint ensures that the totalDiscount()


method is only available on collections containing Discountable elements.
This is crucial because it allows the method to safely assume that every element
in the collection has a discountPercentage property.

Without this constraint, any attempt to use discountPercentage within the


extension would fail to compile, as Swift’s type system couldn’t guarantee the
PROTOCOLS AND GENERICS 109

presence of this property on all Element types.

Adding constraints clarifies the intent of the extension and which types it
should be used with, making the code easier to understand and maintain. It
also prevents misuse of the extension on incompatible types, thereby reducing
runtime errors and logical bugs.
110 PROTOCOLS AND GENERICS

Instantiate protocol-conforming types using static factory


methods

Swift 5.5 enhances protocol functionality by introducing the ability to use static
factory methods for protocol conformance, similar to enum instantiation. This
addition allows for a more declarative style of coding, akin to using enums, but
retains the flexibility and power of protocols.

Protocols in Swift allow different types to conform to the same interface, each
implementing functionalities in their unique way. While using enums or structs
with static methods might require less code for instantiation, protocols offer
the advantage of diverse implementations and the ability to define behavior
that can vary across different types.

Here’s an illustrative example:

protocol PaymentProcessor {
func process(amount: Double)
}

struct PayPalProcessor: PaymentProcessor {


func process(amount: Double) {
print("Processing $\(amount) with PayPal")
}
}

struct StripeProcessor: PaymentProcessor {


func process(amount: Double) {
print("Processing $\(amount) with Stripe")
}
}
PROTOCOLS AND GENERICS 111

extension PaymentProcessor where Self == PayPalProcessor {


static var paypal: Self { PayPalProcessor() }
}

extension PaymentProcessor where Self == StripeProcessor {


static var stripe: Self { StripeProcessor() }
}

// Instantiating and using the processors with dot syntax


let payment: PaymentProcessor = .paypal
payment.process(amount: 250.00)

let anotherPayment: PaymentProcessor = .stripe


anotherPayment.process(amount: 75.50)

In this example, PayPalProcessor and StripeProcessor conform to the


PaymentProcessor protocol. By leveraging constrained extensions, we
introduce static properties paypal and stripe that facilitate the creation of
these processor types using dot syntax. This setup simplifies the call sites
while maintaining the robustness of protocol-oriented programming.

Although this approach requires slightly more setup compared to using enums
or direct static methods on a struct, it combines the simplicity of calling these
types with the flexibility of protocols. This method is highly advantageous in
systems that require decoupled architectures, allowing developers to keep their
code modular and easy to extend or modify.
112 PROTOCOLS AND GENERICS

Implement hierarchical interfaces with protocol inheritance

Protocol inheritance in Swift allows us to build hierarchies of protocols, each


layer adding its own set of requirements or providing default implementations.
This powerful feature enhances code organization and reusability by enabling
a more structured and layered approach to protocol design.

To demonstrate protocol inheritance, let’s consider a vehicle management


system.
protocol Vehicle {
var make: String { get }
var model: String { get }
func start()
func stop()
}

protocol ElectricVehicle: Vehicle {


var batteryLevel: Int { get }
func chargeBattery()
}

struct TeslaModelS: ElectricVehicle {


var make: String = "Tesla"
var model: String = "Model S"
var batteryLevel: Int = 100

func start() {
print("Tesla Model S is starting.")
}

func stop() {
print("Tesla Model S is stopping.")
}

func chargeBattery() {
print("Charging the battery.")
}
}

In this example, we define a base Vehicle protocol that specifies general


requirements for all vehicles, such as properties for make and model, and
functions for start() and stop(). We then create a more specialized protocol,
ElectricVehicle, which inherits from Vehicle. This inheritance allows
PROTOCOLS AND GENERICS 113

ElectricVehicle to adopt all the requirements of Vehicle while adding


specific requirements related to electric vehicles, such as a batteryLevel
property and a chargeBattery() method.

By conforming to ElectricVehicle, TeslaModelS inherits the requirements


from both Vehicle and ElectricVehicle, and provides concrete implementa-
tions for all required methods and properties.

Protocol inheritance is a cornerstone of protocol-oriented programming in


Swift, providing a scalable way to design and implement our APIs. By defining
protocol hierarchies, we can create a clear and organized structure for our code,
making it easier to manage and extend.
114 PROTOCOLS AND GENERICS

Conditionally add methods to generic types in extensions


Swift’s type system allows us to enhance generic types with extensions that have
specific constraints. Further improved in Swift 5.3, this feature now lets us add
specialized methods based on certain conditions. This is incredibly useful for
applying different behaviors to the same type, depending on the situation.

Consider a scenario where we want to add different methods to Collection in


an extension based on different constraints. We can achieve this by specifying
conditions right in the method declarations within the extension.
extension Collection {
func sum() -> Element where Element: Numeric {
self.reduce(0, +)
}

func concatenate() -> String where Element: StringProtocol {


self.reduce("") {
$0 + $1.description
}
}
}

let numbers = [1, 2, 3, 4, 5]


let total = numbers.sum()

let words = ["Hello", "World"]


let sentence = words.concatenate()

In this example, the extension is applied to any Collection. However, the


sum() method is only accessible to collections whose elements conform to the
Numeric protocol, enabling arithmetic summation. The concatenate() method,
on the other hand, is available to collections with elements that conform to
StringProtocol, facilitating string concatenation.

This approach offers a high degree of flexibility by enabling the addition of


specific functionalities to types only when they meet certain conditions, thus
ensuring type safety and preventing misuse. By conditionally adding methods
within a single extension rather than multiple extensions, we can maintain a
cleaner codebase that is easier to manage.
PROTOCOLS AND GENERICS 115

Accurately identify the dynamic type of values in generic con-


texts

In Swift, understanding how to accurately identify the dynamic type of a value,


especially when using generics and protocols, can be challenging. This com-
plexity often surfaces when working with functions that are meant to handle
values generically while adhering to a specific protocol.

Let’s begin by defining a simple protocol and a conforming struct, then creating
an instance of the struct bound to the protocol type.
protocol Animal {}
struct Dog: Animal {}
let dog: Animal = Dog()

In this setup, dog is an instance of Dog but is referred to by the type of the Animal
protocol.

Now, suppose we want to create a generic function that prints the type of the
value it receives.
func printGenericInfo<T>(for value: T) {
let typeDescription = type(of: value)
print("Value of type '\(typeDescription)'")
}

Calling this function with our dog instance might intuitively be expected to
output Value of type 'Dog'. However, due to Swift’s type system behavior in
generic contexts, it actually prints Value of type 'Animal'.
// Prints `Value of type 'Animal'`
printGenericInfo(for: dog)

In this scenario, the generic placeholder T is resolved to the static type Animal,
as dog is declared as an Animal. This means that within the function, type(of:
value) returns Animal, not Dog.

To circumvent this and capture the actual runtime type of the variable, we need
to cast the variable to Any before passing it to type(of:). This strips away any
compile-time type information, allowing us to access the true dynamic type.
116 PROTOCOLS AND GENERICS

func printGenericInfo<T>(for value: T) {


let typeDescription = type(of: value as Any)
print("Value of type '\(typeDescription)'")
}

// Prints `Value of type 'Dog'`


printGenericInfo(for: dog)

By casting value to Any, we instruct Swift’s type system to evaluate the type at
runtime, thereby obtaining the correct dynamic type, Dog, in this case. This
technique is particularly useful in generic programming where type flexibility
and accuracy are critical, and it helps maintain clarity when dealing with types
that conform to one or more protocols.
PROTOCOLS AND GENERICS 117

Create clear semantic distinctions with phantom types

Phantom types are a powerful feature in Swift that use generics to enforce
stricter type safety without impacting runtime behavior. These types are re-
ferred to as “phantom” because they do not appear in the runtime values of in-
stances but play a crucial role in enforcing compile-time constraints. Phantom
types are ideal for differentiating types that share a common underlying struc-
ture but are conceptually distinct.
struct Tagged<Tag, Value: Equatable>: Equatable {
let value: Value
}

// Defining specific tagged types


typealias UserID = Tagged<UserTag, Int>
typealias ProductID = Tagged<ProductTag, Int>

// Tags used to distinguish different concepts


enum UserTag {}
enum ProductTag {}

struct User {
let id: UserID
}

struct Product {
let id: ProductID
}

// Usage
let user = User(id: UserID(value: 123))
let product = Product(id: ProductID(value: 456))

// Checking equality
let anotherUser = User(id: UserID(value: 123))
print(user.id == anotherUser.id) // true

// The following comparison will not compile


print(user.id == product.id)

In the example, the Tagged struct is a generic container with two parameters:
a Tag and a Value. The Value must conform to Equatable to enable equality
checks. The Tag serves as a phantom type that does not directly influence the
stored value but provides a way to differentiate between different uses of the
118 PROTOCOLS AND GENERICS

same underlying type.

For instance, UserID and ProductID are defined using different tags, making
them incompatible for operations like assignment or comparison, despite both
storing an Int. This differentiation prevents logical errors such as accidentally
using a user ID where a product ID is expected, thus enforcing type safety
through compile-time checks.

Phantom types in Swift allow for the creation of semantically distinct types that
enhance the robustness and clarity of the code. By using phantom types, we
can prevent a wide range of bugs associated with type confusion, particularly
in complex systems where multiple entities might share the same base data
type but are used in different contexts.
Collection transformations and optimiz-
ations

In this chapter, we’ll delve into the art of manipulating and optimizing collec-
tions in Swift. From arrays to dictionaries, you’ll explore a variety of techniques
that enhance the functionality and performance of these fundamental data
structures. We’ll cover everything from sorting arrays with closures and map-
ping with key paths to efficiently transforming dictionary values and utilizing
lazy collections for resource management.

This chapter will equip you with the skills to handle large datasets, implement
type-safe operations, and ensure your collections are both efficient and easy to
manage. By the end of this chapter, you’ll be well-versed in leveraging Swift’s
powerful features to manipulate collections in a way that is both memory-
efficient and optimized for performance, making your applications faster and
more reliable.

119
120 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Sort arrays using comparison operators as closures


Sorting elements in an array using comparison operators as closures is a power-
ful example of Swift’s ability to utilize operators in flexible and expressive ways.
This technique allows for concise and readable code when ordering collections.
Let’s take a closer look at how it works.

When we have an array of elements and we want to sort it, we typically call
the sorted(by:) method on the array. This method sorts the elements of the
array using a closure that we provide. The closure must take two elements of
the array as its parameters and return a Boolean value indicating whether the
first element should be ordered before the second.

In Swift, most of the basic comparison operators (<, >, <=, >=) are actually
implemented as global generic functions. These functions match the closure
signature (Element, Element) -> Bool. This is why we can directly use these
operators as shorthand for closures when sorting.
let numbers = [3, 1, 4, 1, 5, 9, 2]

//
let numbersInAscendingOrder = numbers.sorted(by: <)

// Prints `[1, 1, 2, 3, 4, 5, 9]`


print(numbersInAscendingOrder)

let numbersInDescendingOrder = numbers.sorted(by: >)

// Prints `[9, 5, 4, 3, 2, 1, 1]`


print(numbersInDescendingOrder)

In each case, sorted(by:) uses the operator as a function. This works because,
in Swift, functions are first-class citizens and can be passed as arguments to
other functions. The < and > operators are treated as closures that take two Int
parameters and return a Bool, perfectly matching the expected signature for
sorting.

This approach not only simplifies the syntax but also enhances code readability.
It effectively leverages Swift’s functional programming capabilities, making
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 121

our sort operations both elegant and intuitive. By using operators as functions,
we can eliminate the need for more verbose closures, making our intent clear
with less code.
122 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Change a range of array values in a single operation

We can use subscript syntax in Swift to get or set values by index without
needing separate methods for setting and retrieval. Swift also allows us to
replace a range of values in an array with a new set of values using subscripts.
This feature is incredibly flexible because the new set of values doesn’t have to
be of the same length as the range we are replacing. This means we can replace
multiple items with fewer items or vice versa.

Suppose we have a bookList array that represents the books we intend to read.
We realize our schedule has become quite busy, and we decide to shorten our
reading list. We choose to replace “The Great Gatsby”, “To Kill a Mockingbird”,
and “1984” with just one book, “Moby Dick”. Here’s how we can do it with
Swift’s subscript syntax.
var bookList = [
"Pride and Prejudice",
"The Great Gatsby",
"To Kill a Mockingbird",
"1984",
"The Catcher in the Rye"
]

bookList[1...3] = ["Moby Dick"]

/* Prints `[
"Pride and Prejudice",
"Moby Dick",
"The Catcher in the Rye"
]` */
print(bookList)

In this example, three books were replaced by just one, effectively shorten-
ing the list and making our reading plan more manageable. The array has
automatically adjusted its size to accommodate the new set of values.

This feature is particularly useful in scenarios where we need to update our


data dynamically. It offers a high level of flexibility and makes our code concise
and readable. Whether we are dealing with a shopping list, a set of tasks, or
any collection of items, being able to replace a range of values with a new set,
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 123

regardless of their count, can significantly simplify the process of managing


arrays.
124 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Simplify array mapping with key paths


Starting with Swift 5.2, key paths can be used more dynamically, including
their ability to be automatically converted into functions. This enhancement
significantly improves the ease of working with collections of objects.

Here’s an example to illustrate how key paths can be used with methods like
map() on an array. Consider an array of Person objects, each with a name prop-
erty.
struct Person {
var name: String
var age: Int
}

let people = [
Person(name: "Alice", age: 30),
Person(name: "Bob", age: 25),
Person(name: "Charlie", age: 35)
]

Traditionally, to create an array of names, we would use map() with a closure.


let names = people.map { $0.name }

However, with a key path, this becomes much more concise.


let names = people.map(\.name)

Here, \Person.name is a key path that directly references the name property of a
Person object. When passed to map(), Swift automatically treats this key path
as a function that extracts the name from each Person in the array.

This approach is not only more succinct but also enhances readability. It’s
particularly beneficial when dealing with arrays of complex objects where we
need to transform or extract specific properties.
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 125

Instantiate arrays with a specified capacity without default val-


ues
When creating arrays, especially large ones, it can be inefficient to fill them
with default values if those values are immediately overwritten with actual
data. The init(unsafeUninitializedCapacity:initializingWith:) method
introduced in Swift 5.2 addresses this issue by allowing us to create an array
with a specified capacity and directly control how its elements are initialized.
This feature is particularly beneficial for performance-sensitive applications
where initializing default values is unnecessary and can be a performance
bottleneck.

The initializer gives us direct access to the underlying buffer of the array, where
we can manually set the initial values via a closure. This closure receives a
buffer and an inout parameter that tracks the count of initialized elements,
thus providing precise control over the initialization process.

Here’s an example of how this initializer can be used.


let capacity = 10
var array = Array<Int>(
unsafeUninitializedCapacity: capacity
) { buffer, initializedCount in
for i in 0..<capacity {
buffer[i] = i * 2
}
initializedCount = capacity
}

// Prints `[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]`


print(array)

In the above code sample, an array of integers is created with a capacity of 10,
where each element is initialized to twice its index without any pre-filled default
values. This approach optimizes the array creation process, especially when the
default initialization is not required, offering a significant performance boost
in critical scenarios.
126 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Specify default values in dictionary subscripts


When working with dictionaries in Swift, it’s common to encounter situations
where a key might not yet exist, particularly in cases where values are dynamic-
ally aggregated or updated. Swift provides a convenient way to handle such
scenarios using the subscript method with a default value. This approach allows
us to specify a fallback value for keys that are not present in the dictionary.

Consider a dictionary storing scores of participants in a game. To retrieve


a score for a participant, one might typically check if the key exists to avoid
runtime errors. However, Swift’s dictionary subscript with a default value
simplifies this pattern.
var scores = ["Alice": 10, "Bob": 15]

let aliceScore = scores["Alice", default: 0] // 10


let charlieScore = scores["Charlie", default: 0] // 0

In this example, aliceScore retrieves the value of 10 because “Alice” exists


in the dictionary. Conversely, “Charlie” does not have a score entry, so
charlieScore defaults to 0. This method is particularly efficient for scenarios
like initializing and updating counters, default configurations, or handling
optional data without the overhead of conditional checking.

This feature not only minimizes the need for boilerplate code but also ensures
that our operations on dictionary elements are safe and concise. By setting a
default value right within the subscript, Swift lets us focus more on business
logic rather than on checking for the existence of keys, making our code cleaner.
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 127

Refine dictionary data retrieval with type-safe generic sub-


scripts

Generics enhance the flexibility and power of our coding by enabling us to


create adaptable, reusable functions and types. One particularly interesting
use of generics in Swift is through generic subscripts, which allow for type-safe
access to elements within a collection or values within a dictionary, thereby
improving both the safety and clarity of the code.

A generic subscript is defined by specifying a type placeholder in the subscript


declaration, similar to how we would define a generic function or type.
struct JSON {
private var storage: [String: Any]

init(data: [String: Any]) {


self.storage = data
}

subscript<T>(key: String) -> T? {


return storage[key] as? T
}
}

let json = JSON(data: ["name": "Alice", "age": 30])

let name: String? = json["name"]


let age: Int? = json["age"]

In this implementation, JSON is a struct encapsulating a dictionary. The generic


subscript facilitates the retrieval of values by key with automatic type inference
and casting. For instance, when accessing json["name"], we explicitly specify
the type String, prompting Swift to attempt casting the value to that type. If
the cast is successful, the value is returned, if not, we’ll get nil.

The real value of generic subscripts shines in scenarios where the data type
might vary, such as when parsing JSON or managing data from diverse ex-
ternal sources. They ensure that the data handling remains type-safe, reducing
runtime errors and increasing code reliability. The flexibility of generic sub-
scripts allows us to work with data structures in a more intuitive and error-free
128 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

manner, particularly in applications that require high levels of data integrity


and type safety.
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 129

Safely modify dictionary values with optional chaining


Optional chaining in Swift provides a graceful way to access and modify values
in a dictionary where the value might be nil. This approach is invaluable when
dealing with nested data structures or optional types, as it simplifies the code
by eliminating the need for verbose unwrapping checks.

Consider a dictionary mapping book ISBNs to arrays of review scores. Not


every book has reviews, so the corresponding array might be nil for some
ISBNs.
var bookReviews = [
"ISBN-001": [5, 4, 5, 3],
"ISBN-002": [4, 5, 4]
]

To safely update a review score for a book using optional chaining, we check if
the key and array index exist before making any changes.
bookReviews["ISBN-001"]?[0] = 3

Here, the optional chaining (?) ensures that the operation only proceeds if
ISBN-001 is found in the dictionary. The first score is then updated to 3. If
ISBN-001 were absent, the operation would simply do nothing, avoiding any
runtime errors.

Similarly, we can increment a score without worrying about the presence of


the key.
bookReviews["ISBN-002"]?[0] += 1

This line attempts to increment the first review score for ISBN-002 if it ex-
ists, showcasing how optional chaining supports not just safe access but also
arithmetic operations directly.

Using optional chaining in this way allows for concise, safe interactions with
complex data structures in Swift, ensuring our code remains readable and
error-free while handling optional values seamlessly.
130 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Capture previous values on dictionary updates

When we need to update a value for a specific key in a Swift dictionary, we can
use the updateValue(_:forKey:) method. It updates the value stored in the
dictionary for the given key, or adds a new key-value pair if the key does not
exist. An important aspect of updateValue(_:forKey:) is that it returns the
original value that was stored before the update. If the key was not present, it
returns nil. This is useful if we need to check what the previous value was or if
we need to perform additional operations based on the old value.

Imagine we have a system that manages user accounts for an application. Each
user has a set of permissions, and we need to update these permissions based
on certain criteria or requests. We also want to track these changes for auditing
purposes, to ensure that any modifications are traceable.

var userPermissions = [
"Alice": "read",
"Bob": "write",
"Charlie": "read-write"
]

func updatePermission(
for user: String, to newPermission: String
) {
if let oldPermission = userPermissions.updateValue(
newPermission, forKey: user
) {
print("""
Permission for \(user) changed \
from \(oldPermission) to \(newPermission)
""")
} else {
print("""
New user \(user) added with permission: \(newPermission)
""")
}
}

// Prints `Permission for Alice changed from read to read-write`


updatePermission(for: "Alice", to: "read-write")

// Prints `New user Dave added with permission: read`


updatePermission(for: "Dave", to: "read")
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 131

In this example, updatePermission() checks the result of updateValue(_-


:forKey:). If a previous value exists, it indicates that the user’s permissions
were updated, and this change is logged. If nil is returned, it signifies that
a new user has been added to the dictionary, and this addition is recorded.
This method is particularly effective in scenarios requiring precise tracking
of changes to data, such as modifying user roles or permissions within an
application.

The ability to retrieve the previous value before an update enables us to im-
plement additional checks or actions conditionally. For instance, we might
want to trigger a specific workflow only if the previous permission was below a
certain level, or we might avoid logging changes if the old and new values are
identical, thus optimizing performance and reducing clutter in logs.
132 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Transform dictionary values efficiently with mapValues()


We can efficiently transform values in a Swift dictionary using the mapValues()
method. This method provides a clean and expressive way to iterate over each
value and apply transformations, creating a new dictionary with the updated
values while preserving the original keys.

Let’s consider a scenario where we have a dictionary representing a collection


of products and their prices, and we want to apply a discount to each price.
var products = [
"Laptop": 1200.00,
"Smartphone": 800.00,
"Headphones": 150.00
]

let discountedProducts = products.mapValues { price in


price * 0.90
}

/* Prints `[
"Laptop": 1080.0,
"Smartphone": 720.0,
"Headphones": 135.0
]` */
print(discountedProducts)

Here, mapValues() takes a closure that adjusts each price by reducing it by 10%.
The result is a new dictionary where each product retains its original key, but
the values reflect the discounted prices.

This method is particularly useful for operations where only the values of the
dictionary need adjustments without affecting the keys, ensuring that the data
structure remains consistent while allowing efficient transformations. This
approach keeps our original dictionary unchanged, providing a safe way to
manage data modifications without side effects.
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 133

Extend collections whose elements conform to certain protocols

Extending collections with custom protocols using conditional conformances in


Swift is a powerful way to add functionality to collections when their elements
conform to a certain protocol. This approach allows us to write generic, reusable
code that behaves differently based on the types contained within the collection.

Imagine a scenario where different data types need a standardized way of


presenting a summary. By defining a protocol, Summarizable, we can ensure
that any conforming type implements a method to provide a concise summary
of its instance.
protocol Summarizable {
func summary() -> String
}

struct Book: Summarizable {


var title: String
var author: String
var pageCount: Int

func summary() -> String {


return "\(title) by \(author), \(pageCount) pages"
}
}

struct Movie: Summarizable {


var title: String
var director: String
var releaseYear: Int

func summary() -> String {


return """
\(title), directed by \(director), \
released in \(releaseYear)
"""
}
}

To extend this functionality to collections such as arrays, we can conditionally


conform the Array type to the Summarizable protocol when its elements are
Summarizable. This allows the collection itself to be treated as a summarizable
entity, aggregating the summaries of its contents into a single descriptive string.
134 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

extension Array: Summarizable where Element: Summarizable {


func summary() -> String {
let summaries = self.map { $0.summary() }
return summaries.joined(separator: "\n")
}
}

This extension empowers us to apply the summary() method to any array con-
taining Summarizable items, thereby encapsulating the individual summaries
into a cohesive report.
let books = [
Book(
title: "1984",
author: "George Orwell",
pageCount: 328),
Book(
title: "To Kill a Mockingbird",
author: "Harper Lee",
pageCount: 281
)
]
print(books.summary())

let movies = [
Movie(
title: "Inception",
director: "Christopher Nolan",
releaseYear: 2010
),
Movie(
title: "The Matrix",
director: "Lana and Lilly Wachowski",
releaseYear: 1999
)
]
print(movies.summary())

By adopting this strategy, we harness Swift’s type system to build adaptable


and reusable code structures that significantly streamline interactions with
collections.
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 135

Expose collection data through custom read-only subscripts


Subscripts in Swift provide a way to access elements within a collection, se-
quence, or a custom data structure succinctly. Read-only subscripts, in par-
ticular, enhance data security by allowing data access without permitting its
modification, which is crucial for maintaining the integrity of the data within
controlled environments.

Consider a structure representing weekly temperatures that should be access-


ible but not modifiable externally.
struct WeeklyTemperatures {
private var temperatures: [Int] = [
22, 23, 25, 24, 26, 27, 23
]

subscript(day: Int) -> Int {


assert(
day >= 0 && day < temperatures.count,
"Index out of range."
)
return temperatures[day]
}
}

let thisWeek = WeeklyTemperatures()


// Prints `25`
print(thisWeek[2])

In this implementation, the WeeklyTemperatures structure stores daily tem-


peratures privately, preventing direct external modifications. The read-only
subscript, which lacks a set block, ensures data is accessible by day index. It
includes an assertion to guard against out-of-range access, thereby avoiding
potential runtime errors.

Employing read-only subscripts ensures that data exposure does not comprom-
ise the data’s integrity. It allows us to safeguard critical data while providing
necessary access, thus striking a balance between functionality and data pro-
tection in Swift applications. This approach is particularly valuable in environ-
ments where data consistency and security are paramount.
136 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Accumulate collection elements into a single value in a memory-


efficient way
The reduce(into:) function in Swift is a powerful tool that allows us to trans-
form a collection into a single value in a more memory-efficient way than
its counterpart reduce(_:_:). This function is particularly useful when we
are dealing with complex data transformations or accumulating results in a
collection, such as an array or dictionary.

The reduce(into:) method takes two parameters: an initial result, which is


the starting value for the reduction, and a closure that specifies how to combine
the elements of the collection into the initial result.

The closure’s parameters are a reference to the accumulating result and the
next element in the collection. Unlike reduce(_:_:), which returns a new value
each time its closure is called, reduce(into:) modifies the initial result in
place, which can lead to performance improvements by reducing the need for
allocating new objects or arrays.

Let’s say we have an array of integers and we want to create a frequency dic-
tionary that counts how many times each integer appears. Here’s how we can
use reduce(into:) to achieve this.
let numbers = [1, 2, 1, 3, 2, 1, 4, 2]

let frequency = numbers.reduce(into: [:]) { (counts, number) in


counts[number, default: 0] += 1
}

// Prints `[1: 3, 2: 3, 3: 1, 4: 1]`


print(frequency)

In this example, reduce(into:) starts with an empty dictionary and populates


it by iterating over each number in the array. The closure modifies the counts
dictionary directly, adding keys or incrementing their values.

Since reduce(into:) mutates the result in place, it avoids the cost of repeatedly
creating new intermediate values, which can lead to better performance for
large or complex data collections. For many accumulation tasks, reduce(into:)
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 137

can lead to clearer and more concise code compared to loops or other higher-
order functions.
138 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS

Leverage lazy collections for efficient use of resources

In Swift, leveraging the lazy property when dealing with large collections can
lead to substantial performance gains by deferring computation until necessary.
This capability is particularly valuable for operations that are computationally
intensive or resource-demanding.

Consider a scenario involving a large array of image URLs that require down-
loading and processing. Instead of downloading all images at once, which
would be resource-intensive and potentially unnecessary if not all images are
used, we can defer these operations using lazy.

import Foundation

// Large collection of image URLs


let imageUrls = [
"url1.jpg", "url2.jpg", "url3.jpg", "url4.jpg"
]

// Lazy collection for deferred image download


let lazyImages = imageUrls.lazy.map { downloadImage(from: $0) }

func downloadImage(from url: String) -> Data {


// Hypothetical download logic simulated by a print statement
print("Downloading the image from \(url)...")
return Data()
}

// Access the first 2 images to demo deferred execution


for image in lazyImages[0..<2] {
print(image)
}

In this setup, lazyImages is configured to perform the downloadImage() func-


tion lazily. This function is invoked only when an element is specifically ac-
cessed. In the example, downloadImage() is called only for the first two URLs
when they are iterated over in the loop.

The primary benefit of using lazy collections in Swift is the enhanced perform-
ance and reduced memory usage they offer. Computations are only executed
for elements that are actively accessed, effectively minimizing unnecessary
COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS 139

resource consumption.

However, this approach comes with certain considerations. Deferred error


handling means that any errors associated with computations are delayed,
potentially complicating error management in our applications. Additionally,
debugging lazy computations can be more challenging because the deferred
execution model does not follow a straightforward, sequential flow. This makes
it difficult to track when and where operations occur. It’s also important to
note that each access to elements in a lazy collection triggers their computation.
Without caching results, repeated access to the same elements can become
inefficient.
140 COLLECTION TRANSFORMATIONS AND OPTIMIZATIONS
String manipulation techniques

This chapter explores foundational techniques for manipulating strings in Swift,


essential for creating effective and user-friendly applications. You’ll learn how
to handle string indices for precise manipulations, manage multiline strings
to avoid unwanted newlines, and use raw strings to simplify handling special
characters.

Additionally, the chapter delves into enhancing expressiveness and functional-


ity through custom string interpolation, allowing for more flexible and tailored
text output. We also cover dynamic string concatenation techniques that adapt
to various programming needs.

By mastering these string handling techniques, you will enhance the readability
and maintainability of your code, ensuring that your applications can efficiently
process and display text data. These practices provide the essential tools for
any Swift developer aiming to build robust, navigable, and efficient text-based
features in their projects.

141
142 STRING MANIPULATION TECHNIQUES

Manipulate strings using string indices

Using string indices in Swift effectively can be crucial for manipulating and
accessing specific parts of strings. Swift’s String type uses String.Index for
indexing, which respects Unicode graphemes, ensuring that even complex
characters are correctly handled.

Let’s look at how to use string indices to retrieve, insert, and remove characters
or substrings within a string.

var greeting = "Hello, World!"

// Accessing characters using indices


let index = greeting.index(greeting.startIndex, offsetBy: 7)
let character = greeting[index]
// Prints `W`
print(character)

// Inserting a character at a specific index


greeting.insert("!", at: greeting.endIndex)
// Prints `Hello, World!!`
print(greeting)

// Removing a character at a specific index


greeting.remove(at: index)
// Prints `Hello, orld!!`
print(greeting)

// Inserting a substring at a specific index


let commaIndex = greeting.index(greeting.startIndex, offsetBy: 5)
greeting.insert(contentsOf: " dear", at: commaIndex)
// Prints `Hello dear, orld!!`
print(greeting)

// Accessing a substring using a range of indices


let startIndex = greeting.index(greeting.startIndex, offsetBy: 6)
let endIndex = greeting.index(startIndex, offsetBy: 3)
let substring = greeting[startIndex...endIndex]
// Prints `dear`
print(substring)

In this example, we manipulate a string by accessing and modifying it at various


indices. We use index(_:offsetBy:) to compute an index a certain number
of positions away from a given index. This method is crucial for finding the
STRING MANIPULATION TECHNIQUES 143

position within a string while respecting Unicode graphemes. We can insert or


remove characters using the computed indices, allowing us to modify the string
directly without reconstructing it entirely. By creating a range using indices,
we can easily extract substrings. This is useful for slicing strings into segments
for further processing.

Understanding and using string indices allows for more precise string manip-
ulations and can help prevent errors related to handling multi-byte Unicode
characters, ensuring that operations on strings are both safe and efficient.
144 STRING MANIPULATION TECHNIQUES

Avoid unwanted newlines in multiline strings


The ability to create multiline strings in Swift can make the management of
long text data straightforward and readable. Sometimes, for readability or
organizational purposes, we might want to split a long string into multiple lines
of code. However, we might not want these line breaks to appear in the actual
string.

By placing a \ at the end of a line, we can tell Swift to ignore the newline
character in our code, effectively joining the line with the next one.
let string1 = """
This is a very long string
that should appear on a single line.
"""

let string2 = """


This is a very long string \
that appears on a single line.
"""

// Prints `This is a very long string


// that should appear on a single line.`
print(string1)

// Prints `This is a very long string that appears on a single line.`


print(string2)

In the above example, the backslash at the end of the first line escapes the
newline character, and the string is treated as if it’s written: “This is a very long
string that appears on a single line.”

Swift’s multiline strings respect the indentation we use, which can be useful,
but might not always align with the formatting we want in the actual string.
Using \, we can manage this explicitly, ensuring that the indentation in our
code doesn’t affect the string’s content.
STRING MANIPULATION TECHNIQUES 145

Bypass the need to escape special characters using raw strings


Swift’s introduction of raw strings in version 5.0 marked a significant enhance-
ment in handling string literals, especially those containing special characters
or requiring complex formatting. Raw strings allow us to treat special char-
acters such as backslashes and quotes as regular characters, bypassing the
usual need for escaping. This capability is particularly valuable when handling
formatted text such as JSON, where readability can be significantly improved.

Consider a scenario where we are dealing with a JSON string that includes
special characters. Traditionally, we’d have to escape the quotes, leading to a
somewhat cluttered and less readable string. With raw strings, we can present
the JSON in its native format, enhancing readability and maintainability.
let jsonString = """
{\"name\": \"John\", \"age\": 30, \"city\": \"New York\"}
"""

let rawJsonString = #"{"name": "John", "age": 30, "city": "New York"}"#

Here, the # symbols used before and after the quotes denote the boundaries of
the raw string, allowing the interior quotes to be included without escaping.
This makes the JSON string much clearer and easier to understand at a glance.

It’s important to note that while raw strings improve clarity in cases like these,
they also disable string interpolation by default. This means any expressions for
embedding values directly within the string will need to be handled differently,
using an extended syntax if interpolation is necessary.
146 STRING MANIPULATION TECHNIQUES

Craft visually expressive strings with emoji

Emoji can add a dynamic and engaging element to our applications. Swift’s
comprehensive support for Unicode allows us to integrate emoji into strings
effortlessly, either by directly inserting them into string literals or using Unicode
scalars for more precise representations.

For example, adding an emoji to a string can be as straightforward as inserting


it directly into the string.
let message = "Hello, world! �"

Alternatively, for more control or when working with code that needs to be
universally understood without rendering issues, we can insert emojis using
their Unicode scalar values. This method ensures that the emoji displays
correctly across different platforms and development environments.
let emojiString = "Yummy ice cream: \u{1F366}"

// Prints `Yummy ice cream: �`


print(emojiString)

When it comes to character counting, Swift’s approach is particularly developer-


friendly. Each emoji, regardless of the number of Unicode scalars it comprises,
is treated as a single character. This is due to Swift’s use of extended grapheme
clusters, which treat a sequence of one or more Unicode scalars that combine
to form a single human-readable character as one logical character.
let faceWithSpiralEyes = "\u{1F635}\u{200D}\u{1F4AB}"

// Prints `��`
print(faceWithSpiralEyes)

// Prints `1`
print(faceWithSpiralEyes.count)

This capability is particularly advantageous because it simplifies the processing


of string lengths and substring operations, ensuring that operations involving
emojis are as intuitive and error-free as those involving standard characters.
By understanding and utilizing these features, we can create rich, engaging
STRING MANIPULATION TECHNIQUES 147

text-based content that includes colorful and expressive emoji characters easily.
148 STRING MANIPULATION TECHNIQUES

Take advantage of unicode normalization for string comparis-


ons
In Swift, comparing strings for equality takes into account the Unicode ca-
nonical representation, ensuring that different Unicode representations of the
same characters are considered equal. This feature is essential for handling
user input and data that might use different Unicode encodings.

Consider an example where we compare the names of venues, which might


include accented characters.
// `n` followed by combining tilde
let venue1 = "El Ni\u{006E}\u{0303}o"

// `ñ` as a single Unicode scalar


let venue2 = "El Niño"

// Prints `The venue names are equal.`


if venue1 == venue2 {
print("The venue names are equal.")
} else {
print("The venue names are not equal.")
}

Here, venue1 is constructed using a regular n (\u{006E}) and a combining tilde


(\u{0303}), while venue2 uses the single character ñ. Swift recognizes these
strings as equal because it compares them using their normalized forms.

This capability of Swift ensures that your application’s string comparisons are
both robust and reliable, even when dealing with complex, multi-character
Unicode representations. It simplifies handling internationalized text, making
it easier to compare and process strings that may appear in varied forms due
to different input methods or data sources. This is particularly beneficial in
contexts like sorting names, filtering search results, or matching user inputs
against database records, where consistent and accurate string handling is
crucial.
STRING MANIPULATION TECHNIQUES 149

Utilize dynamic string concatenation techniques


Concatenating strings is a fundamental aspect of programming in Swift, where
we often need to combine multiple string values. While basic operations like +
and += are straightforward and commonly used, they may not always be the
most efficient, especially in performance-sensitive contexts or when dealing
with large amounts of data.

Swift provides several methods to concatenate strings dynamically and effi-


ciently. + and += are simple and effective for small-scale concatenations or
when performance is not a critical concern.
var message = "Hello"
message += ", World!"
// Prints `Hello, World!`
print(message)

For scenarios where strings are built incrementally, using the append() method
on a String instance can be more performant, as it modifies the original string
in place without creating a new instance.
var greeting = "Hello"
greeting.append(", World!")
// Prints `Hello, World!`
print(greeting)

For constructing strings from mixed data types or including expressions directly
within our strings, string interpolation can be an elegant and readable option.
It’s clean and clear, especially when integrating different data types.
let name = "Swift"
let year = 2024
let description = "\(name) is popular in \(year)."
// Prints `Swift is popular in 2024.`
print(description)

Each method has its specific advantages, and the choice of which to use often
depends on the particular needs of our code. While + and += are fine for simple
tasks, append() and string interpolation provide more power for intensive
string manipulations. Understanding and using these techniques appropriately
can significantly enhance the performance and clarity of our Swift programs.
150 STRING MANIPULATION TECHNIQUES

Define custom string interpolation behavior

In Swift, string interpolation is a convenient way to construct new strings that


include the values of variables, constants, literals, and expressions. Beyond
simple variable substitutions, Swift’s string interpolation can be customized to
include advanced formatting directly within the string literals.

To utilize advanced string interpolation, we can extend Swift’s String.StringInterpolat


struct. This allows us to define custom interpolation behavior tailored to
specific types or formatting requirements.

Here’s an example that demonstrates how to add custom string interpolation


for a Date object to format it within string literals.

import Foundation

extension String.StringInterpolation {
mutating func appendInterpolation(
_ value: Date, dateFormat: String
) {
let formatter = DateFormatter()
formatter.dateFormat = dateFormat
let dateString = formatter.string(from: value)
appendLiteral(dateString)
}
}

let today = Date()


let formattedString = """
Today's date is \(today, dateFormat: "yyyy-MM-dd")
"""
print(formattedString)

In this example, we extend String.StringInterpolation to include a method


that takes a Date object and a date format string. Inside this method, we use a
DateFormatter to transform the date into a string based on the specified format.
When creating a string, we can directly specify how we want our Date objects
to be formatted using the custom interpolation. This method makes the code
cleaner and reduces the need for repetitive formatting code throughout our
application.
STRING MANIPULATION TECHNIQUES 151

This approach to string interpolation is powerful, allowing us to embed complex


formatting logic directly within string literals, making our code more readable
and maintainable. By leveraging Swift’s type system and extension capabilities,
we can create highly customizable string interpolations suited to the specific
needs of our application.
152 STRING MANIPULATION TECHNIQUES

Leverage collection capabilities to transform strings


Transforming strings isn’t limited to simple replacements or concatenations,
we can also use higher-order functions like map(), filter(), and reduce() to
perform more complex transformations. These functions provide powerful
ways to manipulate strings by working directly on their characters.

Swift strings conform to the Collection protocol, which means we can directly
apply collection methods like filter() to strings.

Filtering characters in a string in Swift can effectively clean data inputs, such
as removing special symbols or numbers.

Consider the need to remove non-alphabetic characters from a string containing


various special characters and spaces.
let messyString = "S!w@i#f$t%"
let cleanedString = messyString.filter { $0.isLetter }
// Prints `Swift`
print(cleanedString)

In this example, the filter() method iterates over each character in


messyString. The closure { $0.isLetter } evaluates whether each character
is a letter, including only those that return true for isLetter in the resulting
cleanedString.

This method is efficient and straightforward, leveraging Swift’s collection capab-


ilities to transform strings without needing intermediate structures, simplifying
code.
Asynchronous programming and error
handling

This chapter delves into advanced techniques for managing asynchronous


operations in Swift, a crucial aspect for building responsive and efficient applic-
ations. You’ll explore how to effectively bridge traditional completion handlers
with modern async/await syntax and execute main-actor-isolated functions
within DispatchQueue.main.async for UI safety. We’ll also discuss how to use
yield() to allow other tasks to proceed during lengthy operations and im-
plement task cancellation mechanisms to stop unnecessary work, optimizing
resource usage.

Additionally, you’ll learn how to manage the execution of concurrent tasks with
priorities and delay tasks using modern clock APIs. Handling and refining error
management in asynchronous code is also covered, including strategies for
rethrowing errors with added context and defining the scope of try explicitly.
Finally, we’ll look at transforming and converting the results of asynchronous
operations, enhancing error handling and the usability of the Result type.

These advanced strategies will provide you with the tools to write cleaner, more
robust asynchronous code, improving the performance and reliability of your
Swift applications.

153
154 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Bridge async/await and completion handlers

Swift’s introduction of async/await has significantly streamlined asynchron-


ous programming, providing a cleaner and more concise syntax compared to
traditional completion handlers. However, in many real-world projects, we
often need to integrate new Swift concurrency features with existing code that
utilizes completion handlers. Bridging this gap efficiently can enhance code
readability and maintain legacy system compatibility.

Here’s how we can wrap a traditional completion handler in an asynchronous


function, allowing it to be used with Swift’s modern concurrency features.

func fetchData(completion: @escaping (Data?, Error?) -> Void) {


// Simulating a network request
DispatchQueue.global(qos: .background).async {
let data = "Sample data".data(using: .utf8)
completion(data, nil)
}
}

func fetchDataAsync() async throws -> Data {


try await withCheckedThrowingContinuation { continuation in
fetchData { data, error in
if let data = data {
continuation.resume(returning: data)
} else if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(
throwing: NSError(
domain: "DataError",
code: -1, userInfo: nil
)
)
}
}
}
}
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 155

Task {
do {
let data = try await fetchDataAsync()
print("Data received: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}

The function fetchData() represents a traditional asynchronous op-


eration using a completion handler. The function fetchDataAsync()
wraps this completion handler in an asynchronous context using
withCheckedThrowingContinuation(). It pauses the current task until
the completion handler is called, at which point it either returns the result
or throws an error. This approach allows us to call fetchDataAsync() using
Swift’s async/await syntax, seamlessly integrating with other asynchronous
code while maintaining compatibility with existing APIs that use completion
handlers.

By utilizing this bridging technique, we can gradually refactor our projects


to adopt new concurrency features without disrupting the existing codebase,
ensuring a smooth transition and improving overall code maintainability.
156 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Run main-actor-isolated functions within the main dispatch


queue

Swift’s concurrency model brings clarity and safety to asynchronous code ex-
ecution, particularly with the use of actors to manage access to shared data.
A notable convenience in this system is how the Swift compiler treats clos-
ures passed to DispatchQueue.main.async {}. This special handling allows
these closures to implicitly run on the @MainActor, simplifying the invoca-
tion of main-actor-isolated functions within them. However, this conveni-
ence comes with a peculiar limitation: it is strictly tied to the exact syntax
DispatchQueue.main.async.

Let’s explore this concept with examples that highlight the syntax sensitivity
and provide alternatives that, while logically sound, fail to compile due to this
restriction.
import Foundation

@MainActor
func updateUI() {
// Must run on the main thread.
}

DispatchQueue.main.async {
updateUI()
}

This snippet is straightforward and works as expected because it matches the


exact syntax the compiler looks for to apply @MainActor inference.

Despite logical equivalence, slight deviations in syntax prevent the compiler


from recognizing the main actor context, leading to compilation errors if strict
actor isolation is enforced.
let mainQueue = DispatchQueue.main
mainQueue.async {
// This will produce an error
updateUI()
}

The compiler’s ability to infer that a closure should run on the @MainActor
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 157

when passed to DispatchQueue.main.async {} is not based on deep semantic


analysis or sophisticated type checking. Instead, it relies on a straightforward
source-code-based check for the literal use of DispatchQueue.main.async. This
means that seemingly equivalent code structures, which logically should behave
the same, do not receive the same treatment if they deviate from this precise
syntax.

Understanding this behavior is crucial for developers, particularly when archi-


tecting apps that handle UI updates or other main-thread-specific operations,
ensuring that all actor isolation constraints are met without causing errors and
unexpected behavior.
158 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Allow execution of other tasks during long operations using


yield()
If we have a computation or process that takes a significant amount of time,
inserting await Task.yield() at strategic points (e.g., inside a loop) can al-
low other tasks to run, which can improve the responsiveness of our applica-
tion. When processing large arrays or collections, especially in a loop, yielding
periodically can prevent our task from monopolizing the CPU, allowing other
concurrent operations to proceed smoothly.

Here is an example showing how we can call await Task.yield() within an


asynchronous context.
func processLargeDataset() async {
for item in largeDataset {
// Process item
// ...

// Yield control to allow other tasks to run


await Task.yield()
}
}

In this example, inserting await Task.yield() within the loop provides peri-
odic pauses. These pauses allow the system to handle other tasks that are ready
to run, potentially improving the overall responsiveness of the application.

While await Task.yield() can improve app responsiveness and fairness


among tasks, unnecessary use can lead to performance overhead. It’s
important to use it judiciously, primarily when we identify a potential for
long-running tasks to block others.

Swift’s concurrency model also includes concepts like task priorities. Some-
times, adjusting the priority of tasks might be a more appropriate tool for
managing task execution order and responsiveness.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 159

Utilize task cancellation mechanisms to stop unnecessary work

In Swift’s concurrency model, managing ongoing tasks efficiently is crucial,


especially when those tasks become unnecessary or their context changes.
Swift provides a built-in mechanism for task cancellation, enhancing resource
management and responsiveness of applications.

Here’s how we can manage task cancellation effectively.


func performLongRunningTask() async {
let task = Task {
for i in 1...10 {
guard !Task.isCancelled else {
print("Task was cancelled!")
return
}
print("Processing \(i)")
try await Task.sleep(
until: .now + .seconds(1),
clock: .suspending
)

// Sleep for 1 second


try? await Task.sleep(nanoseconds: 1_000_000_000)
}
print("Task completed successfully")
}

Task {
// Sleep for 3 seconds
try? await Task.sleep(nanoseconds: 3_000_000_000)

task.cancel()
print("Called cancel on the task")
}
}

Task {
await performLongRunningTask()
}

In this example, we define a long-running task within a Task initializer, which


performs a simple loop operation, sleeping between iterations to simulate
work. Within the loop, we regularly check Task.isCancelled to determine if a
160 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

cancellation has been requested. If true, the task prints a cancellation message
and exits early.

Additionally, a separate task is used to cancel the first task after a delay, show-
casing how we can control task lifetime based on application logic or user
input.

Using task cancellation in Swift allows us to write highly responsive and


resource-efficient applications by halting operations that are no longer
needed or valid. This approach is essential for tasks that involve significant
computation or I/O operations, which could otherwise impact application
performance or user experience if left unchecked.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 161

Manage the execution priority of concurrent tasks

Managing the execution priority of concurrent tasks in Swift can significantly


impact multitasking capabilities and overall system performance. Swift’s con-
currency model includes a way to specify priority for asynchronous tasks using
the Task.Priority enum, which helps the system decide the order and speed
at which tasks are executed.

Here’s how we can manage task priorities.

func fetchUserData() async {


Task(priority: .high) {
print("Fetching user data with high priority...")

// Simulate a network request


try? await Task.sleep(nanoseconds: 1_000_000_000)

print("User data fetched")


}
}

func performBackgroundTask() async {


Task(priority: .background) {
print("Performing background task with low priority...")

// Simulate a long-running background task


try? await Task.sleep(nanoseconds: 2_000_000_000)

print("Background task completed")


}
}

Task {
await fetchUserData()
await performBackgroundTask()
}

In this example, we define two tasks with different priorities. The


fetchUserData() function is assigned a high priority, suggesting it should be
executed sooner and potentially faster compared to other tasks. Conversely,
performBackgroundTask() is given a background priority, indicating it should
run without interfering with more critical tasks.
162 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Task.Priority provides several levels, including high, medium, low, and


background. Assigning these priorities allows us to fine-tune how tasks are
scheduled relative to each other, optimizing the application’s responsiveness
and efficiency.

Prioritizing tasks is crucial in applications where certain operations must be


completed quickly (such as user interactions) while others can be deferred (like
data syncing or processing tasks that run in the background). By strategically
managing task priorities, we can ensure a smooth user experience and efficient
use of system resources.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 163

Delay asynchronous tasks using modern clock APIs


In Swift 5.7 and later, we have access to some advanced timing APIs that
enhance how we manage time-sensitive operations in our applications. These
APIs are built around three key concepts: clocks, instants, and durations,
each playing a crucial role in precise time management. This functionality is
especially valuable when dealing with asynchronous tasks that require delays
or scheduling based on specific timing conditions.

Here’s how we can use these components to implement a delay in an asyn-


chronous Task.
Task {
print("Starting the task")

try await Task.sleep(


until: .now + .seconds(10),
tolerance: .seconds(2),
clock: .suspending
)

print("Continuing the task")


}

In our example, we use Instant.now property to get the current instant, and
add a Duration of 10 seconds to delay the task by 10 seconds. We can also
specify a duration for tolerance, which would allow the underlying scheduling
mechanisms to slightly adjust the deadline if necessary for more power efficient
execution. We can choose either continuous or suspending clock, depending
on whether we would like it to continue incrementing while the system is asleep.

This approach demonstrates a clear and efficient way to handle delays within
asynchronous operations, leveraging Swift’s modern concurrency model and
clock APIs to deliver precise, power-conscious application behaviors.
164 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Handle errors in asynchronous code

Swift’s async/await syntax brings streamlined asynchronous programming to


Swift, mirroring the familiar error handling mechanisms used in synchronous
code. This integration ensures that we can manage errors in asynchronous
functions using the well-understood throws, try, and catch keywords.

Here’s an example demonstrating error handling in an async context.

enum FileError: Error {


case fileNotFound
case unreadableContent
}

func readFileContent(path: String) async throws -> String {


guard
let fileURL = URL(string: path),
FileManager.default.fileExists(atPath: fileURL.path)
else {
throw FileError.fileNotFound
}

do {
let content = try String(
contentsOf: fileURL, encoding: .utf8
)
return content
} catch {
throw FileError.unreadableContent
}
}
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 165

func displayFileContent() async {


do {
let content = try await readFileContent(
path: "path/to/file.txt"
)
print("File content: \(content)")
} catch FileError.fileNotFound {
print("The file was not found.")
} catch FileError.unreadableContent {
print("The file content could not be read.")
} catch {
print("An unexpected error occurred: \(error).")
}
}

Task {
await displayFileContent()
}

In this snippet, the readFileContent() async function is declared with throws,


indicating it can result in an error. We use try await to perform the asynchron-
ous operation, mirroring the try used in synchronous error handling. Errors
are handled using a do-catch block, the same way they are handled in syn-
chronous Swift functions. This maintains consistency across both synchronous
and asynchronous code, making it easier for us to apply our existing knowledge
of error handling without needing to learn new paradigms.

This approach ensures that error handling in Swift’s asynchronous program-


ming remains straightforward and consistent with synchronous programming,
making it easier for us to write robust, error-resistant code.
166 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

Rethrow errors with added context

In concurrent programming, handling and rethrowing errors safely is crucial to


maintain the integrity and reliability of an application. This involves not only
catching and logging the error but also enriching it with additional context that
can help diagnose issues when they are propagated up the call stack. Swift’s
error handling combined with its concurrency model provides a structured way
to approach this.

Here’s a practical example to illustrate how to safely rethrow errors with addi-
tional context in a concurrent environment.

enum DataProcessingError: Error {


case dataFetchFailed(underlyingError: Error)
case dataParsingFailed(underlyingError: Error)
}

func fetchData() async throws -> Data {


// Simulate fetching data that might fail
throw NSError(
domain: "Network", code: 1,
userInfo: [
NSLocalizedDescriptionKey: "Network connection lost"
]
)
}

func parseData(_ data: Data) throws -> [String] {


// Simulate parsing data that might fail
throw NSError(
domain: "Parser", code: 1,
userInfo: [NSLocalizedDescriptionKey: "Parsing error"]
)
}
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 167

func processDataTask() async throws -> [String] {


do {
let data = try await fetchData()
return try parseData(data)
} catch {
let error = error as NSError
switch error.domain {
case "Network":
throw DataProcessingError
.dataFetchFailed(underlyingError: error)
case "Parser":
throw DataProcessingError
.dataParsingFailed(underlyingError: error)
default:
throw error
}
}
}
168 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

// Usage in an async context


func executeTask() {
Task {
do {
let results = try await processDataTask()
print("Data processed successfully: \(results)")
} catch let error as DataProcessingError {
switch error {
case .dataFetchFailed(let underlyingError):
print("""
Failed to fetch data: \(
underlyingError.localizedDescription
)
""")
case .dataParsingFailed(let underlyingError):
print("""
Failed to parse data: \(
underlyingError.localizedDescription
)
""")
}
} catch {
print("""
An unexpected error occurred: \(
error.localizedDescription
)
""")
}
}
}

executeTask()

The snippet above demonstrates wrapping the underlying errors (NSError from
fetching or parsing) into a more descriptive error (DataProcessingError). It
adds a layer of abstraction that can clarify where exactly the error occurred in
a sequence of asynchronous operations. By selectively catching and rethrow-
ing errors, the function processDataTask() provides clear indicators of error
sources—whether they come from data fetching or parsing. This differenti-
ation is crucial for debugging and maintaining code, especially in production
environments where errors need to be resolved quickly and efficiently.

When wrapping errors, it’s essential to maintain the original error information
to avoid losing context, which can be critical for troubleshooting.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 169

Explicitly define the scope of try with infix operators

Swift’s error handling, featuring try, try?, and try!, empowers developers to
manage failing operations effectively. When these constructs are used alongside
infix operators, such as +, -, *, /, etc., understanding their scope and application
becomes crucial for writing robust and error-free code.

When using try with an infix operator, placing try directly before the left-hand
side of the expression without parentheses applies it to the entire expression.
This means that if any part of the expression can throw an error, try will attempt
to handle it.

Consider the following example where we have two functions that can throw
errors.
func fetchCountFromServer() throws -> Int {
// Simulates fetching a count from a server
throw NSError(domain: "NetworkError", code: 1, userInfo: nil)
}

func fetchCountFromDatabase() throws -> Int {


// Simulates fetching a count from a database
throw NSError(domain: "DatabaseError", code: 2, userInfo: nil)
}

To apply try to both functions within a single expression, we would write the
following code.
do {
let totalCount = try fetchCountFromServer() +
fetchCountFromDatabase()
print("Total Count: \(totalCount)")
} catch {
print("An error occurred: \(error)")
}

This code attempts to execute both functions and add their results. If either
function throws an error, the catch block will handle it.

To make the scope of try clearer, especially in complex expressions, we can


use parentheses. This does not change the behavior but improves readability
and makes the intention explicit.
170 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

do {
let totalCount = try (
fetchCountFromServer() + fetchCountFromDatabase()
)
print("Total Count: \(totalCount)")
} catch {
print("An error occurred: \(error)")
}

By explicitly defining the scope of try, we minimize the risk of unintended


behavior and make it easier for others (and ourselves) to understand the flow
of error handling in our application.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 171

Transform success or error values of the Result type

The Result type in Swift is a powerful tool for managing operations that can
yield either success or an error. Defined as an enumeration with two cases,
.success(T) and .failure(Error), it offers a structured approach to handle
outcomes explicitly, complementing Swift’s native error handling techniques
like try-catch.

If we need to transform the success value of a Result type into another value,
we can use the map() method. Essentially, map() allows us to apply a transform-
ation function to the successful outcome, without having to unpack the result
manually. For instance, if we have a Result<String, Error>, and we want to
convert the String to its integer length, we can use map like this:

let result: Result<String, Error> = .success("Hello")

// length is now `Result<Int, Error>`


let length = result.map { $0.count }

If result were a failure, map() would not execute the transformation, and the
error would propagate unchanged.

While map() is for handling successful outcomes, mapError() comes into play
when dealing with errors. This method lets us transform the error value into
another type of error, which is particularly useful when we need to adapt errors
from lower-level APIs to our application’s error types.

For example, if we are dealing with a networking operation that produces a


Result<Data, NetworkError>, and we want to convert NetworkError to a more
generic Error type, we can use mapError():
172 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

import Foundation

enum NetworkError: Error {


case invalidResponse
}

struct MyError: Error {


let networkError: NetworkError
}

let networkResult: Result<Data, NetworkError> =


.failure(.invalidResponse)

// adaptedResult is now `Result<Data, MyError>`


let adaptedResult = networkResult
.mapError { MyError(networkError: $0) }

Using map() and mapError() with Swift’s Result type can provide a streamlined
way to handle transformations and error adaptations, making our code cleaner
and more maintainable.
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 173

Convert Result into a throwing expression

The Result type in Swift provides a structured way to handle operations that
may succeed or fail. When we need to integrate Result with Swift’s error-
handling system, particularly when we want to convert a Result into a throwing
expression, we can use its get() method. This method extracts the success
value if the operation succeeded, or throws the contained error if it failed,
allowing for standard try-catch error handling.

Here’s how we can effectively use the get() method.

enum DataError: Error {


case noDataAvailable
case dataCorrupted
}

func fetchData() -> Result<String, DataError> {


// Simulate an outcome
let didFail = false

if didFail {
return .failure(.noDataAvailable)
} else {
return .success("Data loaded successfully.")
}
}

func processData() throws {


let result = fetchData()
let data = try result.get()
print(data)
}

do {
try processData()
} catch {
print("Failed to process data: \(error)")
}

In this example, fetchData() returns a Result type. When processData()


is called, it uses get() to attempt to unwrap the result. If fetchData() re-
turned a success, data will hold the unwrapped string. If it resulted in a failure,
processData() will throw an error that can be handled in a standard way higher
174 ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING

up the chain.

Utilizing get() to convert a Result into a throwing expression is especially


useful when we need to propagate errors to a higher level in our application,
allowing for cleaner and more centralized error management.
Logging and debugging

This chapter dives into essential debugging and logging techniques in Swift,
focusing on tools and practices that enhance error detection and program
analysis. You’ll learn how to use assertions to catch logical errors early in
the development process and enrich debug logs with contextual information
for clearer insights. We’ll explore how to implement custom nil-coalescing
in debug prints and customize output with CustomDebugStringConvertible
for more informative debugging. Additionally, we’ll look into techniques for
introspecting properties and values at runtime, along with utilizing the dump()
function for in-depth analysis.

These strategies will equip you to effectively debug and refine your Swift ap-
plications, ensuring they run smoothly and efficiently.

175
176 LOGGING AND DEBUGGING

Catch logical errors in development phase with assertions


Assertions are a critical debugging tool, helping us ensure that our code operates
under correct assumptions. They are particularly useful during development
to catch logical errors early, but are disabled in production to avoid impacting
application performance.

The assert() function is used to implement assertions in Swift. It verifies a


condition at runtime, and the condition evaluates to false, the program halts,
displaying an error message.

Here’s an example that demonstrates the use of assertions in a function calcu-


lating the body mass index (BMI).
func calculateBMI(weight: Double, height: Double) -> Double {
assert(height != 0, "Height cannot be zero to calculate BMI.")
return weight / (height * height)
}

In this code, the assert() ensures that the height is not zero, which would
cause a division by zero error when calculating the BMI. If height is zero, the
program will terminate and print “Height cannot be zero to calculate BMI.”

Assertions help clarify the conditions under which our code operates, making
it easier to understand and maintain.

Assertions are primarily for catching issues during development, they’re usu-
ally disabled in production. They are used to catch programming errors, not
to handle user-generated errors or external system changes. It’s crucial to
complement assertions with robust error handling strategies to manage the un-
predictable conditions that our applications may encounter post-deployment.
LOGGING AND DEBUGGING 177

Enrich debug logs with contextual information

Debugging is an integral part of the development process, often consuming


a significant amount of time and resources. Swift offers a suite of literal ex-
pressions that can be incredibly beneficial for debugging. Among these, #file,
#line, #column, and #function are particularly useful, providing detailed con-
text about the source code’s state and behavior.

Literal expressions in Swift are special tokens that get replaced with specific
values by the compiler. They are part of the language’s compile-time introspec-
tion facilities. Here’s a quick rundown of the most commonly used literals for
debugging:

• #file: the name (including the path) of the file


• #line: the line number
• #column: the column number
• #function: the name of the declaration (function, method, or closure) in
which it appears

Incorporating these literals into our debugging mechanism can significantly


enhance the informativeness of our logs. For instance, instead of merely print-
ing error messages, we can include the file name, line number, and function
name where the error occurred, making it much easier to pinpoint the source
of the problem.

func logError(
_ message: String,
file: String = #file,
line: Int = #line,
function: String = #function
) {
print("""
Error: \(message), file: \(file), \
line: \(line), function: \(function)
""")
}
178 LOGGING AND DEBUGGING

func processUserInput(_ input: String) {


guard input != "error" else {
logError("Invalid user input")
return
}
print("User input processed successfully: \(input)")
}

// Prints `User input processed successfully: hello`


processUserInput("hello")

/* Prints `Error: Invalid user input, file:


.../main.swift, line: 19, function: processUserInput(_:)`
*/
processUserInput("error")

When we run this code, we’ll see a detailed error message for the second func-
tion call, including the file name, line number, and function name, which is
immensely helpful for debugging purposes.

However, keep in mind that in real-world projects, print statements are typ-
ically replaced with more sophisticated logging. By integrating these literals
into our logging mechanism, we can make our logs much more informative
and significantly improve our ability to diagnose and resolve issues in our
applications.
LOGGING AND DEBUGGING 179

Implement custom nil-coalescing for optionals in debug prints


In Swift, when interpolating optionals directly into strings for debugging pur-
poses, the compiler issues a warning if the optional could be nil. Additionally,
using the standard nil-coalescing operator (??) with a string as a fallback is not
directly possible for non-string optionals due to type mismatches, leading to
compiler errors.

To resolve this and enhance readability in debug outputs, we can define a


custom operator that allows any optional to be handled elegantly, with a fallback
to a descriptive string like “nil” when the value is absent.

Here’s how to implement a custom nil-coalescing operator specifically for this


purpose.
infix operator ???: NilCoalescingPrecedence

func ???<T>(
optionalValue: T?,
defaultValue: @autoclosure () -> String
) -> String {
optionalValue
.map { String(describing: $0) } ?? defaultValue()
}

var url: URL?

// Assign a URL some time later


url = URL(string: "https://example.com")

print("The URL is \(url ??? "nil")")

This code snippet defines an infix operator ??? that works with any optional. It
uses the standard map function to convert the optional value to a string if it’s
present, otherwise, it uses the provided default string. In this approach, if url
is nil, the output defaults to "nil" without any type errors, regardless of the
original type of the optional. This operator allows for clearer debug statements
and avoids cluttering code with repetitive checks or type conversions.
180 LOGGING AND DEBUGGING

Customize debug output with CustomDebugStringConvertible


Swift’s CustomDebugStringConvertible protocol can enhance our debugging
experience by allowing us to define a custom textual representation for our
types. This is particularly useful when standard descriptions are too generic or
lack the specific details needed to effectively debug complex data structures.

Implementing CustomDebugStringConvertible involves defining a


debugDescription property. When our type conforms to this protocol,
tools like String(reflecting:) or debugPrint(_:) will use our custom
debugDescription instead of the standard output, providing more context or
detail that can be crucial for debugging.

Suppose we have a User struct in a social media application. By default, the


debug description might not provide the details about the user’s status or role
in the format we’d like.
struct User {
let username: String
let isActive: Bool
let role: String
}

let user = User(username: "johnDoe", isActive: true, role: "admin")


// Prints `User(username: "johnDoe", isActive: true, role: "admin")`
print(String(reflecting: user))

To customize the output and make it fit the format we prefer, we can implement
CustomDebugStringConvertible.

extension User: CustomDebugStringConvertible {


var debugDescription: String {
"""
User: \(username) \
[Role: \(role), Active: \(isActive ? "Yes" : "No")]
"""
}
}

let user = User(username: "johnDoe", isActive: true, role: "admin")

// Prints `User: johnDoe [Role: admin, Active: Yes]`


print(String(reflecting: user))
LOGGING AND DEBUGGING 181

Custom debug descriptions can make logs more readable and informative,
especially in complex scenarios involving multiple data types. We can adjust
the level of detail based on our the debugging needs, include more fields or
simplify the output depending on what is relevant.
182 LOGGING AND DEBUGGING

Introspect the properties and values of instances at runtime

Swift’s Mirror is a powerful tool for reflection, which allows us to introspect


and explore the properties and values of instances at runtime. This capability
can be incredibly useful for debugging, especially when dealing with complex
or opaque data structures whose properties are not directly visible or accessible.
Using Mirror, we can programmatically examine the structure and content of
any Swift instance, aiding in understanding program state and behavior during
development.

Here’s how we can use Mirror to inspect and log the properties of an instance.
struct User {
var name: String
var age: Int
var isActive: Bool
}

let user = User(name: "Alice", age: 30, isActive: true)

func logProperties(of object: Any) {


let mirror = Mirror(reflecting: object)
print("Logging properties for \(object):")
for child in mirror.children {
if let propertyName = child.label {
print("\(propertyName): \(child.value)")
}
}
}

logProperties(of: user)

In this example, we define a User struct and create an instance of it. The
logProperties(of:) function creates a Mirror reflecting the passed object and
iterates over its children, which represent the properties. Each property’s name
and value are printed, providing a clear view of the object’s state.

Using Mirror not only enhances the debugging process but also aids in dynamic
operations where properties need to be accessed or modified based on runtime
conditions. However, it’s important to note that reflection in Swift, through
Mirror, is mainly intended for debugging and not for modifying instance prop-
LOGGING AND DEBUGGING 183

erties, which remains against Swift’s strong type safety principles.


184 LOGGING AND DEBUGGING

Utilize the dump() function for in-depth debugging

The dump() function in Swift provides a comprehensive overview of an in-


stance’s properties and contents, which can be invaluable for debugging com-
plex data structures. This function recursively prints the object’s content,
including all properties, values, and additional information about inheritance
and structure.

dump() goes beyond the capabilities of the basic print() function by detailing
not only the instance itself but also its sub-components and nested properties.
This makes it an essential tool for deep debugging, especially when we want to
understand the full structure of complex custom types or data models.

Consider a situation where we are working with a nested data structure, like a
node in a tree or a complex data model with multiple levels of nested objects.
struct User {
var name: String
var age: Int
var address: Address
}

struct Address {
var street: String
var city: String
var zipCode: Int
}

let user = User(


name: "John Doe", age: 30,
address: Address(
street: "123 Elm St",
city: "Somewhere", zipCode: 90210)
)

dump(user)

In the example above, calling dump(user) would print detailed information


about the User object, including the name, age, and nested Address object with
all its properties.

The dump() function in Swift offers various parameters that allow for customiz-
LOGGING AND DEBUGGING 185

ation of how the output is formatted. To control how deeply dump() recurses
through the object, we can use the maxDepth parameter. Setting this to a finite
number can help simplify the output for very deep or recursive structures.

Using dump() can provide detailed insights into the structure of any Swift
type, illustrating how data is organized and nested, which is invaluable for
debugging complex structures. The function is easy to use, requiring no setup or
configuration, making it immediately useful for exploring unknown or intricate
data structures.
186 LOGGING AND DEBUGGING
Code organization

This chapter explores effective strategies for organizing code in Swift to en-
hance readability and maintainability. Learn how to manage shared resources,
encapsulate utilities, and use enums as namespaces to keep your codebase clean
and modular. We’ll also touch on important practices for maintaining code
quality and adaptability, such as highlighting code for review and managing
framework compatibility.

These techniques are essential for building a structured, navigable, and efficient
codebase in your Swift projects.

187
188 CODE ORGANIZATION

Facilitate access to shared resources using type subscripts


Accessing shared resources or data seamlessly across various components of
an application is a common requirement, whether for managing global config-
urations, shared data caches, or utility functions that operate without instance
dependency. Traditionally, developers might rely on singleton patterns, static
methods, or global variables to manage such shared access.

In Swift, we can also consider using type subscripts. This feature allows us
to access shared resources directly through a type’s interface, eliminating the
need for object instantiation and simplifying the codebase. Type subscripts are
similar to instance subscripts but are called on the type itself rather than on an
instance.

Consider an application that manages a set of configuration settings, such as


themes or language preferences. Rather than passing around a configuration
object or relying on a singleton, type subscripts can provide direct and tidy
access to these settings.
struct Configuration {
private static var settings: [String: String] = [
"Theme": "Dark", "Language": "English"
]

static subscript(key: String) -> String? {


return settings[key]
}
}

if let theme = Configuration["Theme"] {


print("The current theme is \(theme).")
}

In this structure, a private dictionary holds the settings in a static context,


ensuring that they are shared across all uses of the Configuration. A type
subscript provides access to the dictionary, enabling easy retrieval of settings
by key. This design encapsulates the storage of shared settings, allowing for
streamlined access within the application.

Type subscripts can be used in a variety of scenarios, such as managing global


CODE ORGANIZATION 189

settings and accessing centralized resources. They offer a practical way for
applications to handle shared data efficiently and ensure that access patterns
are consistent throughout the application.
190 CODE ORGANIZATION

Encapsulate context-specific utilities within a parent type or


extension

The ability to define nested types in Swift enhances code organization and scope
management. Nested types can be classes, structures, or enumerations that
serve specific roles tightly coupled with their enclosing type. This encapsulation
not only streamlines the architecture by limiting the use scope of the nested
types but also improves readability by grouping related functionalities together.

An even more powerful feature of Swift is the ability to declare these nested
types within extensions. This allows us to expand the functionality of a class,
struct, or enum without cluttering the primary definition of the type, and it
helps maintain separation of concerns within a single cohesive unit.

Here’s an example using a NetworkManager class, which is responsible for net-


work operations. We can define a nested type within an extension to manage
API endpoints specific to this manager.
class NetworkManager {
// Initial definition of the NetworkManager
}

extension NetworkManager {
struct Endpoint {
let path: String
let queryParameters: [String: String]?
}

func request(to endpoint: Endpoint) {


// Implementation of a request method
}
}

// Accessing `Endpoint` in the rest of the code


let endpoint = NetworkManager.Endpoint(
path: "https://example.com", queryParameters: nil
)

In this structure, the Endpoint struct is defined within the extension of


NetworkManager. This design decision ensures that all details related to
constructing endpoints are encapsulated within the network management
CODE ORGANIZATION 191

context. The Endpoint struct, by being nested, is clearly associated with


NetworkManager and is not accessible as a standalone entity, reinforcing its
utility role.

By leveraging the capability of defining nested types in extensions, we can create


highly cohesive and modular codebases. This approach not only simplifies the
code but also aids in maintaining a clear and logical structure, making the code
easier to manage and extend.
192 CODE ORGANIZATION

Organize related functionalities using enums as namespaces


One of the practices for organizing and encapsulating related functionalities,
constants, or utility functions in Swift is the use of enum or struct types as
namespaces. This approach helps keep the global namespace uncluttered and
makes our code more modular and easier to navigate.

Swift does not support true namespaces like some other languages, but we can
mimic this functionality using enum or struct. Since enums in Swift cannot be
instantiated if they don’t have cases, they are ideal for this purpose.
enum MathConstants {
static let pi = 3.14159
static let e = 2.71828
}

enum UtilityFunctions {
static func computeArea(radius: Double) -> Double {
return MathConstants.pi * radius * radius
}
}

let area = UtilityFunctions.computeArea(radius: 5)

In the example above, MathConstants and UtilityFunctions act as namespaces.


MathConstants groups all mathematical constants, and UtilityFunctions
provides a logical grouping for utility functions.

This method of using enum as a namespace is particularly effective for lar-


ger projects where keeping the global namespace clean and organized can
significantly improve developer experience and code clarity.
CODE ORGANIZATION 193

Draw attention to code that needs review or modification


Swift offers various directives to enhance code quality and readability. One
such directive is #warning, a powerful tool for developers aiming to keep their
codebase clean and intention-driven.

The #warning directive serves as a reminder or a flag within our code. It’s a way
to generate warnings during the compilation process, intentionally drawing
attention to pieces of code that need review or modification. Unlike errors that
prevent code from compiling, warnings don’t stop the build process but signify
something noteworthy or potentially problematic.

Implementing #warning is straightforward. We can simply include it in our


code followed by a custom message.

Consider we are working on a Swift project involving an API. We’ve set up


a function to handle API requests but haven’t implemented caching yet. We
could use #warning to remind ourselves or inform our team about this pending
enhancement.
func fetchUserData(from url: URL) {
#warning("TODO: Implement caching to optimize network calls")
URLSession.shared.dataTask(with: url) { data, response, error in
// Handle response and error
}.resume()
}

In this scenario, the #warning directive clearly indicates that while the func-
tion for fetching user data is operational, it lacks a caching mechanism. This
reminder ensures that the enhancement is not forgotten in the development
process.

By flagging sections of code, developers are less likely to overlook or forget


about refinements, leading to a higher quality end product. But we should aim
to address warnings promptly, as accumulating too many can lead to technical
debt.
194 CODE ORGANIZATION

Tackle framework availability with compiler directives

In Swift development, writing code that’s adaptable across various Apple plat-
forms is a common challenge. While platform checks have been a traditional
solution, Swift’s canImport directive can offer a more refined and forward-
looking approach. As platforms evolve and frameworks become available on
new platforms, canImport ensures our code remains compatible without re-
quiring updates.

Let’s consider an app that wants to utilize the CoreHaptics framework for tactile
feedback on supported devices.

#if canImport(CoreHaptics)
import CoreHaptics

class HapticFeedbackManager {
func triggerHapticFeedback() {
// CoreHaptics implementation
print("Haptic feedback triggered.")
}
}

#else

class HapticFeedbackManager {
func triggerHapticFeedback() {
// Non-CoreHaptics implementation or a simple message
print("CoreHaptics not available. Feedback not triggered.")
}
}

#endif

let hapticManager = HapticFeedbackManager()


hapticManager.triggerHapticFeedback()

In this code, canImport checks for the presence of CoreHaptics. If available,


HapticFeedbackManager utilizes its features. If not, a fallback implementation
ensures the app remains functional, albeit without the haptic feedback.

Code within a canImport block clearly indicates its reliance on a specific frame-
work, enhancing the code’s readability and ease of maintenance. It focuses on
CODE ORGANIZATION 195

framework availability, sidestepping the complexities associated with platform


differences.
196 CODE ORGANIZATION

Disambiguate standard library types from custom declarations

As Swift evolves, new types and protocols are introduced, enhancing the lan-
guage’s capabilities and robustness. However, these additions can sometimes
lead to naming conflicts, especially in codebases that predate these introduc-
tions or in those using similar names for custom types. Two notable examples
are the Result type, introduced in Swift 5, and the Identifiable protocol, intro-
duced in Swift 5.1. Let’s see how the Swift. prefix can be used to disambiguate
these modern Swift features, ensuring clarity and precision in our code.

Imagine we have an existing Swift project with custom Result and


Identifiable types or protocols. With the introduction of Swift 5 and 5.1, the
standard library now includes these names, potentially leading to conflicts.
Here’s how such conflicts might appear:

// Custom Result type defined before Swift 5


enum Result<T> {
case success(T)
case failure(Error)
}

// Custom Identifiable protocol defined before Swift 5.1


protocol Identifiable {
var id: Int { get }
}

// Ambiguous: custom Result or Swift's Result?


func fetchData() -> Result<Data> {
// Function implementation
}

// Ambiguous: custom Identifiable or Swift's Identifiable?


struct CustomItem: Identifiable {
var id: Int
}

To clarify our intent and specify that we want to use Swift’s native types and
protocols, we can prepend Swift. to the type or protocol.
CODE ORGANIZATION 197

func fetchData() -> Swift.Result<Data, Error> {


// Clearly Swift's native Result
}

struct CustomItem: Swift.Identifiable {


var id: Int
// Clearly conforming to Swift's Identifiable protocol
}

If we need to continue using our custom types or protocols in certain parts of our
code, we can do so without the Swift. prefix, maintaining a clear distinction.

Using the Swift. prefix clarifies which versions of the types or protocols are
intended, helping maintain code clarity and preventing conflicts. This method
allows for both the continued use of custom types and the adoption of new
standard library features, ensuring code remains precise and understandable.
198 CODE ORGANIZATION

You might also like