Swift Cookbook - Second Edition

By Keith Moon , Chris Barker
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Mastering the Building Blocks

About this book

Swift is an exciting, multi-platform, general-purpose programming language, and with this book, you'll explore the features of its latest version, Swift 5.3.

The book begins with an introduction to the basic building blocks of Swift 5.3, its syntax, and the functionalities of Swift constructs. You’ll then discover how Swift Playgrounds provide an ideal platform to write, execute, and debug your Swift code. As you advance through the chapters, the book will show you how to bundle variables into tuples or sets, order your data with an array, store key-value pairs with dictionaries, and use property observers. You’ll also get to grips with the decision-making and control structures in Swift, examine advanced features such as generics and operators, and explore functionalities outside of the standard library. Once you’ve learned how to build iOS applications using UIKit, you'll find out how to use Swift for server-side programming, run Swift on Linux, and investigate Vapor. Finally, you'll discover some of the newest features of Swift 5.3 using SwiftUI and Combine to build adaptive and reactive applications, and find out how to use Swift to build and integrate machine learning models along with Apple’s Vision Framework.

By the end of this Swift book, you'll have discovered solutions to boost your productivity while developing code using Swift 5.3.

Publication date:
February 2021
Publisher
Packt
Pages
500
ISBN
9781839211195

 
Mastering the Building Blocks

The previous chapter explained the basic types that form the building blocks of the Swift language. In this chapter, we will build on this knowledge to create more complex structures, such as arrays and dictionaries, before moving on and looking at some of the little gems Swift offers, such as tuples and typealias. Finally, we'll round off this chapter by looking at extensions and access control – both of which are key components that contribute to a sound yet efficient codebase.

In this chapter, we will cover the following recipes:

  • Bundling variables into tuples
  • Ordering your data with arrays
  • Containing your data in sets
  • Storing key-value pairs with dictionaries
  • Subscripts for custom types
  • Changing your name with typealias
  • Getting property changing notifications using property observers
  • Extending functionality with extensions
  • Controlling access with access control

Let's get started!

 

Technical requirements

 

Bundling variables into tuples

A tuple is a combination of two or more values that can be treated as one. If you have ever wished you could return more than one value from a function or method, you should find tuples very interesting.

Getting ready

Create a new playground and add the following statement:

import Foundation 

This example uses one function from Foundation. We will delve into Foundation in more detail in Chapter 5, Beyond the Standard Library, but for now, we just need to import it.

How to do it...

Let's imagine that we are building an app that pulls movie ratings from multiple sources and presents them together to help the user decide which movie to watch. These sources may use different rating systems, such as the following:

  • Number of stars out of 5
  • Points out of 10
  • Percentage score

We want to normalize these ratings so that they can be compared directly and displayed side by side. We want all the ratings to be represented as a number of stars out of 5, so we will write a function that will return the number of whole stars out of 5. We will then use this to display the correct number of stars in our user interface (UI).

Our UI also includes a label that will read x Star Movie, where x is the number of stars. It would be useful if our function returned both the number of stars and a string that we can put in the UI. We can use a tuple to do this. Let's get started:

  1. Create a function to normalize the star ratings. The following function takes a rating and a total possible rating, and then returns a tuple of the normalized rating and a string to display in the UI:
func normalizedStarRating(forRating rating: Float, 
ofPossibleTotal total: Float) -> (Int, String) {

}
  1. Inside the function, calculate the fraction of the total score. Then, multiply that by our normalized total score, 5, and round it to the nearest whole number:
let fraction = rating / total 
let ratingOutOf5 = fraction * 5
let roundedRating = round(ratingOutOf5) // Rounds to the nearest
// integer.
  1. Still within the function, take the rounded fraction and convert it from a Float into an Int. Then, create the display string and return both Int and String as a tuple:
let numberOfStars = Int(roundedRating) // Turns a Float into an Int 
let ratingString = "\(numberOfStars) Star Movie"
return (numberOfStars, ratingString)
  1. Call our new function and store the result in a constant:
let ratingAndDisplayString = normalisedStarRating(forRating: 5, 
ofPossibleTotal: 10)
  1. Retrieve the number of stars rating from the tuple and print the result:
let ratingNumber = ratingAndDisplayString.0 
print(ratingNumber) // 3 - Use to show the right number of stars
  1. Retrieve the display string from the tuple and print the result:
let ratingString = ratingAndDisplayString.1 
print(ratingString) // "3 Star Movie" - Use to put in the label

With that, we have created and used a tuple.

How it works...

A tuple is declared as a comma-separated list of the types it contains, within brackets. In the preceding code, you can see a tuple being declared as (Int, String). The function, normalizedStarRating, normalizes the rating and creates numberOfStars as the closest round number of stars and ratingString as a display string. These values are then combined into a tuple by putting them, separated by a comma, within brackets; that is, (numberOfStars, ratingString). This tuple value is then returned by the function.

Next, let's look at what we can do with that returned tuple value:

let ratingAndDisplayString = normalizedStarRating(forRating: 5, 
ofPossibleTotal: 10)

Calling our function returns a tuple that we store in a constant called ratingAndDisplayString. We can access the tuple's components by accessing the numbered member of the tuple:

let ratingNumber = ratingAndDisplayString.0 
print(ratingNumber) // 3 - Use to show the right number of stars

let ratingString = ratingAndDisplayString.1
print(ratingString) // "3 Star Movie" - Use to put in the label
As is the case with most numbered systems in programming languages, the member numbering system starts with 0. The number that's used to identify a certain place within a numbered collection is called an index.

There is another way to retrieve the components of a tuple that can be easier to remember than the numbered index. By specifying a tuple of variable names, each value of the tuple will be assigned to the respective variable names. Due to this, we can simplify accessing the tuple values and printing the result:

let (nextNumber, nextString) = normalizedStarRating(forRating: 8, 
ofPossibleTotal: 10)
print(nextNumber) // 4
print(nextString) // "4 Star Movie"

Since the numerical value is the first value in the returned tuple, this gets assigned to the nextNumber constant, while the second value, the string, gets assigned to nextString. These can then be used like any other constant and removes the need to remember which index refers to which value.

There's more...

As we mentioned previously, accessing a tuple's components via a number is not ideal as we have to remember their order in the tuple to ensure that we are accessing the correct one. To provide some context, we can add labels to the tuple components, which can be used to identify them when they are accessed. Tuple labels are defined in a similar way to parameter labels, preceding the type and separated by a :. Let's add labels to the function we created in this recipe and then use those labels to access the tuple values:

func normalizedStarRating(forRating rating: Float, 
ofPossibleTotal total: Float)
-> (starRating: Int, displayString: String) {

let fraction = rating / total
let ratingOutOf5 = fraction * 5
let roundedRating = round(ratingOutOf5) // Rounds to the nearest
// integer.
let numberOfStars = Int(roundedRating) // Turns a Float into an Int
let ratingString = "\(numberOfStars) Star Movie"

return (starRating: numberOfStars, displayString: ratingString)
}

let ratingAndDisplayString = normalizedStarRating(forRating: 5,
ofPossibleTotal: 10)

let ratingInt = ratingAndDisplayString.starRating
print(ratingInt) // 3 - Use to show the right number of stars

let ratingString = ratingAndDisplayString.displayString
print(ratingString) // "3 Stars" - Use to put in the label

As part of the function declaration, we can see the tuple being declared:

(starRating: Int, displayString: String)

When a tuple of that type is created, the provided values are preceded by the label:

return (starRating: numberOfStars, displayString: ratingString)

To access the components of the tuple, we can use these labels (although the number of indexes still work):

let ratingValue = ratingAndDisplayString.starRating 
print(ratingValue) // 3 - Use to show the right number of stars

let ratingString = ratingAndDisplayString.displayString
print(ratingString) // "3 Stars" - Use to put in the label

Tuples are a convenient and lightweight way to bundle values together.

In this example, we created a tuple with two components. However, a tuple can contain any number of components.

See also

Further information about tuples can be found in Apple's documentation on the Swift language at https://docs.swift.org/swift-book/ReferenceManual/Types.html.

 

Ordering your data with arrays

