Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

7. Arrays, Dictionaries & Sets
Written by Eli Ganim

As discussed in the introduction to this section, collections are flexible “containers” that let you store any number of values together. Before discussing these collections, you need to understand the concept of mutable vs. immutable collections.

As part of exploring the differences between the collection types, you’ll also consider performance: how quickly the collections can perform certain operations, such as adding or searching through it.

The usual way to talk about performance is with big-O notation. If you’re unfamiliar with it, start reading the chapter for a brief introduction.

Big-O notation is a way to describe running time, or how long an operation takes to complete. The idea is that the exact time an operation takes isn’t as important; the relative difference in scale matters.

Imagine you have a list of names in some random order, and you must look up the first name on the list. It doesn’t matter whether the list has a single name or a million names — glancing at the first name always takes the same time. That’s an example of a constant time operation, or O(1) in big-O notation.

Now say you have to find a particular name on the list. You need to scan the list and look at every name until you either find a match or reach the end. Again, we’re not concerned with the exact amount of time this takes, just the relative time compared to other operations.

To figure out the running time, think in terms of units of work. You need to look at every name, so consider there to be one “unit” of work per name. If you had 100 names, that’s 100 units of work. What if you double the number of names to 200? How does that change the amount of work?

The answer is it also doubles the amount of work. Similarly, if you quadruple the number of names, that quadruples the amount of work.

This increase in work is an example of a linear time operation, or O(N) in big-O notation. The input size is the variable N, which means the amount of time the process takes is also N. There’s a direct, linear relationship between the input size (the number of names in the list) and the time it will take to search for one name.

You can see why constant time operations use the number one in O(1). They’re just a single unit of work, no matter what!

You can read more about big-O notation by searching the Web. You’ll only need constant and linear time in this book, but there are other such time complexities out there.

Big-O notation is essential when dealing with collection types because collections can store vast amounts of data. You need to be aware of running times when you add, delete or edit values.

For example, if collection type A has constant-time searching and collection type B has linear-time searching, which you choose to use will depend on how much searching you’re planning to do.

Mutable Versus Immutable Collections

Like the previous types you’ve read about, such as Int or String, when you create a collection, you must declare it as either a constant or a variable.

If the collection doesn’t need to change after you’ve created it, you should make it immutable by declaring it as a constant with let. Alternatively, if you need to add, remove or update values in the collection, you should create a mutable collection by declaring it as a variable with var.

Arrays

Arrays are the most common collection type you’ll run into in Swift. Arrays are typed, just like regular variables and constants, and store multiple values like a simple list.

Before you create your first array, consider in detail what an array is and why you might want to use one.

What Is an Array?

An array is an ordered collection of values of the same type. The elements in the array are zero-indexed, which means the index of the first element is 0, the index of the second element is 1, and so on. Knowing this, you can determine that the last element’s index is the number of values in the array minus one.

Anna Bob Anna Anna Cindy 0 1 2 3 4

There are five elements in this array, at indices 0–4.

All values are of type String, so you can’t add non-string types to an array that holds strings. Notice that the same value can appear multiple times.

When Are Arrays Useful?

Arrays are useful when storing your items in a particular order. You may want the elements sorted, or you may need to fetch elements by index without iterating through the entire array.

For example, if you were storing high-score data, then the order would matter. You would want the highest score to come first in the list (i.e., at index 0) with the next-highest score after that, and so on.

Creating Arrays

The easiest way to create an array is using an array literal. This approach concisely provides array values as a list of values separated by commas and surrounded by square brackets.

let evenNumbers = [2, 4, 6, 8]

Since the array literal only contains integers, Swift infers the type of evenNumbers to be an array of Int values. This type is written as [Int]. The type inside the square brackets defines the type of values the array can store, which the compiler will enforce when adding elements to the array. If you try to add a string, for example, the compiler will return a type error, and your code won’t compile.

