Search icon CANCEL
Subscription
0
Cart icon
Cart
Close icon
You have no products in your basket yet
Save more on your purchases!
Savings automatically calculated. No voucher code required
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Swift Cookbook - Third Edition
Swift Cookbook - Third Edition

Swift Cookbook: Proven recipes for developing robust iOS applications with Swift 5.9, Third Edition

By Keith Moon , Chris Barker , Daniel Bolella , Nathan Lawlor
€26.99
Book Jun 2024 422 pages 3rd Edition
eBook
€26.99
Print
€33.99
Subscription
€14.99 Monthly
eBook
€26.99
Print
€33.99
Subscription
€14.99 Monthly

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now
Table of content icon View table of contents Preview book icon Preview Book

Swift Cookbook - Third Edition

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 type aliases. 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 code base.

By the end of this chapter, you will be better equipped to organize and handle any data you’ll want to work with in your apps, plus the ability to make the code that interacts with your data more flexible to your needs!

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 a type alias
  • Getting property-changing notifications using property observers
  • Extending functionality with extensions
  • Controlling access with access control

Let’s get started!

Technical requirements

All the code for this chapter can be found in this book’s GitHub repository at https://github.com/PacktPublishing/Swift-Cookbook-Third-Edition/tree/main/Chapter%202.

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, without defining a new struct or class, 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, helping a user decide which movie to watch. These sources may use different rating systems, such as the following:

  • The number of stars out of 5
  • Points out of 10
  • The 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 the 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) {
    }
  2. Inside the function, calculate the fraction of the total score. Then, multiply that by our normalized total score, 5, and round it up to the nearest whole number:
    let fraction = rating / total
    let ratingOutOf5 = fraction * 5
    let roundedRating = round(ratingOutOf5) // Rounds to the nearest
    // integer.
  3. Still within the function, take the rounded fraction and convert it from Float into 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)
  4. Call our new function and store the result in a constant:
    let ratingAndDisplayString = normalisedStarRating(forRating: 5,
    ofPossibleTotal: 10)
  5. 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
  6. 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 section, in step 1, 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) in step 3. This tuple value is then returned by the function.

Now, 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

Note

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, eliminating 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 :. Let’s add labels to the function we created in this recipe and then use them 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 works):

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.

Tip

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/documentation/the-swift-programming-language/types.

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 following 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 – A collection of data structures

Figure 2.1 – A 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 ready