So far in this book, we have learned about many different Swift constructs: classes, structs, enums, closures, protocols, and tuples. However, it is rare to deal with just one instance of these on their own. Often, we will have many of these constructs, and we need a way to collect multiple instances and place them in useful data structures. Over the next few recipes, we will examine three collection data structures provided by Swift; that is, arrays, sets, and dictionaries (dictionaries are often called hash tables in other programming languages):

Figure 2.1 – Collection of data structures

While doing this, we will look at how to use them to store and access information, and then examine their relative characteristics.

Getting started

First, let's investigate arrays, which are ordered lists of elements. We won't be using any components from the previous recipes, so you can create a new playground for this recipe.

How to do it...

Let's use an array to organize a list of movies to watch:

  1. Create an array called moviesToWatch. This will hold our strings:
var moviesToWatch: Array<String> = Array()
  1. Append three movies to the end of our movie list array:
moviesToWatch.append("The Shawshank Redemption") 
moviesToWatch.append("Ghostbusters")
moviesToWatch.append("Terminator 2")
  1. Print the names of each movie in the list, in turn:
print(moviesToWatch[0]) // "The Shawshank Redemption"
print(moviesToWatch[1]) // "Ghostbusters"
print(moviesToWatch[2]) // "Terminator 2"
  1. Print a count of the number of movies in the list so far:
print(moviesToWatch.count) // 3
  1. Insert a new movie into the list so that it's the third one in it. Since arrays are zero-based, this is done at index 2:
moviesToWatch.insert("The Matrix", at: 2)
  1. Print the list count to check it has increased by one and print the newly updated list:
print(moviesToWatch.count) // 4
print(moviesToWatch)
// The Shawshank Redemption
// Ghostbusters
// The Matrix
// Terminator 2
  1. Use the first and last array properties to access their respective values and print them:
let firstMovieToWatch = moviesToWatch.first 
print(firstMovieToWatch as Any) // Optional("The Shawshank
Redemption")
let lastMovieToWatch = moviesToWatch.last
print(lastMovieToWatch as Any) // Optional("Terminator 2")
  1. Use an index subscript to access the second movie in the list and print it. Then, set a new value to that same subscript. Once you've done that, print the list count to check the number of movies that haven't changed and print the list to check that the second array element has changed:
let secondMovieToWatch = moviesToWatch[1] 
print(secondMovieToWatch) // "Ghostbusters"
moviesToWatch[1] = "Ghostbusters (1984)"
print(moviesToWatch.count) // 4
print(moviesToWatch)
// The Shawshank Redemption
// Ghostbusters (1984)
// The Matrix
// Terminator 2
  1. Create a new array of spy movies by initializing it with some movies using the array literal syntax:
let spyMovieSuggestions: [String] = ["The Bourne Identity", 
"Casino Royale",
"Mission Impossible"]
  1. Combine the two arrays we have created using the addition operator (+) and assign them back to the moviesToWatch variable. Then, print the array count so that it reflects the two lists combined and print the new list:
moviesToWatch = moviesToWatch + spyMovieSuggestions 
print(moviesToWatch.count) // 7
print(moviesToWatch)
// The Shawshank Redemption
// Ghostbusters (1984)
// The Matrix
// Terminator 2
// The Bourne Identity
// Casino Royale
// Mission Impossible
  1. Next, use an array convenience initializer to create an array that contains three entries that are the same. Then, update each array element so that the rest of their movie titles are shown:
var starWarsTrilogy = Array<String>(repeating: "Star Wars: ", 
count: 3)
starWarsTrilogy[0] = starWarsTrilogy[0] + "A New Hope"
starWarsTrilogy[1] = starWarsTrilogy[1] + "Empire Strikes Back"
starWarsTrilogy[2] = starWarsTrilogy[2] + "Return of the Jedi"
print(starWarsTrilogy)
// Star Wars: A New Hope
// Star Wars: Empire Strikes Back
// Star Wars: Return of the Jedi
  1. Let's replace part of our existing movie list with our starWarsTrilogy list, and then print the count and list:
moviesToWatch.replaceSubrange(2...4, with: starWarsTrilogy) 
print(moviesToWatch.count) // 7
print(moviesToWatch)
// The Shawshank Redemption
// Ghostbusters (1984)
// Star Wars: A New Hope
// Star Wars: Empire Strikes Back
// Star Wars: Return of the Jedi
// Casino Royale
// Mission Impossible
  1. Lastly, remove the last movie in the list and check that the array count has reduced by one:
moviesToWatch.remove(at: 6) 
print(moviesToWatch.count) // 6
print(moviesToWatch)
// The Shawshank Redemption
// Ghostbusters (1984)
// Star Wars: A New Hope
// Star Wars: Empire Strikes Back
// Star Wars: Return of the Jedi
// Casino Royale

With that, we've looked at many ways we can create and manipulate arrays.

How it works...

When creating an array, we need to specify the type of elements that will be stored in the array. The array element type is declared in angular brackets as part of the array's type declaration. In our case, we are storing strings:

var moviesToWatch: Array<String> = Array() 
moviesToWatch.append("The Shawshank Redemption")
moviesToWatch.append("Ghostbusters")
moviesToWatch.append("Terminator 2")

The preceding code uses a Swift language feature called generics, which can be found in many programming languages, and will be covered in detail in Chapter 4, Generics, Operators, and Nested Types.

The append method of Array will add a new element to the end of the array. Now that we have put some elements in the array, we can retrieve and print those elements:

print(moviesToWatch[0]) // "The Shawshank Redemption" 
print(moviesToWatch[1]) // "Ghostbusters"
print(moviesToWatch[2]) // "Terminator 2"

Elements in an array are numbered with a zero-based index, so the first element in the array is at index 0, the second is at index 1, the third is at index 2, and so on. We can access the elements in the array using a subscript, in which we provide the index of the element we want to access. A subscript is specified in square brackets, after the array instance's name.

When an element is accessed using the index subscript, no check is done to ensure you have provided a valid index. In fact, if an index is provided that the array doesn't contain, this will cause a crash. Instead, we can use some index helper methods on Array to ensure that we have an index that is valid for this array. Let's use one of these helper methods to check an index that we know is valid for our array, and then another that we know is not valid:

let index5 = moviesToWatch.index(moviesToWatch.startIndex,
offsetBy: 5,
limitedBy: moviesToWatch.endIndex)
print(index5 as Any) // Optional(5)

let index10 = moviesToWatch.index(moviesToWatch.startIndex,
offsetBy: 10,
limitedBy: moviesToWatch.endIndex)
print(index10 as Any) // nil

The index method lets us specify the index we want as an offset of the first index parameter, but as something that's limited by the last index parameter. This will return the valid index if it is within the bounds, or nil if it is not. By the end of the playground, the moviesToWatch array contains six elements, in which case retrieving index 5 is successful but index 10 returns nil.

In the next chapter, we will cover how to make decisions based on whether this index exists, but for now, it's just useful to know that this method is available.

Arrays have a count property that tells us how many elements they store. So, when we add an element, this value will change:

print(moviesToWatch.count) // 3 

Elements can be inserted anywhere in the array using the same zero-based index that we used in the preceding code:

moviesToWatch.insert("The Matrix", at: 2) 

So, by inserting "The Matrix" at index 2, it will be placed at the third position in our array, and all the elements at position 2 or greater will be moved down by 1.

This increases the array's count:

print(moviesToWatch.count) // 4

The array also provides some helpful computed properties for accessing elements at either end of the array:

let firstMovieToWatch = moviesToWatch.first 
print(firstMovieToWatch as Any) // Optional("The Shawshank Redemption")
let lastMovieToWatch = moviesToWatch.last
print(firstMovieToWatch as Any) // Optional("Terminator 2")
let secondMovieToWatch = moviesToWatch[1]
print(secondMovieToWatch) // "Ghostbusters"

These properties are optional values as the array may be empty, and if it is, these will be nil. However, accessing an array element via an index subscript returns a non-optional value.

In addition to retrieving values via the subscript, we can also assign values to an array subscript:

moviesToWatch[1] = "Ghostbusters (1984)" 

This will replace the element at the given index with the new value.

When we created our first array, we created an empty array and then appended values to it. Additionally, an array literal can be used to create an array that already contains values:

let spyMovieSuggestions: [String] = ["The Bourne Identity", 
"Casino Royale",
"Mission Impossible"]

An array type can be specified with the element type enclosed by square brackets, and the array literal can be defined by comma-separated elements within square brackets. So, we can define an array of integers like this:

let fibonacci: [Int] = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] 

As we learned in the previous chapter, the compiler can often infer the type from the value we assign, and when the type is inferred, we don't need to specify it. In both the preceding arrays, spyMovieSuggestions and fibonacci, all the elements in the array are of the same type; that is, String and Int, respectively. Since these types can be inferred, we don't need to define them:

let spyMovieSuggestions = ["The Bourne Identity", "Casino Royale", 
"Mission Impossible"]
let fibonacci = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Arrays can be combined using the + operator:

moviesToWatch = moviesToWatch + spyMovieSuggestions

This will create a new array by appending the elements in the second array to the first.

The array provides a convenience initializer that will fill an array with repeating elements. We can use this initializer to create an array with the name of a well-known movie trilogy:

var starWarsTrilogy = Array<String>(repeating: "Star Wars: ", count: 3)

We can then combine subscript access, string appending, and subscript assignment to add the full movie name to our trilogy array:

starWarsTrilogy[0] = starWarsTrilogy[0] + "A New Hope" 
starWarsTrilogy[1] = starWarsTrilogy[1] + "Empire Strikes Back"
starWarsTrilogy[2] = starWarsTrilogy[2] + "Return of the Jedi"

The array also provides a helper for replacing a range of values with the values contained in another array:

moviesToWatch.replaceSubrange(2...4, with: starWarsTrilogy)

Here, we have specified a range using ... to indicate a range between two integer values, inclusive of those values. So, this range contains the integers 2, 3, and 4.

We will specify ranges in this way in subsequent chapters. Alternatively, you can specify a range that goes up to, but not including, the top of the range. This is known as a half-open range:

moviesToWatch.replaceSubrange(2..<5, with: starWarsTrilogy)  

For our arrays, we've added elements, accessed them, and replaced them, so we need to know how to remove elements from an array:

moviesToWatch.remove(at: 6) 

Provide the index of the element to the remove method. By doing this, the element at that index will be removed from the array, and all the subsequent elements will move up one place to fill the empty space. This will reduce the array's count by 1:

print(moviesToWatch.count) // 6 

There's more...

If you are familiar with Objective-C, you will have used NSArray, which provides similar functionalities to a Swift array. You may also remember that NSArray is immutable, which means its contents can't be changed once it's been created. If you need to change its contents, then an NSMutableArray should be used instead. Due to this, you may be wondering if Swift has similar concepts of mutable and immutable arrays. It does but rather than using separate mutable and immutable types, you create a mutable array by declaring it as a variable and an immutable array by declaring it as a constant:

let evenNumbersTo10 = [2, 4, 6, 8, 10] 
evenNumbersTo10.append(12) // Doesn't compile

var evenNumbersTo12 = evenNumbersTo10
evenNumbersTo12.append(12) // Does compile

To understand why this is the case, it's important to know that an array is a value type, as are the other collection types in Swift.

As we saw in the previous chapter, a value type is immutable in nature and creates a changed copy whenever it is mutated. Therefore, by assigning the array to a constant using let, we prevent any new value from being assigned, making mutating the array impossible.

See also

Further information about arrays can be found in Apple's documentation on the Swift language at https://developer.apple.com/documentation/swift/array.

Arrays use generics to define the element type they contain. Generics will be discussed in detail in Chapter 4, Generics, Operators, and Nested Types.

 

Containing your data in sets

The next collection type we will look at is a set. Sets differ from arrays in two important ways. The elements in a set are stored unordered, and each unique element is only held once. In this recipe, we will learn how to create and manipulate sets.

How to do it...

First, let's explore some ways we can create sets and perform set algebra on them:

  1. Create an array that contains the first nine Fibonacci numbers, and also a set containing the same:
let fibonacciArray: Array<Int> = [1, 1, 2, 3, 5, 8, 13, 21, 34] 
let fibonacciSet: Set<Int> = [1, 1, 2, 3, 5, 8, 13, 21, 34]
print(fibonacciArray.count) // 9
print(fibonacciSet.count) // 8
  1. Print out the number of elements in each collection using the count property. Despite being created with the same elements, the count value is different:
print(fibonacciArray.count) // 9 
print(fibonacciSet.count) // 8
  1. Insert an element into a set of animals, remove an element, and check whether a set contains a given element:
var animals: Set<String> = ["cat", "dog", "mouse", "elephant"] 
animals.insert("rabbit")
print(animals.contains("dog")) // true
animals.remove("dog")
print(animals.contains("dog")) // false
  1. Create some sets containing common mathematical number groups. We will use these to explore some methods for set algebra:
let evenNumbers = Set<Int>(arrayLiteral: 2, 4, 6, 8, 10) 
let oddNumbers: Set<Int> = [1, 3, 5, 7, 9]
let squareNumbers: Set<Int> = [1, 4, 9]
let triangularNumbers: Set<Int> = [1, 3, 6, 10]
  1. Obtain the union of two sets and print the result:
let evenOrTriangularNumbers = evenNumbers.union(triangularNumbers) 
// 2, 4, 6, 8, 10, 1, 3, unordered
print(evenOrTriangularNumbers.count) // 7
  1. Obtain the intersection of two sets and print the result:
let oddAndSquareNumbers = oddNumbers.intersection(squareNumbers) 
// 1, 9, unordered
print(oddAndSquareNumbers.count) // 2
  1. Obtain the symmetric difference of two sets and print the result:
let squareOrTriangularNotBoth = 
squareNumbers.symmetricDifference(triangularNumbers)
// 4, 9, 3, 6, 10, unordered
print(squareOrTriangularNotBoth.count) // 5
  1. Obtain the result of subtracting one set from another and print the result:
let squareNotOdd = squareNumbers.subtracting(oddNumbers) // 4 
print(squareNotOdd.count) // 1

Next, we will examine the set membership comparison methods that are available:

  1. Create some sets with overlapping membership:
let animalKingdom: Set<String> = ["dog", "cat", "pidgeon", 
"chimpanzee", "snake", "kangaroo",
"giraffe", "elephant", "tiger",
"lion", "panther"]

let vertebrates: Set<String> = ["dog", "cat", "pidgeon",
"chimpanzee", "snake", "kangaroo",
"giraffe", "elephant", "tiger",
"lion", "panther"]

let reptile: Set<String> = ["snake"]

let mammals: Set<String> = ["dog", "cat", "chimpanzee",
"kangaroo", "giraffe", "elephant",
"tiger", "lion", "panther"]

let catFamily: Set<String> = ["cat", "tiger", "lion", "panther"]

let domesticAnimals: Set<String> = ["cat", "dog"]
  1. Use the isSubset method to determine whether one set is a subset of another. Then, print the result:
print(mammals.isSubset(of: animalKingdom)) // true
  1. Use the isSuperset method to determine whether one set is a superset of another. Then, print the result:
print(mammals.isSuperset(of: catFamily)) // true
  1. Use the isStrictSubset method to determine whether one set is a strict subset of another. Then, print the result:
print(vertebrates.isStrictSubset(of: animalKingdom)) // false 
print(mammals.isStrictSubset(of: animalKingdom)) // true
  1. Use the isStrictSuperset method to determine whether one set is a strict superset of another. Then, print the result:
print(animalKingdom.isStrictSuperset(of: vertebrates)) // false 
print(animalKingdom.isStrictSuperset(of: domesticAnimals)) // true
  1. Use the isDisjoint method to determine whether one set is disjointed with another. Then, print the result:
print(catFamily.isDisjoint(with: reptile)) // true

How it works...

Sets are created in almost the same way as arrays, and like arrays, we have to specify the element type that we will be stored in them:

let fibonacciArray: Array<Int> = [1, 1, 2, 3, 5, 8, 13, 21, 34] 
let fibonacciSet: Set<Int> = [1, 1, 2, 3, 5, 8, 13, 21, 34]

Arrays and sets store their elements differently. If you provide multiple elements of the same value to an array, it will store them multiple times. A set works differently; it will only store one version of each unique element. Therefore, in the preceding Fibonacci number sequence, the array stores two elements for the first two values, 1, 1, but the set will store this as just one 1 element. This leads to the collections having different counts, despite being created with the same values:

print(fibonacciArray.count) // 9 
print(fibonacciSet.count) // 8