You can create an empty using the empty array literal []. Because the compiler isn’t able to infer a type from this, you need to use a type annotation to make the type explicit:

var subscribers: [String] = []

It’s also possible to create an array with all of its values set to a default value:

let allZeros = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]

It’s good practice to declare arrays that aren’t going to change as constants. For example, consider this array:

let vowels = ["A", "E", "I", "O", "U"]

vowels is an array of strings whose values can’t be changed. But that’s fine since the list of vowels doesn’t tend to change very often!

Accessing Elements

Being able to create arrays is useless unless you know how to fetch values from an array. In this section, you’ll learn several ways to access an array’s elements.

Using Properties and Methods

Imagine creating a game of cards, and you want to store the players’ names in an array. The list will need to change as players join or leave the game, so you need to declare a mutable array:

var players = ["Alice", "Bob", "Cindy", "Dan"]

In this example, players is a mutable array because you assigned it to a variable.

Before the game starts, you need to make sure there are enough players. You can use the isEmpty property to check if there’s at least one player:

print(players.isEmpty)
// > false

Note: You’ll learn about properties in Chapter 12, “Properties”. For now, think of them as variables that are built into values. To access a property, place a dot after the name of the constant or variable that holds the value and follow it with the name of the property you want to access.

The array isn’t empty, but you need at least two players to start a game. You can get the number of players using the count property:

if players.count < 2 {
  print("We need at least two players!")
} else {
  print("Let’s start!")
}
// > Let’s start!

It’s time to start the game! You decide that the order of play is by the order of names in the array. How would you get the first player’s name?

Arrays provide the first property to fetch the first object of an array:

var currentPlayer = players.first

Printing the value of currentPlayer reveals something interesting:

print(currentPlayer as Any)
// > Optional("Alice")

The property first returns an optional because if the array were empty, the first element would be missing and thus nil. The print() method realizes currentPlayer is optional and generates a warning. To suppress the warning, add as Any to the type to be printed.

Similarly, arrays have a last property that returns the last value in an array or nil if the array is empty:

print(players.last as Any)
// > Optional("Dan")

Another way to get values from an array is by calling min(). This method returns the element with the lowest value in the array — not the lowest index!

If the array contained strings, then it would return the string that’s the lowest in alphabetical order, which in this case is "Alice":

currentPlayer = players.min()
print(currentPlayer as Any)
// > Optional("Alice")

Note: You’ll learn about methods in Chapter 13, “Methods”. For now, think of them as functions that are built into values. To call a method, place a dot after the name of the constant or variable that holds the value and follow it with the name of the method you want to call. Like functions, don’t forget to include the parameter list in parenthesis, even if it’s empty, to call the method.

Obviously, first and min() will not always return the same value. For example:

print([2, 3, 1].first as Any)
// > Optional(2)
print([2, 3, 1].min() as Any)
// > Optional(1)

As you might have guessed, arrays also have a max() method.

Note: The first and last properties and the min() and max() methods aren’t unique to arrays. Every collection type has these properties and methods, in addition to many others. You’ll learn more about this behavior when you read about protocols in Chapter 17, “Protocols”.

Now that you know how to get the first player, you’ll announce who that player is:

if let currentPlayer {
  print("\(currentPlayer) will start")
}
// > Alice will start

You use if let to unwrap the optional you got back from min(); otherwise, the statement would print Optional("Alice") will start, which is not what you want.

These properties and methods are helpful if you want to get the first, last, minimum or maximum elements. But what if the element you want can’t be obtained with one of these properties or methods?

Using Subscripting

The most convenient way to access elements in an array is by using the subscript syntax. This syntax lets you access any value directly by using its index inside square brackets:

var firstPlayer = players[0]
print("First player is \(firstPlayer)")
// > First player is "Alice"

Because arrays are zero-indexed, you use index 0 to fetch the first object. You can use a greater index to get the next elements in the array, but if you try to access an index beyond the array’s size, you’ll get a runtime error.