First, let’s investigate arrays, which are an ordered list 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 gamesToPlay. This will hold our strings:
    var gamesToPlay = [String]()
  2. Append three movies to the end of our movie list array:
    gamesToPlay.append("The Secret of Monkey Island")
    gamesToPlay.append("Half Life 2")
    gamesToPlay.append("Alien Isolation")
  3. Print the names of each movie in the list, in turn:
    print(gamesToPlay[0]) // "The Secret of Monkey Island"
    print(gamesToPlay[1]) // "Half Life 2"
    print(gamesToPlay[2]) // "Alien Isolation"
  4. Print a count of the number of movies in the list so far:
    print(gamesToPlay.count) // 3
  5. 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:
    gamesToPlay.insert("Breath of the Wild", at: 2)
  6. Print the list count to check that it has increased by one, and print the newly updated list:
    print (gamesToPlay.count) // 4
    print(gamesToPlay)
    // "The Secret of Monkey Island"
    // "Half Life 2"
    // "Breath of the Wild"
    // "Alien Isolation"
  7. Use the first and last array properties to access their respective values and print them:
    let firstGameToPlay = gamesToPlay.first ?? ""
    print(firstGameToPlay) // "The Secret of Monkey Island"
    let lastGameToPlay = gamesToPlay.last ?? ""
    print(lastGameToPlay as Any) // "Alien Isolation"
  8. 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 = gamesToPlay[1]
    print(secondMovieToWatch) // "Ghostbusters"
    gamesToPlay[1] = "Half Life 2 (2004)"
    print(gamesToPlay.count) // 4
    print(gamesToPlay)
    // "The Secret of Monkey Island"
    // "Half Life 2 (2004)"
    // "Breath of the Wild"
    // "Alien Isolation"
  9. Create a new array of spy movies by initializing it with some movies, using the array literal syntax:
    let graphicAdventureGames: [String] = ["Monkey Island 2",
       "Loom",
       "Sam & Max"]
  10. Combine the two arrays we have created, using the addition operator (+), and assign them back to the gamesToPlay variable. Then, print the array count so that it reflects the two lists combined, and print the new list:
    gamesToPlay = gamesToPlay + graphicAdventureGames
    print(gamesToPlay.count) // 7
    print(gamesToPlay)
    // "The Secret of Monkey Island"
    // "Half Life 2 (2004)"
    // "Breath of the Wild"
    // "Alien Isolation"
    // "Monkey Island 2"
    // "Loom"
    // "Sam & Max"
  11. Now, 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 batmanGames = Array<String>(repeating: "Batman: ", count: 3)
    batmanGames[0] = batmanGames[0] + "Arkham Asylum"
    batmanGames[1] = batmanGames[1] + "Arkham City"
    batmanGames[2] = batmanGames[2] + "Arkham Knight"
    print(batmanGames)
    // Batman: Arkham Asylum
    // Batman: Arkham City
    // Batman: Arkham Knight
  12. Let’s replace part of our existing movie list with our batmanGames list, and then print the count and list:
    gamesToPlay.replaceSubrange(2...4, with: batmanGames)
    print(gamesToPlay.count) // 7
    print(gamesToPlay)
    // "The Secret of Monkey Island"
    // "Half Life 2 (2004)"
    // Batman: Arkham Asylum
    // Batman: Arkham City
    // Batman: Arkham Knight
    // "Breath of the Wild"
    // "Alien Isolation"
  13. Lastly, remove the last movie in the list and check that the array count has reduced by one:
    gamesToPlay.remove(at: 6)
    print(gamesToPlay.count) // 6
    print(gamesToPlay)
    // "The Secret of Monkey Island"
    // "Half Life 2 (2004)"
    // Batman: Arkham Asylum
    // Batman: Arkham City
    // Batman: Arkham Knight
    // "Breath of the Wild"

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 gamesToPlay = Array<String>()
gamesToPlay.append("The Secret of Monkey Island")
gamesToPlay.append("Half Life 2")
gamesToPlay.append("Alien Isolation")

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(gamesToPlay[0]) // "The Secret of Monkey Island"
print(gamesToPlay[1]) // "Half Life 2"
print(gamesToPlay[2]) // "Alien Isolation"

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 = gamesToPlay.index(gamesToPlay.startIndex,
offsetBy: 5,
limitedBy: gamesToPlay.endIndex) print(index5 as Any) // Optional(5)
let index10 = gamesToPlay.index(gamesToPlay.startIndex,
offsetBy: 10,
limitedBy: gamesToPlay.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 gamesToPlay array contains six elements, in which case retrieving index 5 is successful, but index 10 returns nil.

In Chapter 3, Data Wrangling with Swift, 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(gamesToPlay.count) // 3

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

gamesToPlay.insert("Breath of the Wild ", at: 2)

So, by inserting "Breath of the Wild" 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 one.

This increases the array’s count:

print(gamesToPlay.count) // 4

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

let firstGameToPlay = gamesToPlay.first ?? ""
print(firstGameToPlay) // "The Secret of Monkey Island"
let lastGameToPlay = gamesToPlay.last ?? ""
print(lastGameToPlay as Any) // "Alien Isolation"

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.

Note

In the preceding example, we used a nil-coalescing operator (??). This operator allows us to handle situations where the value is nil (e.g., if gamesToPlay was empty), but we need a default returned value (in this case, we return an empty string, "").

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

gamesToPlay[1] = "Half Life 2 (2004)"

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 graphicAdventureGames: [String] = ["Monkey Island 2",
   "Loom",
   "Sam & Max"]

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 Chapter 1, Swift Fundamentals, in the Using the basic types – strings, ints, floats, and booleans recipe, 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, graphicAdventureGames 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 graphicAdventureGames = ["Monkey Island 2", "Loom", "Sam & Max"]
let fibonacci = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Arrays can be combined using the + operator:

gamesToPlay = gamesToPlay + graphicAdventureGames

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 batmanGames = Array<String>(repeating: "Batman: ", count: 3)

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

batmanGames[0] = batmanGames[0] + "Arkham Asylum"
batmanGames[1] = batmanGames[1] + "Arkham City"
batmanGames[2] = batmanGames[2] + "Arkham Knight"

The array also provides a helper to replace a range of values with the values contained in another array:

gamesToPlay.replaceSubrange(2...4, with: batmanGames)

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

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:

gamesToPlay.replaceSubrange(2..<5, with: batmanGames)

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

gamesToPlay.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(gamesToPlay.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 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 Chapter 1, Swift Fundamentals, 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.

Further information about the nil-coalescing operator can be found at https://docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators/#Nil-Coalescing-Operator

Containing your data in sets

The following 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.

Getting ready

In this recipe, we can use the playground from the previous recipe. Don’t worry if you didn’t work through the previous recipe, as this one will contain all the code you need.

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]
  2. 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
  3. 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
  4. 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]
  5. 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
  6. Obtain the intersection of two sets, and print the result:
    let oddAndSquareNumbers = oddNumbers.intersection(squareNumbers)
    // 1, 9, unordered
    print(oddAndSquareNumbers.count) // 2
  7. 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
  8. Obtain the result of subtracting one set from another, and print the result:
    let squareNotOdd = squareNumbers.subtracting(oddNumbers) // 4 print(squareNotOdd.count) // 1

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

  1. Create some sets with overlapping memberships:
    let animalKingdom: Set<String> = ["dog", "cat", "pigeon", "chimpanzee", "snake", "kangaroo", "giraffe", "elephant", "tiger", "lion", "panther"]
    let vertebrates: Set<String> = ["dog", "cat", "pigeon", "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"]
  2. Use the isSubset method to determine whether one set is a subset of another. Then, print the result:
    print(mammals.isSubset(of: animalKingdom)) // true
  3. Use the isSuperset method to determine whether one set is a superset of another. Then, print the result:
    print(mammals.isSuperset(of: catFamily)) // true
  4. 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
  5. 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
  6. 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 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 – A union of sets

