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
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:
- Create an array called
gamesToPlay
. This will hold our strings:var gamesToPlay = [String]()
- 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")
- 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"
- Print a count of the number of movies in the list so far:
print(gamesToPlay.count) // 3
- 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)
- 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"
- 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"
- 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"
- 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"]
- 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"
- 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
- 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"
- 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