var player = players[4]
// > fatal error: Index out of range

You receive this error because players contains only four strings. Index 4 represents the fifth element, but there is no fifth element in this array.

When you use subscripts, you don’t have to worry about optionals since trying to access a non-existing index doesn’t return nil; it simply causes a runtime error.

Using Countable Ranges to Make an ArraySlice

You can use the subscript syntax with countable ranges to fetch more than a single value from an array. For example, if you’d like to get the next two players, you could do this:

let upcomingPlayersSlice = players[1...2]
print(upcomingPlayersSlice[1], upcomingPlayersSlice[2])
// > "Bob Cindy\n"

The constant upcomingPlayersSlice is an ArraySlice of the original array. The reason for this type difference is to make clear that upcomingPlayersSlice shares storage with players.

The range you used is 1...2, representing the second and third items in the array. You can use an index here if the start value is smaller than or equal to the end value and within the array’s bounds.

It is also easy to make a brand-new, zero-indexed Array from an ArraySlice like so:

let upcomingPlayersArray = Array(players[1...2])
print(upcomingPlayersArray[0], upcomingPlayersArray[1])
// > "Bob Cindy\n"

Checking for an Element

You can check if there’s at least one occurrence of a specific element in an array by using contains(_:), which returns true if it finds the element in the array, and false otherwise.

You can use this strategy to write a function that checks if a given player is in the game:

func isEliminated(player: String) -> Bool {
  !players.contains(player)
}

Now you can use this function any time you need to check if a player has been eliminated:

print(isEliminated(player: "Bob"))
// > false

You could even test for the existence of an element in a specific range using an ArraySlice:

players[1...3].contains("Bob") // true

Now that you can get data out of your arrays, it’s time to look at mutable arrays and how to change their values.

Modifying Arrays

You can make changes to mutable arrays, such as adding and removing elements, updating existing values, and moving elements around into a different order. In this section, you’ll see how to work with the array to match up what’s going on with your game.

Appending Elements

If new players want to join the game, they must sign up and add their names to the array. Eli is the first player to join the existing four players. You can add Eli to the end of the array using the append(_:) method:

players.append("Eli")

If you try to append anything other than a string, the compiler will show an error. Remember, arrays can only store values of the same type. Also, append(_:) only works with mutable arrays.

The next player to join the game is Gina. You can append her to the game another way by using the += operator:

players += ["Gina"]

The right-hand side of this expression is an array with a single element: the string "Gina". By using +=, you’re appending the elements of that array to players.

Now the array looks like this:

print(players)
// > ["Alice", "Bob", "Cindy", "Dan", "Eli", "Gina"]

Here, you added a single element to the array, but you can see how easy it would be to append multiple items using the += operator by adding more names after Gina’s.

Inserting Elements

An unwritten rule of this card game is that the players’ names have to be in alphabetical order. This list is missing a player that starts with the letter F. Luckily, Frank has just arrived. You want to add him to the list between Eli and Gina. To do that, you can use the insert(_:at:) method:

players.insert("Frank", at: 5)

The at argument defines where you want to add the element. Remember that the array is zero-indexed, so index 5 is Gina’s index, causing her to move up as Frank takes her place.

Removing Elements

During the game, the other players caught Cindy and Gina cheating. They should be removed from the game! You know that Gina is last in players, so you can remove her easily with the removeLast() method:

var removedPlayer = players.removeLast()
print("\(removedPlayer) was removed")
// > Gina was removed

This method does two things: It removes the last element and then returns it, in case you need to print it or store it somewhere else — like in an array of cheaters!

To remove Cindy from the game, you need to know the exact index where her name is stored. Looking at the list of players, you see that she’s third, so her index is 2.

removedPlayer = players.remove(at: 2)
print("\(removedPlayer) was removed")
// > Cindy was removed