This ability to store elements uniquely is made possible due to a requirement that a set has regarding the type of elements it can hold. A set's elements must conform to the Hashable protocol. This protocol requires a hashValue property to be provided as an Int, and the set uses this hashValue to do its uniqueness comparison. Both the Int and String types conform to Hashable, but any custom types that will be stored in a set will also need to conform to Hashable.

A set's insert, remove, and contains methods work as you would expect, with the compiler enforcing that the correct types are provided. This compiler type checking is done thanks to the generics constraints that all the collection types have. We will cover generics in more detail in Chapter 4, Generics, Operators, and Nested Types.

Union

The union method returns a set containing all the unique elements from the set that the method is called on, as well as the set that was provided as a parameter:

let evenOrTriangularNumbers = evenNumbers.union(triangularNumbers) 
// 2,4,6,8,10,1,3,unordered

The following diagram depicts the Union of Set A and Set B:

Figure 2.2 – Union of sets

Intersection

The intersection method returns a set of unique elements that were contained in both the set that the method was called on and the set that was provided as a parameter:

let oddAndSquareNumbers = oddNumbers.intersection(squareNumbers) 
// 1, 9, unordered

The following diagram depicts the Intersection of Set A and Set B:

Figure 2.3 – Set intersection

Symmetric difference

The symmetricDifference method returns a set of unique elements that are in either the set the method is called on, or the set that's provided as a parameter, but not elements that are in both:

let squareOrTriangularNotBoth =
squareNumbers.symmetricDifference(triangularNumbers)
// 4, 9, 3, 6, 10, unordered
This set operation is sometimes referred to as method is so exclusiveOr, both other programming languages, including previous versions of Swift.

The following diagram depicts the Symmetric Difference of Set A and Set B:

Figure 2.4 – Symmetric difference

Subtracting

The subtracting method returns a unique set of elements that can be found in the set the method was called on, but not in the set that was passed as a parameter. Unlike the other set manipulation methods we've mentioned, this will not necessarily return the same value if you swap the set that the method is called on with the set provided as a parameter:

let squareNotOdd = squareNumbers.subtracting(oddNumbers) // 4 

The following diagram depicts the set that's created by Subtracting Set B from Set A:

Figure 2.5– Subtracting a set

Membership comparison

In addition to set manipulation methods, there are a number of methods we can use to determine information about set membership.

The isSubset method will return true if all the elements in the set that the method is called on are contained within the set that's passed as a parameter:

print(mammals.isSubset(of: animalKingdom)) // true

The following diagram depicts Set B as the subset of Set A:

Figure 2.6 – Subset

This will also return true if the two sets are equal (they contain the same elements). If you only want a true value if the set that the method is called on is a subset and not equal, then you can use isStrictSubset:

print(vertebrates.isStrictSubset(of: animalKingdom)) // false 
print(mammals.isStrictSubset(of: animalKingdom)) // true

The isSuperset method will return true if all the elements in the set that have been passed as a parameter are within the set that the method is called on:

print(mammals.isSuperset(of: catFamily)) // true 

The following diagram depicts Set A as the superset of Set B:

Figure 2.7 – Superset

This will also return true if the two sets are equal (they contain the same elements). If you only want a true value if the set that the method is called on is a superset and not equal, then you can use isStrictSuperset:

print(animalKingdom.isStrictSuperset(of: vertebrates))     // false 
print(animalKingdom.isStrictSuperset(of: domesticAnimals)) // true

The isDisjoint method will return true if there are no common elements between the set that the method is called on and the set that was passed as a parameter:

print(catFamily.isDisjoint(with: reptile)) // true 

The following diagram shows that Set A and Set B are disjoint:

Figure 2.8 – Disjoint

As with arrays, a set can be declared immutable by assigning it to a let constant instead of a var variable:

let planets: Set<String> = ["Mercury", "Venus", "Earth", 
"Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto"]
planets.remove("Pluto") // Doesn't compile

This is because a set, like the other collection types, is a value type. Removing an element would mutate the set, which creates a new copy, but a let constant can't have a new value assigned to it, so the compiler prevents any mutating operations.

See also

Further information about arrays can be found in Apple's documentation on the Swift language at https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html.

Sets use generics to define the element types they contain. Generics will be discussed in detail in Chapter 4, Generics, Operators, and Nested Types.

 

Storing key-value pairs with dictionaries

The last collection type we will look at is the dictionary. This is a familiar construct in programming languages, where it is sometimes referred to as a hash table. A dictionary holds a collection of pairings between a key and a value. The key can be any element that conforms to the Hashable protocol (just like elements in a set), while the value can be any type. The contents of a dictionary is not stored in order, unlike an array; instead, the key is used both when storing a value and as a lookup when retrieving a value.

Getting ready

In this recipe, we will use a dictionary to store details of people at a place of work. We need to store and retrieve a person's information based on their role in the organization, such as a company directory. To hold this person's information, we will use a modified version of our Person class from Chapter 1, Swift Building Blocks.

Enter the following code into a new playground:

struct PersonName { 
let givenName: String
let familyName: String
}

enum CommunicationMethod {
case phone
case email
case textMessage
case fax
case telepathy
case subSpaceRelay
case tachyons
}

class Person {
let name: PersonName
let preferredCommunicationMethod: CommunicationMethod

convenience init(givenName: String,
familyName: String,
commsMethod: CommunicationMethod) {
let name = PersonName(givenName: givenName, familyName:
familyName)
self.init(name: name, commsMethod: commsMethod)
}

init(name: PersonName, commsMethod: CommunicationMethod) {
self.name = name
preferredCommunicationMethod = commsMethod
}

var displayName: String {
return "\(name.givenName) \(name.familyName)"
}
}

How to do it...

Let's use the Person object we defined previously to build up our workplace directory using a dictionary:

  1. Create a Dictionary for the employee directory:
var crew = Dictionary<String, Person>()
  1. Populate the dictionary with employee details:
crew["Captain"] = Person(givenName: "Jean-Luc", 
familyName: "Picard",
commsMethod: .phone)

crew["First Officer"] = Person(givenName: "William",
familyName: "Riker",
commsMethod: .email)

crew["Chief Engineer"] = Person(givenName: "Geordi",
familyName: "LaForge",
commsMethod: .textMessage)

crew["Second Officer"] = Person(givenName: "Data",
familyName: "Soong",
commsMethod: .fax)

crew["Councillor"] = Person(givenName: "Deanna",
familyName: "Troi",
commsMethod: .telepathy)

crew["Security Officer"] = Person(givenName: "Tasha",
familyName: "Yar",
commsMethod: .subSpaceRelay)

crew["Chief Medical Officer"] = Person(givenName: "Beverly",
familyName: "Crusher",
commsMethod: .tachyons)
  1. Retrieve an array of all the keys in the dictionary. This will give us an array of all the roles in the organization:
let roles = Array(crew.keys) 
print(roles)
  1. Use a key to retrieve one of the employees and print the result:
let firstRole = roles.first! // Chief Medical Officer 
let cmo = crew[firstRole]! // Person: Beverly Crusher
print("\(firstRole): \(cmo.displayName)")
// Chief Medical Officer: Beverly Crusher
  1. Replace a value in the dictionary by assigning a new value against an existing key. The previous value for the key is discarded when a new value is set:
print(crew["Security Officer"]!.name.givenName) // Tasha

crew["Security Officer"] = Person(givenName: "Worf",
familyName: "Son of Mogh",
commsMethod: .subSpaceRelay)

print(crew["Security Officer"]!.name.givenName) // Worf

With that, we have learned how to create, populate, and look up values in a dictionary.

How it works...

As with the other collection types, when we create a dictionary, we need to provide the types that the dictionary will be holding. For dictionaries, there are two types that we need to define. The first is the type of the key (which must conform to Hashable), while the second is the type of the value being stored against the key. For our dictionary, we are using String for the key and Person for the values being stored:

var crew = Dictionary<String, Person>() 

As with an array, we can specify a dictionary type using square brackets and create one using a dictionary literal, where : separates the key and the value:

let intByName: [String: Int] = ["one": 1, "two": 2, "three": 3] 

Therefore, we can change our dictionary definition so that it looks like this:

var crew: [String: Person] = [:] 

The [:] symbol denotes an empty dictionary as a dictionary literal.