Figure 2.2 – A 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 – The set intersection

Figure 2.3 – The 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

Note

This set operation is sometimes referred to as an exclusiveOr method by other programming languages, including previous versions of Swift.

The following diagram depicts the symmetric difference between Set A and Set B:

Figure 2.4 – The symmetric difference

Figure 2.4 – The 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

Figure 2.5 – Subtracting a set

A 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 – The subset

Figure 2.6 – The subset

This will also return true if the two sets are equal (i.e., they contain the same elements). If you only want a true value when 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 – The superset

Figure 2.7 – The superset

This will also return true if the two sets are equal (i.e., they contain the same elements). If you only want a true value when 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 disjointed:

Figure 2.8 – A disjoint

Figure 2.8 – A 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 declaration is possible 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/documentation/the-swift-programming-language/collectiontypes/.

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 are not stored in order, unlike an array; instead, 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 an 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 Fundamentals.

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>()
  2. 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)
  3. 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)
  4. 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
  5. 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 hold. 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 assign 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 in the dictionary that provide all the keys and values. These properties are of a custom collection type, which 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 Chapter 3, Data Wrangling with Swift, so for now, we will create an array.

Now, 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 to retrieve the value via subscript is also optional, so it also needs to be force-unwrapped before we print its values.

Note

Force unwrapping is usually an unsafe thing to do, as 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 Chapter 3.

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 enum role:

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 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 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()
  2. 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 and column position 1 (since it’s a zero-based array):
    // Move 1
    game.gridStorage[1][1] = .player1
    print(game.gameStateString())
    /*

|

|

|

|

|

|

o |

|

|

|

|

|

*/

Figure 2.9 – The grid positions

3. Then, player 2 places their cross in the top-right position, which is row position 0 and 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.

4. 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
  }
 }
 //...
}

5. 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 |
*/

6. 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 subscript, we would also provide the labels in it:

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 a type alias

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 type alias can be a useful way to simplify its use.

Getting ready

In this recipe, 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...

We will use a type alias 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
    }
  2. Now, we can create an array that will contain instances of a Pug struct:
    let pugs = [Pug]()