But how would you get the index of an element if you didn’t already know it? There’s a method for that! firstIndex(of:) returns the first index of the element because the array might contain multiple copies of the same value. If the method doesn’t find the element, it returns nil.

Mini-Exercise

Use firstIndex(of:) to determine the position of the element "Dan" in players.

Updating Elements

Frank has decided everyone should call him Franklin from now on. You could remove the value "Frank" from the array and then add "Franklin", but that’s too much work for a simple task. Instead, you should use the subscript syntax to update the name.

print(players)
// > ["Alice", "Bob", "Dan", "Eli", "Frank"]
players[4] = "Franklin"
print(players)
// > ["Alice", "Bob", "Dan", "Eli", "Franklin"]

Be careful not to use an index beyond the array’s bounds, or your program will halt.

As the game continues, some players are eliminated, and new ones come to replace them. You can also use subscripting with ranges to update multiple values in a single line of code:

players[0...1] = ["Donna", "Craig", "Brian", "Anna"]
print(players)
// > ["Donna", "Craig", "Brian", "Anna", "Dan", "Eli", "Franklin"]

This code replaces the first two players, Alice and Bob, with the four players in the new player’s array. As you can see, the size of the range doesn’t have to be equal to the size of the array that holds the values you’re adding.

Moving Elements

Take a look at this mess! The players array contains names that start with A to F, but they aren’t in the correct order, which violates the rules of the game.

You can try to fix this situation by moving values one by one to their correct positions:

let playerAnna = players.remove(at: 3)
players.insert(playerAnna, at: 0)
print(players)
// > ["Anna", "Donna", "Craig", "Brian", "Dan", "Eli", "Franklin"]

…or by swapping elements, by using swapAt(_:_:):

players.swapAt(1, 3)
print(players)
// > ["Anna", "Brian", "Craig", "Donna", "Dan", "Eli", "Franklin"]

This works for a few elements, but to sort the entire array, you should use sort():

players.sort()
print(players)
// > ["Anna", "Brian", "Craig", "Dan", "Donna", "Eli", "Franklin"]

If you’d like to leave the original array untouched and return a sorted copy instead, use sorted() instead of sort().

Iterating Through an Array

It’s getting late, so the players decide to stop for the night and continue tomorrow. In the meantime, you’ll keep their scores in a separate array. You’ll investigate a better approach for this when you learn about dictionaries, but for now, you can continue to use arrays:

let scores = [2, 2, 8, 6, 1, 2, 1]

Before the players leave, you want to print the names of those still in the game. You can do this using the for-in loop you read about in Chapter 4, “Advanced Control Flow”:

for player in players {
  print(player)
}
// > Anna
// > Brian
// > Craig
// > Dan
// > Donna
// > Eli
// > Franklin

This code iterates over all the elements of players, from index 0 to players.count - 1 and prints their values. In the first iteration, player is equal to the first element of the array; in the second iteration, it’s equal to the second element of the array; and so on, until the loop has printed all the elements in the array.

If you need the index of each element, you can iterate over the return value of the array’s enumerated() method, which returns tuples with each element’s index and value:

for (index, player) in players.enumerated() {
  print("\(index + 1). \(player)")
}
// > 1. Anna
// > 2. Brian
// > 3. Craig
// > 4. Dan
// > 5. Donna
// > 6. Eli
// > 7. Franklin

Now you can use the technique you’ve just learned to write a function that takes an array of integers as its input and returns the sum of its elements:

func sumOfElements(in array: [Int]) -> Int {
  var sum = 0
  for number in array {
    sum += number
  }
  return sum
}

You could use this function to calculate the sum of the players’ scores:

print(sumOfElements(in: scores))
// > 22

Mini-Exercise

Write a for-in loop that prints the players’ names and scores.

Running Time for Array Operations

Arrays are stored as a contiguous block in memory. That means if you have ten elements in an array, the ten values are all stored one next to the other. With that in mind, here’s the performance cost of various array operations:

Accessing elements: The cost of fetching an element is cheap, meaning it happens in a fixed or constant time. Sometimes this is written O(1). Since all the values are sequential, it’s easy to use random access and fetch a value at a particular index; all the compiler needs to know is where the array starts and what index you want to fetch.

Inserting elements: The complexity of adding an element depends on the position in which you add the new element:

  • If you add to the beginning of the array, Swift requires time proportional to the size of the array because it has to shift all elements over by one to make room. This complexity is called linear time and is sometimes written O(n).

  • Likewise, if you add to the middle of the array, all values from that index on need to be shifted over. Doing so will require n/2 operations; therefore, the running time is still linear with the size of the array or O(n).

  • If you add to the end of the array using append and there’s room, it will take O(1). If there isn’t room, Swift will need to make space somewhere else and copy the entire array before adding the new element, which will take O(n). The average case is O(1) because arrays are not full most of the time.

Deleting elements: Deleting an element leaves a gap where the removed element was. All elements in the array must be sequential, so this gap needs to be closed by shifting elements forward.

The complexity is similar to inserting elements: If you remove an element from the end, it’s an O(1) operation. Otherwise, the complexity is O(n).

Searching for an element: If the element you’re searching for is the first element in the array, the search will end after a single operation. If the element doesn’t exist, you need to perform N operations until you realize that the element is not found. On average, searching for an element will take n/2 operations; therefore, searching has a complexity of O(n).

As you learn about dictionaries and sets, you’ll see how their performance characteristics differ from arrays. That could hint at which collection type to use for your particular case.

Dictionaries

A dictionary is an unordered collection of pairs, where each pair comprises a key and a value.

As shown in the diagram below, keys are unique. The same key can’t appear twice in a dictionary, but different keys may point to the same value. All keys must be of the same type, and all values must be of the same type.

Craig Brian Donna 8 2 2 6 Keys Values Anna

Dictionaries are useful when you want to look up values through an identifier. For example, the table of contents of this book maps chapter names to their page numbers, making it easy to skip to the chapter you want to read.

How is this different from an array? With an array, you can only fetch a value by its index, which has to be an integer, and all indexes have to be sequential. In a dictionary, the keys can be of any type and in no particular order.

Creating Dictionaries

The easiest way to create a dictionary is by using a dictionary literal. This is a list of key-value pairs separated by commas, enclosed in square brackets.

For your card game from earlier, instead of using the two arrays to map players to their scores, you can use a dictionary literal:

var namesAndScores = ["Anna": 2, "Brian": 2, "Craig": 8, "Donna": 6]
print(namesAndScores)
// > ["Craig": 8, "Anna": 2, "Donna": 6, "Brian": 2]

The compiler infers the dictionary type in this example as [String: Int]. This means namesAndScores is a dictionary with strings as keys and integers as values.

When you print the dictionary, you see no particular order to the pairs. Remember that, unlike arrays, dictionaries are unordered! The empty dictionary literal looks like this: [:]. You can use that to empty an existing dictionary like so:

namesAndScores = [:]

…or create a new dictionary, like so:

var pairs: [String: Int] = [:]

The type annotation is required here, as the compiler can’t infer the type of the dictionary from the empty dictionary literal.

After you create a dictionary, you can define its capacity:

pairs.reserveCapacity(20)

Calling pairs.reserveCapacity(_:) is an easy way to improve performance when you know how much data the dictionary needs to store. You can add items to the dictionary, and no expensive memory reallocations will occur as long the count remains below the dictionary’s capacity.

Accessing Values

As with arrays, there are several ways to access dictionary values.

Using Subscripting

Dictionaries support subscripting to access values. Unlike arrays, you don’t access a value by its index but rather by its key. For example, if you want to get Anna’s score, you would type:

namesAndScores = ["Anna": 2, "Brian": 2, "Craig": 8, "Donna": 6]
// Restore the values