Elements are added to a dictionary using a subscript. Unlike an array, which takes an Int index in the subscript, a dictionary takes the key and then pairs the given value with the given key. In the following example, we are assigning a Person object to the "Captain" key:

crew["Captain"] = Person(givenName: "Jean-Luc", 
familyName: "Picard",
commsMethod: .phone)

If no value currently exists, the assigned value will be added. If a value already exists for the given key, the old value will be replaced with the new value and the old value will be discarded.

There are properties on the dictionary that provide all the keys and values. These properties are of a custom collection type that can be passed to an array initializer to create an array:

let roles = Array(crew.keys)  
print(roles)

To display all the dictionary's keys, as provided by the keys property, we can either create an array or iterate over the collection directly. We will cover iterating over a collection's values in the next chapter, so for now, we will create an array.

Next, we will use one of the values from an array of keys, alongside the crew, to retrieve full details about the associated Person:

let firstRole = roles.first! // Chief Medical Officer 
let cmo = crew[firstRole]! // Person: Beverly Crusher
print("\(firstRole): \(cmo.displayName)")
// Chief Medical Officer: Beverly Crusher

We get the first element using the first property, but since this is an optional type, we need to force unwrap it using !. We can pass firstRole, which is now a non-optional String to the dictionary subscript, to get the Person object associated with that key. The return type for retrieving the value via subscript is also optional, so it also needs to be force unwrapped before we print its values.

Force unwrapping is usually an unsafe thing to do since if we force unwrap a value that turns out to be nil, our code will crash. We advise you to check that a value isn't nil before unwrapping the optional. We will cover how to do this in the next chapter.

There's more...

In this recipe, we used strings as the keys for our dictionary. However, we can also use a type that conforms to the Hashable protocol.

One downside of using String as a key for our employee directory is that it is very easy to mistype an employee's role or look for a role that you expect to exist but doesn't. So, we can improve our implementation by using something that conforms to Hashable and is better suited to being used as a key in our model.

We have a finite set of employee roles in our model, and an enumeration is perfect for representing a finite number of options, so let's define our roles as an enum:

enum Role: String { 
case captain = "Captain"
case firstOfficer = "First Officer"
case secondOfficer = "Second Officer"
case chiefEngineer = "Chief Engineer"
case councillor = "Councillor"
case securityOfficer = "Security Officer"
case chiefMedicalOfficer = "Chief Medical Officer"
}

Now, let's change our Dictionary definition so that it uses this new enum as a key, and then insert our employees using these enum values:

var crew = Dictionary<Role, Person>() 

crew[.captain] = Person(givenName: "Jean-Luc",
familyName: "Picard",
commsMethod: .phone)

crew[.firstOfficer] = Person(givenName: "William",
familyName: "Riker",
commsMethod: .email)

crew[.chiefEngineer] = Person(givenName: "Geordi",
familyName: "LaForge",
commsMethod: .textMessage)

crew[.secondOfficer] = Person(givenName: "Data",
familyName: "Soong",
commsMethod: .fax)

crew[.councillor] = Person(givenName: "Deanna",
familyName: "Troi",
commsMethod: .telepathy)

crew[.securityOfficer] = Person(givenName: "Tasha",
familyName: "Yar",
commsMethod: .subSpaceRelay)

crew[.chiefMedicalOfficer] = Person(givenName: "Beverly",
familyName: "Crusher",
commsMethod: .tachyons)

You will also need to change all the other uses of crew so that they use the new enum-based key.

Let's take a look at how and why this works. We created Role as a String-based enum:

enum Role: String { 
//...
}

Defining it in this way has two benefits:

  • We intend to display these roles to the user, so we will need a string representation of the Role enum, regardless of how we defined it.
  • Enums have a little bit of protocol and generics magic in them, which means that if an enum is backed by a type that implements the Hashable protocol (as String does), the enum also automatically implements the Hashable protocol. Therefore, defining Role as being String-based satisfies the dictionary requirement of a key being Hashable without us having to do any extra work.

With our crew dictionary now defined as having a Role-based key, all subscript operations have to use a value in the role enum:

crew[.captain] = Person(givenName: "Jean-Luc", 
familyName: "Picard",
commsMethod: .phone)
let cmo = crew[.chiefMedicalOfficer]

The compiler enforces this, so it's no longer possible to use an incorrect role when interacting with our employee directory. This pattern of using Swift's constructs and type system to enforce the correct use of your code is something we should strive to do, as it can reduce bugs and prevent our code from being used in unexpected ways.

See also

Further information about dictionaries can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/collections.

 

Subscripts for custom types

By using collection types, we have seen that their elements are accessed through subscripts. However, it's not just collection types that can have subscripts; your own custom types can provide subscript functionality too.

Getting ready

In this recipe, we will create a simple game of tic-tac-toe, also known as Noughts and Crosses. To do this, we need a three-by-three grid of positions, with each position being filled by either a nought from Player 1, a cross from Player 2, or nothing. We can store these positions in an array of arrays.

The initial game setup code uses the concepts we've already covered in this book, so we won't go into its implementation. Enter the following code into a new playground so that we can see how subscripts can improve its usage:

enum GridPosition: String { 
case player1 = "o"
case player2 = "x"
case empty = " "
}

struct TicTacToe {

var gridStorage: [[GridPosition]] = []

init() {
gridStorage.append(Array(repeating: .empty, count: 3))
gridStorage.append(Array(repeating: .empty, count: 3))
gridStorage.append(Array(repeating: .empty, count: 3))
}

func gameStateString() -> String {
var stateString = "-------------\n"
stateString += printableString(forRow: gridStorage[0])
stateString += "-------------\n"
stateString += printableString(forRow: gridStorage[1])
stateString += "-------------\n"
stateString += printableString(forRow: gridStorage[2])
stateString += "-------------\n"

return stateString
}

func printableString(forRow row: [GridPosition]) -> String {
var rowString = "| \(row[0].rawValue) "
rowString += "| \(row[1].rawValue) "
rowString += "| \(row[2].rawValue) |\n"
return rowString
}
}

How to do it...

Let's run through how we can use the tic-tac-toe game defined previously, as well as how we can improve how it is used, using a subscript. We will also examine how this works:

  1. Let's create an instance of our TicTacToe grid:
var game = TicTacToe()
  1. For a player to make a move, we need to change the GridPosition value that's been assigned to the relevant place in the array of arrays. This is used to store the grid positions. Player 1 will place a nought in the middle position of the grid, which would be row position 1, column position 1 (since it's a zero-based array):
// Move 1 
game.gridStorage[1][1] = .player1
print(game.gameStateString())
/*
-------------
| | | |
-------------
| | o | |
-------------
| | | |
-------------
*/
  1. Next, Player 2 places their cross in the top-right position, which is row position 0, column position 2:
// Move 2 
game.gridStorage[0][2] = .player2
print(game.gameStateString())
/*
-------------
| | | x |
-------------
| | o | |
-------------
| | | |
-------------
*/

We can make moves in our game. We can do this by adding information directly to the gridStorage array, which isn't ideal. The player shouldn't need to know how the moves are stored, and we should be able to change how we store the game information without having to change how the moves are made. To solve this, let's create a subscript of our game struct so that making a move in the game is just like assigning a value to an array.

  1. Add the following subscript method to the TicTacToe struct:
struct TicTacToe { 
var gridStorage: [[GridPosition]] = []
//...
subscript(row: Int, column: Int) -> GridPosition {
get {
return gridStorage[row][column]
}
set(newValue) {
gridStorage[row][column] = newValue
}
}
//...
}
  1. So, now, we can change how each player makes their move and finish the game:
// Move 1 
game[1, 1] = .player1
print(game.gameStateString())
/*
-------------
| | | |
-------------
| | o | |
-------------
| | | |
-------------
*/

// Move 2
game[0, 2] = .player2
print(game.gameStateString())
/*
-------------
| | | x |
-------------
| | o | |
-------------
| | | |
-------------
*/

// Move 3
game[0, 0] = .player1
print(game.gameStateString())
/*
-------------
| o | | x |
-------------
| | o | |
-------------
| | | |
-------------
*/

// Move 4
game[1, 2] = .player2
print(game.gameStateString())
/*
-------------
| o | | x |
-------------
| | o | x |
-------------
| | | |
-------------
*/

