Swift Gems
Swift Gems
Swift Gems
Natalia Panferova
2024
ii
Contents
Introduction 1
iii
iv CONTENTS
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.
“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
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.
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
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")
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.
import Foundation
ensuring that the function remains operational even when new enum cases are
added, and prompting a review of the handling code.
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
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.
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
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
}
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
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")
}
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"
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
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
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.
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
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.
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
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
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.
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."
}
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
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.
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()
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()
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.
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
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.
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
printNumbers(numbers: 1, 2, 3)
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
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.
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
}
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
}
updateUserDetails(
user: ¤tUser, newName: "Bob", newAge: 25
)
func logMessage(
level: LogLevel,
message: @autoclosure () -> String
) {
if level >= currentLogLevel {
print(message())
}
}
currentLogLevel = .debug
logMessage(
level: .debug,
message: "Debug: \(expensiveStringComputation())"
)
debug.
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.
In this example, the InputHandler class has three versions of the handleInput()
34 FUNCTIONS, METHODS, AND CLOSURES
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
}
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.
enum UserCommand {
case start, stop, pause, resume, unknown
}
print(processCommand(.start))
print(processCommand(.unknown))
inputs, which ideally should never occur if the application logic is correctly
implemented.
protocol Equipment {
var description: String { get }
}
func produceHighQualityCustomizedBall(
) -> CustomizedEquipment<QualityCheckedEquipment<SoccerBall>> {
let qualityCheckedBall = QualityCheckedEquipment(
baseEquipment: SoccerBall()
)
return CustomizedEquipment(baseEquipment: qualityCheckedBall)
}
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
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
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.
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.
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()
}
}
deinit {
timer?.invalidate()
print("Scheduler deinitialized")
}
}
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
// 8
let doubled = transformNumber(4, using: { $0 * 2 })
// 16
let squared = transformNumber(4, using: { $0 * $0 })
// 5
let incremented = transformNumber(4, using: { $0 + 1 })
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
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.
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.
49
50 CUSTOM TYPES: STRUCTS, CLASSES, ENUMS
return """
Coordinates: \(abs(latitude))°\(latDirection), \
\(abs(longitude))°\(lonDirection)
"""
}
}
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
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.
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
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
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.
This approach works because extensions in Swift can add functionality to types,
CUSTOM TYPES: STRUCTS, CLASSES, ENUMS 55
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.
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
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
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
if morningShift.contains(timeSlot1) {
print("Time slot 1 is within the morning shift.")
}
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
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
}
}
// No copy is made
var copyOfDataData = largeData
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.
class Membership {
func renew() {
// Generic renewal process
}
}
protects the class from being utilized in ways that contradict its lifetime nature.
init(customParameter: [Int]) {
// Custom initialization logic
super.init(frame: .zero)
// Additional setup using customParameter
}
}
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])
}
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
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
In this example, the Comparable protocol allows for a natural and intuitive
comparison of growth stages, reflecting their sequential nature.
// 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.
/* 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
/* 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.
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)
}
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)
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
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
Closures allow us to include the initialization logic within the property declara-
tion itself, keeping related code organized and easily accessible.
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"
}
}()
init(gameMode: String) {
self.gameMode = gameMode
}
}
level without requiring additional input from the developer or user at the time
of initialization.
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.
class UserProfile {
var userID: String
init(userID: String) {
self.userID = userID
}
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
init(radius: Double) {
self.radius = radius
}
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.
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.
init(value: String) {
myProperty = "Initial value"
setupPropertyValue(value: 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"
}
}
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.
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
// Prints `100.0`
print(account.balance)
restricted to the methods deposit() and withdraw() within the Account class,
preventing any unauthorized changes.
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.
protocol NetworkService {
var baseURL: URL { get }
var timeoutInterval: TimeInterval { get }
}
extension NetworkService {
var baseURL: URL {
return URL(string: "https://api.example.com")!
}
// Prints `https://api.example.com`
print(productService.baseURL)
// 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")!
}
}
// Prints `https://special.api.example.com`
print(specialUserService.baseURL)
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
}
}
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
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.
import Foundation
class UserProfile {
var birthDate: Date?
return age
}
}
init(birthDate: Date) {
self.birthDate = birthDate
}
}
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
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.
struct MyValue<T> {
static var a: Int {
get {
_gValues[ObjectIdentifier(Self.self)] as? Int ?? 42
}
set {
_gValues[ObjectIdentifier(Self.self)] = newValue
}
}
}
// 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)
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.
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
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
struct User {
@SafeString var username: String
}
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
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 }
}
func areSameInstance(
_ lhs: IdentifiableClass, _ rhs: IdentifiableClass
) -> Bool {
return lhs === rhs
}
// 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
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
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.
protocol Clonable {
func clone() -> Self
}
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.
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.
protocol Container {
associatedtype ItemType
mutating func append(_ item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
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
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.
protocol Drawable {
func draw()
}
extension Drawable {
func draw() {
print("Default drawing")
}
}
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
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.
protocol PaymentProcessor {
func process(amount: Double)
}
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
func start() {
print("Tesla Model S is starting.")
}
func stop() {
print("Tesla Model S is stopping.")
}
func chargeBattery() {
print("Charging the battery.")
}
}
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
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
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
}
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
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
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
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: <)
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
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"
]
/* 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.
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)
]
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
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.
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
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
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
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.
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
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)
""")
}
}
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
/* 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
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())
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
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]
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
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
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.
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
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.
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
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.
"""
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
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\"}
"""
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
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.
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 `��`
print(faceWithSpiralEyes)
// Prints `1`
print(faceWithSpiralEyes.count)
text-based content that includes colorful and expressive emoji characters easily.
148 STRING MANIPULATION TECHNIQUES
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
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
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)
}
}
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.
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
Task {
do {
let data = try await fetchDataAsync()
print("Data received: \(data)")
} catch {
print("Failed to fetch data: \(error)")
}
}
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()
}
The compiler’s ability to infer that a closure should run on the @MainActor
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 157
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.
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
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()
}
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.
Task {
await fetchUserData()
await performBackgroundTask()
}
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
do {
let content = try String(
contentsOf: fileURL, encoding: .utf8
)
return content
} catch {
throw FileError.unreadableContent
}
}
ASYNCHRONOUS PROGRAMMING AND ERROR HANDLING 165
Task {
await displayFileContent()
}
Here’s a practical example to illustrate how to safely rethrow errors with addi-
tional context in a concurrent environment.
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
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)
}
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.
do {
let totalCount = try (
fetchCountFromServer() + fetchCountFromDatabase()
)
print("Total Count: \(totalCount)")
} catch {
print("An error occurred: \(error)")
}
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:
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.
import Foundation
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
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.
if didFail {
return .failure(.noDataAvailable)
} else {
return .success("Data loaded successfully.")
}
}
do {
try processData()
} catch {
print("Failed to process data: \(error)")
}
up the chain.
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
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
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:
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
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
func ???<T>(
optionalValue: T?,
defaultValue: @autoclosure () -> String
) -> String {
optionalValue
.map { String(describing: $0) } ?? defaultValue()
}
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
To customize the output and make it fit the format we prefer, we can implement
CustomDebugStringConvertible.
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
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
}
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
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
}
dump(user)
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
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.
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
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.
extension NetworkManager {
struct Endpoint {
let path: String
let queryParameters: [String: String]?
}
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
}
}
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.
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.
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
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
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.
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
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