print(namesAndScores["Anna"]!) // 2

Notice that the return type is an optional. The dictionary will check if there’s a pair with the key Anna, and if there is, return its value. If the dictionary doesn’t find the key, it will return nil.

namesAndScores["Greg"] // nil

With arrays, out-of-bounds subscript access causes a runtime error, but dictionaries are different since their results are wrapped in an optional. Subscript access using optionals is powerful. You can find out if a specific player is in the game without iterating over all the keys, as you must do when using an array.

Using Properties and Methods

Dictionaries, like arrays, conform to Swift’s Collection protocol. Because of that, they share many of the same properties. For example, both arrays and dictionaries have isEmpty and count properties:

namesAndScores.isEmpty  //  false
namesAndScores.count    //  4

Note: If you want to know whether a collection has elements, it is always better to use the isEmpty property than to compare count to zero. Although arrays and dictionaries compute count in constant time, not every collection is guaranteed to do so. For example, count on a String needs to loop through all of its characters. isEmpty, by contrast, always runs in constant time, no matter how many values there are for every collection type.

Modifying Dictionaries

It’s easy enough to create dictionaries and access their contents — but what about modifying them?

Adding Pairs

Bob wants to join the game.

Take a look at his details before you let him join:

var bobData = [
  "name": "Bob",
  "profession": "Card Player",
  "country": "USA"
]

This dictionary is of type [String: String], and it’s mutable because it’s assigned to a variable. Imagine you received more information about Bob, and you wanted to add it to the dictionary. This is how you’d do it:

bobData.updateValue("CA", forKey: "state")

There’s even a shorter way to add pairs, using subscripting:

bobData["city"] = "San Francisco"

Bob’s a professional card player. So far, he sounds like an excellent addition to your roster.

Mini-Exercise

Write a function that prints a given player’s city and state.

Updating Values

It appears that in the past, Bob was caught cheating when playing cards. He’s not just a professional — he’s a card shark! He asks you to change his name and profession so no one will recognize him.

Because Bob seems eager to change his ways, you agree. First, you change his name from Bob to Bobby:

bobData.updateValue("Bobby", forKey: "name") // Bob

You saw this method above when you read about adding pairs. Why does it return the string Bob? updateValue(_:forKey:) replaces the value of the given key with the new value and returns the old value. If the key doesn’t exist, this method will add a new pair and return nil.

As with adding, you can do this with less code by using subscripting:

bobData["profession"] = "Mailman"

Like updateValue(_:forKey:), this code updates the value for this key or, if the key doesn’t exist, creates a new pair.

Removing Pairs

Bob — er, sorry — Bobby, still doesn’t feel safe, and he wants you to remove all information about his whereabouts:

bobData.removeValue(forKey: "state")

This method will remove the key state and its associated value from the dictionary. As you might expect, there’s a shorter way to do this using subscripting:

bobData["city"] = nil

Assigning nil as a key’s associated value removes the pair from the dictionary.

Note: If you’re using a dictionary that has values that are optional types, dictionary[key] = nil still removes the key completely. If you want to keep the key and set the value to nil, you must use the updateValue method.

Iterating Through Dictionaries

The for-in loop also works when you want to iterate over a dictionary. But since the items in a dictionary are pairs, you can use a tuple:

for (player, score) in namesAndScores {
  print("\(player) - \(score)")
}
// > Craig - 8
// > Anna - 2
// > Donna - 6
// > Brian - 2

It’s also possible to iterate over just the keys:

for player in namesAndScores.keys {
  print("\(player), ", terminator: "") // no newline
}
print("") // print one final newline
// > Craig, Anna, Donna, Brian,

You can iterate over just the values in the same manner with the values property of the dictionary.

Running Time for Dictionary Operations

To examine how dictionaries work, you need to understand what hashing is and how it works. Hashing is the process of transforming a value — String, Int, Double, Bool, etc. — to a numeric value, known as the hash value. This value can then be used to quickly look up the values in a hash table.