// Move 5
game[2, 2] = .player1
print(game.gameStateString())
/*
-------------
| o | | x |
-------------
| | o | x |
-------------
| | | o |
-------------
*/
  1. Just like when using an array, we can use a subscript to access the value, as well as assign a value to it:
let topLeft = game[0, 0] 
let middle = game[1, 1]
let bottomRight = game[2, 2]
let p1HasWon = (topLeft == .player1)
&& (middle == .player1)
&& (bottomRight == .player1)

How it works...

Subscript functionality can be defined within a class, struct, or enum, or declared within a protocol as a requirement. To do this, we can define subscript (which is a reserved keyword that activates the required functionality) with input parameters and an output type:

subscript(row: Int, column: Int) -> GridPosition

This subscript definition works like a computed property, where get can be defined to allow you to access values through subscript and set can be defined to assign values using subscript:

subscript(row: Int, column: Int) -> GridPosition {
get {
return gridStorage[row][column]
}
set(newValue) {
gridStorage[row][column] = newValue
}
}

Any number of input parameters can be defined, and these should be provided as comma-separated values in the subscript:

game[1, 2] = .player2 // Assigning a value
let topLeft = game[0, 0] // Accessing a value

There's more...

Just like parameters defined in a function, subscript parameters can have additional labels. If defined, these become required at the call site, so the subscript we added can alternatively be defined as follows:

subscript(atRow row: Int, atColumn column: Int) -> GridPosition 

In this case, when using the subscript, we would also provide the labels in the subscript:

game[atRow: 1, atColumn: 2] = .player2 // Assigning a value 
let topLeft = game[atRow: 0, atColumn: 0] // Accessing a value

See also

Further information about subscripts can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/subscripts.

 

Changing your name with typealias

The typealias declaration allows you to create an alias for a type (and is therefore pretty accurately named!). You can specify a name that can be used in place of any given type of definition. If this type is quite complex, a typeAlias can be a useful way to simplify its use.

How to do it...

We will use a typealias to replace an array definition:

  1. First, let's create something we can store in an array. In this instance, let's create a Pug struct:
struct Pug { 
let name: String
}
  1. Now, we can create an array that will contain instances of a Pug struct:
let pugs = [Pug]() 
As you may or may not know, the collective noun for a group of pugs is called a grumble.
  1. We can set up a typealias to define an array of pugs as a Grumble:
typealias Grumble = [Pug] 
  1. With this defined, we can substitute Grumble wherever we would use [Pug] or Array<Pug>:
var grumble = Grumble()
  1. However, this isn't some new type it is just an array with all the same functionalities:
let marty = Pug(name: "Marty McPug") 
let wolfie = Pug(name: "Wolfgang Pug")
let buddy = Pug(name: "Buddy")
grumble.append(marty)
grumble.append(wolfie)
grumble.append(buddy)

There's more...

The preceding example allows us to use types in a more natural and expressive way. In addition, we can use a typealias to simplify a more complex type that may be used in multiple places.

To see how this might be useful, we can partially build an object to fetch program information:

enum Channel { 
case BBC1
case BBC2
case BBCNews
//...
}

class ProgrammeFetcher {

func fetchCurrentProgrammeName(forChannel channel: Channel,
resultHandler: (String?, Error?) -> Void) {
// ...
// Do the work to get the current programme
// ...
let exampleProgramName = "Sherlock"
resultHandler(exampleProgramName, nil)
}

func fetchNextProgrammeName(forChannel channel: Channel,
resultHandler: (String?, Error?) -> Void) {
// ...
// Do the work to get the next programme
// ...
let exampleProgramName = "Luther"
resultHandler(exampleProgramName, nil)
}
}

In the ProgrammeFetcher object, we have two methods that take a channel and a result handler closure. The result handler closure has the following definition. We have to define this twice; once for each method:

(String?, Error?) -> Void 

Instead, we can define this closure definition with a typealias called FetchResultHandler and replace each method definition with a reference to this typealias:

class ProgrammeFetcher { 

typealias FetchResultHandler = (String?, Error?) -> Void

func fetchCurrentProgrammeName(forChannel channel: Channel,
resultHandler: FetchResultHandler) {
// Get next programme
let programmeName = "Sherlock"
resultHandler(programmeName, nil)
}

func fetchNextProgrammeName(forChannel channel: Channel,
resultHandler: FetchResultHandler) {
// Get next programme
let programmeName = "Luther"
resultHandler(programmeName, nil)
}
}

Not only does this save us from defining the closure type twice, but it is also a better description of the function that the closure performs.

Using typealias doesn't affect how we provide closure to the method:

let fetcher = ProgrammeFetcher() 
fetcher.fetchCurrentProgrammeName(forChannel: .BBC1,
resultHandler: { programmeName, error in
print(programmeName as Any)
})

See also

Further information about typealias can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/declarations.

 

Getting property changing notifications using property observers

It's common to want to know when a property's value changes. Perhaps you want to update the value of another property or update some user interface element. In Objective-C, this was often accomplished by writing your own getter and setter or using Key-Value Observing (KVO), but in Swift, we have native support for property observers.

Getting ready

To examine property observers, we should create an object with a property that we want to observe. Let's create an object that manages users and a property that holds the current user's name:

class UserManager { 
var currentUserName: String = "Emmanuel Goldstein"
}

We want to present some friendly messages when the current user changes. We'll use property observers to do this.

How to do it...

Let's get started:

  1. Amend the currentUserName property definition so that it looks as follows:
class UserManager { 
var currentUserName: String = "Emmanuel Goldstein" {
willSet (newUserName) {
print("Goodbye to \(currentUserName)")
print("I hear \(newUserName) is on their way!")
}
didSet (oldUserName) {
print("Welcome to \(currentUserName)")
print("I miss \(oldUserName) already!")
}
}
}
  1. Create an instance of UserManager and change the current username. This will generate friendly messages:
let manager = UserManager() 

manager.currentUserName = "Dade Murphy"
// Goodbye to Emmanuel Goldstein
// I hear Dade Murphy is on their way!
// Welcome to Dade Murphy
// I miss Emmanuel Goldstein already!

manager.currentUserName = "Kate Libby"
// Goodbye to Dade Murphy
// I hear Kate Libby is on their way!
// Welcome to Kate Libby
// I miss Dade Murphy already!

How it works...

Property observers can be added within curly brackets after the property declaration, and there are two types: willSet and didSet.

The willSet observer will be called before the property is set and provides the value that will be set on the property. This new value can be given a name within brackets; for example, newUserName:

willSet (newUserName) { 
//...
}

The didSet observer will be called after the property is set and provides the value that the property had before being set. This old value can be given a name within brackets; for example, oldUserName:

didSet (oldUserName) { 
//...
}

There's more...

The new value and old value that are passed into the property observers have implicit names, so there is no need to explicitly name them. The willSet observer is passed a value with an implicit name of newValue, and the didSet observer is passed a value with an implicit name of oldValue.

Therefore, we can remove our explicit names and use the implicit value names:

class UserManager { 
var currentUserName: String = "Emmanuel Goldstein" {
willSet {
print("Goodbye to \(currentUserName)")
print("I hear \(newValue) is on their way!")
}
didSet {
print("Welcome to \(currentUserName)")
print("I miss \(oldValue) already!")
}
}
}

See also

Further information about property observers can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/properties.

 

Extending functionality with extensions

Extensions let us add functionality to our existing classes, structs, enums, and protocols. This can be especially useful when the original type is provided by an external framework, which means you aren't able to add functionality directly.

Getting ready

Imagine that we often need to obtain the first word from a given string. Rather than repeatedly writing the code to split the string into words and then retrieving the first word, we can extend the functionality of String to provide its own first word.

How to do it...

Let's get started:

  1. Create an extension of String:
extension String { 

}
  1. Within the extension's curly brackets, add a function that returns the first word from the string:
extension String {
func firstWord() -> String {
let spaceIndex = firstIndex(of: " ") ?? endIndex
let word = prefix(upTo: spaceIndex)
return String(word)
}
}
  1. Now, we can use this new method on String to get the first word from a phrase:
let llap = "Live long, and prosper" 
let firstWord = llap.firstWord()
print(firstWord) // Live

How it works...

We can define an extension using the extension keyword and then specify the type we want to extend. The implementation of this extension is defined within curly brackets:

extension String { 
//...
}

Methods and computed properties can be defined in extensions in the same way that they can be defined within classes, structs, and enums. Here, we will add a firstWord function to the String struct:

extension String {
func firstWord() -> String {
let spaceIndex = firstIndex(of: " ") ?? endIndex
let word = prefix(upTo: spaceIndex)
return String(word)
}
}

The implementation of the firstWord method is not important for this recipe, so we'll just touch on it briefly.

In Swift, String is a collection, so we can use the collection methods to find the first index of an empty space. However, this could be nil since the string may contain only one word or no characters at all, so if the index is nil, we must use the endIndex instead. The nil coalescing operator (??) is only used to assign endIndex if firstIndex(of: " ") is nil. More generally, it will evaluate the value on the left-hand side of the operator, unless it is nil, in which case it will assign the value on the right-hand side.

Then, we use the index of the first space to retrieve the substring up to the index, which has a SubString type. We then use that to create and return a String.

Extensions can implement anything that uses the existing functionality, but they can't store information in a new property. Therefore, computed properties can be added, but stored properties cannot. Let's change our firstWord method so that it's a computed property instead:

extension String {
var firstWord: String {
let spaceIndex = firstIndex(of: " ") ?? endIndex
let word = prefix(upTo: spaceIndex)
return String(word)
}
}

There's more...

Extensions can also be used to add protocol conformance, so let's create a protocol that we want to add conformance to:

  1. The protocol declares that something can be represented as an Int:
protocol IntRepresentable { 
var intValue: Int { get }
}
  1. We can extend Int and have it conform to IntRepresentable by returning itself:
extension Int: IntRepresentable { 
var intValue: Int {
return self
}
}
  1. Next, we'll extend String, and we'll use an Int constructor that takes a String and returns an Int if our String contains digits that represent an integer:
extension String: IntRepresentable { 
var intValue: Int {
return Int(self) ?? 0
}
}
  1. We can also extend our own custom types and add conformance to the same protocol, so let's create an enum that can be IntRepresentable:
enum CrewComplement: Int { 
case enterpriseD = 1014
case voyager = 150
case deepSpaceNine = 2000
}
  1. Since our enum is Int-based, we can conform to IntRepresentable by providing a rawValue:
extension CrewComplement: IntRepresentable { 
var intValue: Int {
return rawValue
}
}
  1. We now have String, Int, and CrewComplement all conforming to IntRepresentable, and since we didn't define String or Int, we have only been able to add conformance through the use of extensions. This common conformance allows us to treat them as the same type:
var intableThings = [IntRepresentable]() 
intableThings.append(55)
intableThings.append(1200)
intableThings.append("5")
intableThings.append("1009")
intableThings.append(CrewComplement.enterpriseD)
intableThings.append(CrewComplement.voyager)
intableThings.append(CrewComplement.deepSpaceNine)

let over1000 = intableThings.compactMap { $0.intValue > 1000 ?
$0.intValue: nil }
print(over1000)

The preceding example includes the use of compactMap and the ternary operator, which haven't been covered in this book. Further information can be found in the See also section.

See also

Further information about extensions can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/extensions.

The documentation for compactMap can be found at https://developer.apple.com/documentation/swift/sequence/2950916-compactmap.

Further information about the ternary operator can be found at https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID71.

 

Controlling access with access control

Swift provides fine-grained access control, allowing you to specify the visibility that your code has to other areas of code. This enables you to be deliberate about the interface you provide to other parts of the system, thus encapsulating implementation logic and helping separate the areas of concern.

Swift has five access levels:

  • Private: Only accessible within the existing scope (defined by curly brackets) or extensions in the same file.
  • File private: Accessible to anything in the same file, but nothing outside the file.
  • Internal: Accessible to anything in the same module, but nothing outside the module.
  • Public: Accessible both inside and outside the module, but cannot be subclassed or overwritten outside of the defining module.
  • Open: Accessible everywhere, with no restrictions in terms of its use, and can therefore be subclassed and overwritten.

These can be applied to types, properties, and functions.

Getting ready

To explore each of these access levels, we need to step outside our playground comfort zone and create a module. To have something that will hold our module and a playground that can use it, we will need to create an Xcode workspace:

  1. In Xcode, select File | New | Workspace... from the menu:
Figure 2.9 – Xcode – New project
  1. Give your workspace a name, such as AccessControl, and choose a save location. You will now see an empty workspace:
Figure 2.10 – Xcode – New project structure

In this workspace, we need to create a module. To illustrate the access controls that are available, let's have our module represent something that tightly controls which information it exposes, and which information it keeps hidden. One thing that fits this definition is Apple; that is, the company.

  1. Create a new project from the Xcode menu by selecting File | New | Project...:
Figure 2.11 – New project
  1. From the template selector, select Framework:
Figure 2.12 – New project framework
  1. Name the project AppleInc:
Figure 2.13 – Naming the project
  1. Choose a location. Then, at the bottom of the window, ensure that Add to: has been set to the workspace we just created:
Figure 2.14 – New project workspace group
  1. Now that we have a module, let's set up a playground to use it in. From the Xcode menu, select File | New | Playground...:
Figure 2.15 – New playground
  1. Give the playground a name and save it to a location:
Figure 2.16 – New project
  1. This playground will not be added to the workspace automatically; you will need to locate the playground you just created and drag it into the file explorer pane on the left-hand side of your workspace.
  2. Press the run button on the Xcode toolbar to build the AppleInc module:
Figure 2.17 – Xcode toolbar
  1. Select the playground from the file navigator and add an import statement to the top of the file:
import AppleInc 

We are now ready to look into the different access controls that are available.

How to do it...

Let's investigate the most restrictive of the access controls: private. Structures marked as private are only visible within the scope of the type they have been defined in, as well as any extensions of that type that are located in the same file. We know that Apple has super-secret areas where it works on its new products, so let's create one:

  1. Select the AppleInc group in the file navigator and create a new file by selecting File | New | File... from the menu. Let's call it SecretProductDepartment.
  2. In this new file, create a SecretProductDepartment class using the private access control:
class SecretProductDepartment { 

private var secretCodeWord = "Titan"
private var secretProducts = ["Apple Glasses",
"Apple Car",
"Apple Brain Implant"]

func nextProduct(codeWord: String) -> String? {
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}
}

Next, let's look at the fileprivate access control. Structures marked as fileprivate are only visible within the file that they are defined in, so a collection of related structures defined in the same file will be visible to each other, but anything outside the file will not see these structures.

When you buy an iPhone from the Apple Store, it's not made in-store; it's made in a factory that the public doesn't have access to. So, let's model this using fileprivate.

  1. Create a new file called AppleStore. Then, create structures for AppleStore and Factory using the fileprivate access control:
public enum DeviceModel {
case iPhone12
case iPhone12Mini
case iPhone12Pro
case iPhone12ProMax
}

public class AppleiPhone {

public let model: DeviceModel

fileprivate init(model: DeviceModel) {
self.model = model
}
}

fileprivate class Factory {
func makeiPhone(ofModel model: DeviceModel) -> AppleiPhone {
return AppleiPhone(model: model)
}
}

public class AppleStore {

private var factory = Factory()

public func selliPhone(ofModel model: DeviceModel)
-> AppleiPhone {
return factory.makeiPhone(ofModel: model)
}
}

To investigate the public access control, we will be defining something that is visible outside the defining module but cannot be subclassed or overridden. Apple itself is the perfect candidate to model this behavior as certain parts of it are visible to the public. However, it closely guards its image and brand, so subclassing Apple to alter and customize it will not be allowed.

  1. Create a new file called Apple and create a class for Apple that uses the public access control:
public class Person { 

public let name: String

public init(name: String) {
self.name = name
}
}

public class Apple {

public private(set) var ceo: Person
private var employees = [Person]()
public let store = AppleStore()
private let secretDepartment = SecretProductDepartment()

public init() {
ceo = Person(name: "Tim Cook")
employees.append(ceo)
}

public func newEmployee(person: Person) {
employees.append(person)
}

func weeklyProductMeeting() {

var superSecretProduct =
secretDepartment.nextProduct(codeWord: "Not sure...
Abracadabra?") // nil

// Try again
superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Titan")
print(superSecretProduct as Any) // "Apple Glasses"
}
}

Lastly, we have the open access control. Structures defined as open are available outside the module and can be subclassed and overridden without restriction. To explain this last control, we want to model something that exists within Apple's domain but is completely open and free from restrictions. So, for this, we can use the Swift language itself!

Swift has been open sourced by Apple, so while they maintain the project, the source code is fully available for others to take, modify, and improve.