Note

You may or may not know that the collective noun for a group of pugs is called grumble.

  1. We can set up typealias to define an array of pugs as Grumble:
    typealias Grumble = [Pug]
  2. With this defined, we can substitute Grumble wherever we would use [Pug] or Array<Pug>:
    var grumble = Grumble()
  3. 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 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

Alternatively, 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 also 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 type alias can be found in Apple’s documentation on the Swift language at https://docs.swift.org/swift-book/documentation/the-swift-programming-language/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 UI element. In Objective-C, this was often accomplished by writing your own getter and setter or using Key-Value Observing (KVO). However, 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.

Enter the following code into a new playground:

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 = "Guybrush Threepwood" {
      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!")
      }
     }
    }
  2. Create an instance of UserManager, and change the current username. This will generate friendly messages:
    let manager = UserManager()
    manager.currentUserName = "Elaine Marley"
    // Goodbye to Guybrush Threepwood
    // I hear Elaine Marley is on their way!
    // Welcome to Elaine Marley
    // I miss Guybrush Threepwood already!
    manager.currentUserName = "Ghost Pirare LeChuck"
    // Goodbye to Elaine Marley
    // I hear Ghost Pirare LeChuck is on their way!
    // Welcome to Ghost Pirare LeChuck
    // I miss Elaine Marley 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 = "Guybrush Threepwood" {
  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. These can be especially useful when the original type is provided by an external framework, which means you aren’t able to add functionality directly.

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.

Getting ready

In this recipe, 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 get started:

  1. Create an extension of String:
    extension String {
    }
  2. Within the extension’s curly brackets, add a function that returns the first word from the string:
    func firstWord() -> String {
     let spaceIndex = firstIndex(of: " ") ?? endIndex
     let word = prefix(upTo: spaceIndex)
     return String(word)
    }
  3. Now, we can use this new method on String to get the first word from a phrase:
    let llap = "Ask me about Loom"
    let firstWord = llap.firstWord()
    print(firstWord) // Ask

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 endIndex instead. The nil coalescing operator (??) is only used to assign endIndex if firstIndex(of: " ") is nil.

More generally, the operation 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 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 Int:
    protocol IntRepresentable {
     var intValue: Int { get }
    }
  2. We can extend Int and have it conform to IntRepresentable by returning itself:
    extension Int: IntRepresentable {
     var intValue: Int {
      return self
     }
    }
  3. Now, we’ll extend String, and we’ll use an Int constructor that takes String and returns Int if our String contains digits that represent an integer:
    extension String: IntRepresentable {
     var intValue: Int {
      return Int(self) ?? 0
     }
    }
  4. 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
    }
  5. Since our enum is Int-based, we can conform to IntRepresentable by providing rawValue:
    extension CrewComplement: IntRepresentable {
     var intValue: Int {
      return rawValue
     }
    }
  6. 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/compactmap(_:).

Further information about the ternary operator can be found at https://docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators/#Ternary-Conditional-Operator.

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.10 – Xcode – a new project

Figure 2.10 – Xcode – a 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.11 – Xcode – a new project structure

Figure 2.11 – Xcode – a 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.12 – A new project

Figure 2.12 – A new project

  1. From the template selector, select Framework:
Figure 2.13 – A new project framework

Figure 2.13 – A new project framework

  1. Name the project AppleInc:
Figure 2.14 – Naming the project

Figure 2.14 – 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.15 – The new project workspace group

Figure 2.15 – The 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.16 – A new playground

Figure 2.16 – A new playground

  1. Give the playground a name, and add it to your workspace:
    Figure 2.17 – A new project

Figure 2.17 – A new project

  1. Press the run button on the Xcode toolbar to build the AppleInc module:
Figure 2.18 – The Xcode toolbar

Figure 2.18 – The 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
     }
    }
  3. Now, 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.

    Create a new file called AppleStore. Then, create structures for AppleStore and Factory using the fileprivate access control:

    public enum DeviceModel {
     case iPhone13
     case iPhone13Mini
     case iPhone13Pro
     case iPhone13ProMax
    }
    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 define 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.

  4. Create a new file called Apple, and then 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"
     }
    }
  5. 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.

    Create a new file called SwiftLanguage, and then 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 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 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.