Swift dictionaries have a type requirement for keys. Keys must be Hashable, or you will get a compiler error.

Fortunately, in Swift, all basic types are already Hashable and have a hash value. This value must be deterministic — meaning that a given value must always return the same hash value. No matter how many times you calculate the hash value for some string, it will always give the same value. However, you should never save a hash value because it will be different each time you run your program.

Here’s the performance of various dictionary operations. This extraordinary performance hinges on having a good hashing function that avoids value collisions.

All operations below degenerate to linear time O(n) performance if you have a poor hashing function. Fortunately, the built-in types have great, general-purpose Hashable implementations.

Accessing elements: Getting the value for a key is a constant time operation, or O(1).

Inserting elements: To insert an element, the dictionary needs to calculate the key’s hash value and then store data based on that hash. These are all O(1) operations.

Deleting elements: Again, the dictionary needs to calculate the hash value to know exactly where to find and remove the element. This is also an O(1) operation.

Searching for an element: As mentioned above, accessing an element has constant running time, so the complexity for searching is also O(1).

While these running times compare favorably to arrays, remember that you lose order information when using dictionaries.

Sets

A set is an unordered collection of unique values of the same type. This can be extremely useful when you want to ensure that an item doesn’t appear more than once in your collection and when the order of your items isn’t important.

“Bob” “Cindy” “Dan” “Alice”

There are four strings in the Set illustration above. Notice that there’s no order for the elements.

Creating Sets

You can declare a set explicitly by writing Set followed by the type inside angle brackets:

let setOne: Set<Int> = [1]

Set Literals

Sets don’t have their own literals. You use array literals to create a set with initial values. Consider this example:

let someArray = [1, 2, 3, 1]

This is an array. How do you use array literals to create a set? Like this:

var explicitSet: Set<Int> = [1, 2, 3, 1]

You have to explicitly declare the variable as a Set. However, you can let the compiler infer the element type like so:

var someSet = Set([1, 2, 3, 1])

To see the most important features of a set in action, print the set you just created:

print(someSet)
// > [2, 3, 1] but the order is not defined

First, you can see there’s no specific ordering. Second, although you created the set with two instances of the value 1, that value only appears once. Remember, a set’s values must be unique.

Accessing Elements

You can use contains(_:) to check for the existence of a specific element:

print(someSet.contains(1))
// > true
print(someSet.contains(4))
// > false

You can also use the first and last properties, which return one of the elements in the set. However, you won’t know which item you’ll get because sets are unordered.

Adding and Removing Elements

You can use insert(_:) to add elements to a set. If the element already exists, the method does nothing.

someSet.insert(5)

You can remove the element from the set like this:

let removedElement = someSet.remove(1)
print(removedElement!)
// > 1

remove(_:) returns the removed element if it’s in the set or nil otherwise.

Running Time for Set Operations

Sets have a very similar implementation to dictionaries, and they also require the elements to be hashable. The running time of all the operations is identical to those of dictionaries.

Challenges

Before moving on, here are some challenges to test your knowledge of arrays, dictionaries and sets. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Which Is Valid

Which of the following are valid statements?

1. let array1 = [Int]()
2. let array2 = []
3. let array3: [String] = []

For the next five statements, array4 has been declared as:

let array4 = [1, 2, 3]
4. print(array4[0])
5. print(array4[5])
6. array4[1...2]
7. array4[0] = 4
8. array4.append(4)

For the final five statements, array5 has been declared as:

var array5 = [1, 2, 3]
9. array5[0] = array5[1]
10. array5[0...1] = [4, 5]
11. array5[0] = "Six"
12. array5 += 6
13. for item in array5 { print(item) }

Challenge 2: Remove the First Number

Write a function that removes the first occurrence of a given integer from an array of integers. This is the signature of the function:

func removingOnce(_ item: Int, from array: [Int]) -> [Int]

Challenge 3: Remove the Numbers

Write a function that removes all occurrences of a given integer from an array of integers. This is the signature of the function:

func removing(_ item: Int, from array: [Int]) -> [Int]

Challenge 4: Reverse an Array

Arrays have a reversed() method that returns an array holding the same elements as the original array in reverse order. Write a function that does the same thing without using reversed(). This is the signature of the function:

func reversed(_ array: [Int]) -> [Int]

Challenge 5: Return the Middle

Write a function that returns the middle element of an array. When the array size is even, return the first of the two middle elements.

func middle(_ array: [Int]) -> Int?

Challenge 6: Find the Minimum and Maximum

Write a function that calculates the minimum and maximum values in an array of integers. Calculate these values yourself; don’t use the methods min and max. Return nil if the given array is empty.

This is the signature of the function:

func minMax(of numbers: [Int]) -> (min: Int, max: Int)?

Challenge 7: Which Is Valid

Which of the following are valid statements?

1. let dict1: [Int, Int] = [:]
2. let dict2 = [:]
3. let dict3: [Int: Int] = [:]

For the next four statements, use the following dictionary:

let dict4 = ["One": 1, "Two": 2, "Three": 3]
4. dict4[1]
5. dict4["One"]
6. dict4["Zero"] = 0
7. dict4[0] = "Zero"

For the next three statements, use the following dictionary:

var dict5 = ["NY": "New York", "CA": "California"]
8. dict5["NY"]
9. dict5["WA"] = "Washington"
10. dict5["CA"] = nil

Challenge 8: Long Names

Given a dictionary with two-letter state codes as keys, and the full state names as values, write a function that prints all the states with names longer than eight characters. For example, for the dictionary ["NY": "New York", "CA": "California"], the output would be California.

Challenge 9: Merge Dictionaries

Write a function that combines two dictionaries into one. If a certain key appears in both dictionaries, ignore the pair from the first dictionary. This is the function’s signature:

func merging(_ dict1: [String: String], with dict2: [String: String]) -> [String: String]

Challenge 10: Count the Characters

Declare a function occurrencesOfCharacters that calculates which characters occur in a string, as well as how often each of these characters occur. Return the result as a dictionary. This is the function signature:

func occurrencesOfCharacters(in text: String) -> [Character: Int]

Hint: String is a collection of characters that you can iterate over with a for statement. Bonus: To make your code shorter, dictionaries have a special subscript operator that lets you add a default value if it is not found in the dictionary. For example, dictionary["a", default: 0] creates a 0 entry for the character “a” if it is not found instead of just returning nil.

Challenge 11: Unique Values

Write a function that returns true if all of the values of a dictionary are unique. Use a set to test uniqueness. This is the function signature:

func isInvertible(_ dictionary: [String: Int]) -> Bool

Challenge 12: Removing Keys and Setting Values to nil

Given the dictionary:

var nameTitleLookup: [String: String?] = ["Mary": "Engineer", "Patrick": "Intern", "Ray": "Hacker"]

Set the value of the key "Patrick" to nil and completely remove the key and value for "Ray".

Key Points

Sets:

  • Are unordered collections of unique values of the same type.
  • Are most useful when you need to know whether something is included in the collection or not.

Dictionaries:

  • Are unordered collections of key-value pairs.
  • The keys are all of the same type, and the values are all of the same type.
  • Use subscripting to get values and to add, update or remove pairs.
  • If a key is not in a dictionary, lookup returns nil.
  • The key of a dictionary must be a type that conforms to the Hashable protocol.
  • Basic Swift types such as String, Int, Double are Hashable out of the box.

Arrays:

  • Are ordered collections of values of the same type.
  • Use subscripting, or one of the many properties and methods, to access and update elements.
  • Be wary of accessing an index that’s out of bounds – doing so halts your program.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.