  1. Create a new file called SwiftLanguage and create a class for the Swift language that uses the open access control:
open class SwiftLanguage { 

open func versionNumber() -> Float {
return 5.1
}

open func supportedPlatforms() -> [String] {
return ["iOS", "macOS", "tvOS", "watchOS", "Linux"]
}
}

We now have a module that uses Swift's access controls to provide interfaces that match our model and provide the appropriate visibility.

How it works...

Let's examine our SecretProductDepartment class to see how its visibility matches our model:

class SecretProductDepartment { 

private var secretCodeWord = "Titan"
private var secretProducts = ["Apple Glasses",
"Apple Car",
"Apple Brain Implant"]

func nextProduct(codeWord: String) -> String? {
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}
}

The SecretProductDepartment class is declared without an access control keyword, and when no access control is specified, the default control of internal is applied. Since we want the secret product department to be visible within Apple, but not from outside Apple, this is the correct access control to use.

The two properties of the secretCodeWord and secretProducts classes are marked as private, thus hiding their values and existence from anything outside the SecretProductDepartment class. To see this restriction in action, add the following to the same file, but outside the class:

let insecureCodeWord = SecretProductDepartment().secretCodeWord 

When you try to build the module, you are told that secretCodeWord can't be accessed due to the private protection level.

While these properties are not directly accessible, we can provide an interface that allows the information to be provided in a controlled way. This is what the nextProduct method provides:

func nextProduct(codeWord: String) -> String? { 
let codeCorrect = codeWord == secretCodeWord
return codeCorrect ? secretProducts.first : nil
}

If the correct codeword is passed, it will provide the name of the next product from the secret department, but the details of all other products, and the codeword itself, will be hidden. Since this method doesn't have a specified access control, it is set to the default of internal.

It's not possible for contents within a structure to have a more permissive access control than the structure itself. For instance, we can't define the nextProduct method as being public because this is more permissive than the class it is defined in, which is only internal.
Thinking about it, this is the obvious outcome as you cannot create an instance of an internal class outside of the defining module, so how can you possibly call a method on a class instance that you can't even create?

Next, let's look at the AppleStore.swift file we created. The purpose here is to provide people outside of Apple with the ability to purchase an iPhone through the Apple Store, but to restrict the creation of iPhones to just the factories where they are built, and then restrict access to those factories to just the Apple Store:

public enum DeviceModel {
case iPhone12
case iPhone12Mini
case iPhone12Pro
case iPhone12ProMax
}

public class AppleiPhone {

public let model: DeviceModel

fileprivate init(model: DeviceModel) {
self.model = model
}
}

public class AppleStore {

private var factory = Factory()

public func selliPhone(ofModel model: DeviceModel)
-> AppleiPhone {
return factory.makeiPhone(ofModel: model)
}
}

Since we want to be able to sell iPhones outside of the AppleInc module, the DeviceModel enum and the AppleiPhone and AppleStore classes are all declared as public. This has the benefit of making them available outside the module but preventing them from being subclassed or modified. Given how Apple protects the look and feel of their phones and stores, this seems correct for this model.

The Apple Store needs to get their iPhones from somewhere; that is, from the factory:

fileprivate class Factory { 

func makeiPhone(ofModel model: DeviceModel) -> AppleiPhone {
return AppleiPhone(model: model)
}
}

By making the Factory class fileprivate, it is only visible within this file, which is perfect because we only want the Apple Store to be able to use the factory to create iPhones.

We have also restricted the iPhone's initialization method so that it can only be accessed from structures in this file:

fileprivate init(model: DeviceModel) 

The resulting iPhone is public, but only structures within this file can create iPhone class objects in the first place. In this case, this is done by the factory.

Next, let's look at the Apple.swift file:

public class Person { 

public let name: String

public init(name: String) {
self.name = name
}
}

public class Apple {

public private(set) var ceo: Person
private var employees = [Person]()
public let store = AppleStore()
private let secretDepartment = SecretProductDepartment()

public init() {
ceo = Person(name: "Tim Cook")
employees.append(ceo)
}

public func newEmployee(person: Person) {
employees.append(person)
}

func weeklyProductMeeting() {

var superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Not sure...
Abracadabra?") // nil

// Try again
superSecretProduct =
secretDepartment.nextProduct(givenCodeWord: "Titan")
print(superSecretProduct) // "Apple Glasses"
}
}

The preceding code made both the Person and Apple classes public, along with the newEmployee method. This allows new employees to join the company. The CEO, however, is defined as both public and private:

public private(set) var ceo: Person 

We can define a separate, more restrictive, access control for setting a property than the one that was set for getting it. This has the effect of making it a read-only property from outside the defining structure. This provides the access we require since we want the CEO to be visible outside of the AppleInc module, but we want to only be able to change the CEO from within Apple.

The final access control is open. We applied this to the SwiftLanguage class:

open class SwiftLanguage { 

open func versionNumber() -> Float {
return 5.0
}

open func supportedPlatforms() -> [String] {
return ["iOS", "macOSX", "tvOS", "watchOS", "Linux"]
}
}

By declaring the class and methods as open, we are allowing them to be subclassed, overridden, and modified by anyone, including those outside the AppleInc module. With the Swift language being fully open source, this matches what we are trying to achieve.

There's more...

With our module fully defined, let's see how things look from outside the module. We need to build the module to make it available to the playground. Select the playground; it should contain a statement that imports the AppleInc module:

import AppleInc 

First, let's look at the most accessible class that we created; that is, SwiftLanguage. Let's subclass the SwiftLanguage class and override its behavior:

class WinSwift: SwiftLanguage { 

override func versionNumber() -> Float {
return 5.3
}

override func supportedPlatforms() -> [String] {

var supported = super.supportedPlatforms()
supported.append("Windows")

return supported
}
}

Since SwiftLanguage is open, we can subclass it to add more supported platforms and increase its version number.

Next, let's create an instance of the Apple class and see how we can interact with it:

let apple = Apple() 

let keith = Person(name: "Keith Moon")
apple.newEmployee(person: keith)

print("Current CEO: \(apple.ceo.name)")
let craig = Person(name: "Craig Federighi")
apple.ceo = craig // Doesn't compile

We can create Person and provide it to Apple as a new employee since the Person class and the newEmployee method are declared as public. We can retrieve information about the CEO, but we aren't able to set a new CEO as we defined the property as private (set).

Another one of the public interfaces selliPhone provided by the module allows us to buy an iPhone from the Apple Store:

// Buy new iPhone  
let boughtiPhone = apple.store.selliPhone(ofModel: .iPhone12Pro)
// This works

// Try and create your own iPhone
let buildAniPhone = AppleiPhone(model: .iPhone12Pro)
// Doesn't compile

We can retrieve a new iPhone from the Apple Store because we declared the selliPhone method as public. However, we can't create a new iPhone directly since the iPhone's init method is declared as fileprivate.

See also

Further information about access control can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/access-control.

About the Authors

  • Keith Moon

    Keith Moon is an award-winning iOS developer, author and speaker based in London. He has worked with some of the biggest companies in the world to create engaging and personal mobile experiences. Keith has been developing in Swift since its release, working on projects both fully Swift, and mixed Swift and Objective-C. Keith has been invited to speak about Swift development in conferences from Moscow to Minsk and London.

    Browse publications by this author
  • Chris Barker

    Chris Barker is an iOS developer and tech lead for fashion retailer N Brown (JD Williams, SimplyBe, Jacamo), where he heads up the iOS team. Chris started his career developing .NET applications for online retailer dabs (now BT Shop) before he made his move into mobile app development with digital agency Openshadow (now MyStudioFactory Paris). There, he worked on mobile apps for clients such as Louis Vuitton, L'Oréal Paris, and the Paris Metro. Chris often attends and speaks at local iOS developer meetups and conferences such as NSManchester, Malaga Mobile, and CodeMobile.

    Browse publications by this author
Book Title
Unlock this book and the full library for FREE
Start free trial