Note

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?

Now, 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, restricting the creation of iPhones to just the factories where they are built, and then restricting access to those factories to just the Apple Store:

public enum DeviceModel {
 case iPhone13
 case iPhone13Mini
 case iPhone13Pro
 case iPhone13ProMax
}
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 AppleiPhone is public, but only structures within this file can create AppleiPhone class objects in the first place. In this case, this is done by the factory.

Now, 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 to set a property than the one that was set to get 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 allow 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.9
 }
 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.

Now, 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 provided by the module, selliPhone, allows us to buy an iPhone from the Apple Store:

// Buy new iPhone
let boughtiPhone = apple.store.selliPhone(ofModel: .iPhone13ProMax)
// This works
// Try and create your own iPhone
let buildAniPhone = AppleiPhone(model: .iPhone6S)
// 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.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Harness the power of Swift to create stunning applications, spanning mobile apps to machine learning models
  • Learn and practice the flexible Swift programming language with a practical, recipe-based approach
  • Leverage the new features in the latest release of Swift and iOS
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

Unlock the full potential of Swift and elevate your iOS development skills with this new edition of Swift Cookbook, highlighting the latest features in Swift 5.9. This cookbook will take your Swift programming skills to the next level, boosting your productivity and efficiency step by step through a plethora of practical recipes. Although this book is primarily for experienced iOS developers, it provides an introductory overview of Swift 5.9, including its basic building blocks, syntax, and the functionalities of Swift constructs, to get you warmed up. Once you’ve mastered the fundamentals, you’ll get down to business. Unless you’re completely new to Swift, this recipe-based guide doesn’t need to be read in order; you can jump to whichever topic takes your fancy, from UIKit and SwiftUI to advanced UI techniques, from Swift’s control flow and generics to machine learning with Vision, CoreML, and augmented reality with ARKit. By the end of this book, you’ll be fully up to speed with Swift’s capabilities and be able to develop amazing applications across a wide variety of domains.

What you will learn

Define flexible classes and structs using generics Use advanced operators and create custom ones Build iOS apps using UIKit and SwiftUI framework Import custom functionality into Swift Playgrounds Implement machine learning models using CoreML and Vision Manage 3D models and assets for ARKit using Swift and Xcode

Product Details

Country selected

Publication date : Jun 7, 2024
Length 422 pages
Edition : 3rd Edition
Language : English
ISBN-13 : 9781803239583
Vendor :
Apple
Category :
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Buy Now

Product Details


Publication date : Jun 7, 2024
Length 422 pages
Edition : 3rd Edition
Language : English
ISBN-13 : 9781803239583
Vendor :
Apple
Category :
Languages :
Tools :

Table of Contents

15 Chapters
Preface Chevron down icon Chevron up icon
1. Chapter 1: Swift Fundamentals Chevron down icon Chevron up icon
2. Chapter 2: Mastering the Building Blocks Chevron down icon Chevron up icon
3. Chapter 3: Data Wrangling with Swift Chevron down icon Chevron up icon
4. Chapter 4: Generics, Operators, and Nested Types Chevron down icon Chevron up icon
5. Chapter 5: Beyond the Standard Library Chevron down icon Chevron up icon
6. Chapter 6: Understanding Concurrency in Swift Chevron down icon Chevron up icon
7. Chapter 7: Building iOS Apps with UIKit Chevron down icon Chevron up icon
8. Chapter 8: Building iOS Apps with SwiftUI Chevron down icon Chevron up icon
9. Chapter 9: Getting to Grips with Combine Chevron down icon Chevron up icon
10. Chapter 10: Using CoreML and Vision in Swift Chevron down icon Chevron up icon
11. Chapter 11: Immersive Swift with ARKit and Augmented Reality Chevron down icon Chevron up icon
12. Chapter 12: Visualizing Data with Swift Charts Chevron down icon Chevron up icon
13. Index Chevron down icon Chevron up icon
14. Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Empty star icon Empty star icon Empty star icon Empty star icon Empty star icon 0
(0 Ratings)
5 star 0%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
Top Reviews
No reviews found
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.