The last collection type we will look at is the dictionary. This is a familiar construct in programming languages, where it is sometimes referred to as a hash table. A dictionary holds a collection of pairings between a key and a value. The key can be any element that conforms to the Hashable protocol (just like elements in a set), while the value can be any type. The contents of a dictionary is not stored in order, unlike an array; instead, the key is used both when storing a value and as a lookup when retrieving a value.
Getting ready
In this recipe, we will use a dictionary to store details of people at a place of work. We need to store and retrieve a person's information based on their role in the organization, such as a company directory. To hold this person's information, we will use a modified version of our Person class from Chapter 1, Swift Building Blocks.
Enter the following code into a new playground:
struct PersonName {
let givenName: String
let familyName: String
}
enum CommunicationMethod {
case phone
case email
case textMessage
case fax
case telepathy
case subSpaceRelay
case tachyons
}
class Person {
let name: PersonName
let preferredCommunicationMethod: CommunicationMethod
convenience init(givenName: String,
familyName: String,
commsMethod: CommunicationMethod) {
let name = PersonName(givenName: givenName, familyName:
familyName)
self.init(name: name, commsMethod: commsMethod)
}
init(name: PersonName, commsMethod: CommunicationMethod) {
self.name = name
preferredCommunicationMethod = commsMethod
}
var displayName: String {
return "\(name.givenName) \(name.familyName)"
}
}
How to do it...
Let's use the Person object we defined previously to build up our workplace directory using a dictionary:
- Create a Dictionary for the employee directory:
var crew = Dictionary<String, Person>()
- 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)
- 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)
- 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
- Replace a value in the dictionary by assigning a new value against an existing key. The previous value for the key is discarded when a new value is set:
print(crew["Security Officer"]!.name.givenName) // Tasha
crew["Security Officer"] = Person(givenName: "Worf",
familyName: "Son of Mogh",
commsMethod: .subSpaceRelay)
print(crew["Security Officer"]!.name.givenName) // Worf
With that, we have learned how to create, populate, and look up values in a dictionary.
How it works...
As with the other collection types, when we create a dictionary, we need to provide the types that the dictionary will be holding. For dictionaries, there are two types that we need to define. The first is the type of the key (which must conform to Hashable), while the second is the type of the value being stored against the key. For our dictionary, we are using String for the key and Person for the values being stored:
var crew = Dictionary<String, Person>()
As with an array, we can specify a dictionary type using square brackets and create one using a dictionary literal, where : separates the key and the value:
let intByName: [String: Int] = ["one": 1, "two": 2, "three": 3]
Therefore, we can change our dictionary definition so that it looks like this:
var crew: [String: Person] = [:]
The [:] symbol denotes an empty dictionary as a dictionary literal.
Elements are added to a dictionary using a subscript. Unlike an array, which takes an Int index in the subscript, a dictionary takes the key and then pairs the given value with the given key. In the following example, we are assigning a Person object to the "Captain" key:
crew["Captain"] = Person(givenName: "Jean-Luc",
familyName: "Picard",
commsMethod: .phone)
If no value currently exists, the assigned value will be added. If a value already exists for the given key, the old value will be replaced with the new value and the old value will be discarded.
There are properties on the dictionary that provide all the keys and values. These properties are of a custom collection type that can be passed to an array initializer to create an array:
let roles = Array(crew.keys)
print(roles)
To display all the dictionary's keys, as provided by the keys property, we can either create an array or iterate over the collection directly. We will cover iterating over a collection's values in the next chapter, so for now, we will create an array.
Next, we will use one of the values from an array of keys, alongside the crew, to retrieve full details about the associated Person:
let firstRole = roles.first! // Chief Medical Officer
let cmo = crew[firstRole]! // Person: Beverly Crusher
print("\(firstRole): \(cmo.displayName)")
// Chief Medical Officer: Beverly Crusher
We get the first element using the first property, but since this is an optional type, we need to force unwrap it using !. We can pass firstRole, which is now a non-optional String to the dictionary subscript, to get the Person object associated with that key. The return type for retrieving the value via subscript is also optional, so it also needs to be force unwrapped before we print its values.
There's more...
In this recipe, we used strings as the keys for our dictionary. However, we can also use a type that conforms to the Hashable protocol.
One downside of using String as a key for our employee directory is that it is very easy to mistype an employee's role or look for a role that you expect to exist but doesn't. So, we can improve our implementation by using something that conforms to Hashable and is better suited to being used as a key in our model.
We have a finite set of employee roles in our model, and an enumeration is perfect for representing a finite number of options, so let's define our roles as an enum:
enum Role: String {
case captain = "Captain"
case firstOfficer = "First Officer"
case secondOfficer = "Second Officer"
case chiefEngineer = "Chief Engineer"
case councillor = "Councillor"
case securityOfficer = "Security Officer"
case chiefMedicalOfficer = "Chief Medical Officer"
}
Now, let's change our Dictionary definition so that it uses this new enum as a key, and then insert our employees using these enum values:
var crew = Dictionary<Role, Person>()
crew[.captain] = Person(givenName: "Jean-Luc",
familyName: "Picard",
commsMethod: .phone)
crew[.firstOfficer] = Person(givenName: "William",
familyName: "Riker",
commsMethod: .email)
crew[.chiefEngineer] = Person(givenName: "Geordi",
familyName: "LaForge",
commsMethod: .textMessage)
crew[.secondOfficer] = Person(givenName: "Data",
familyName: "Soong",
commsMethod: .fax)
crew[.councillor] = Person(givenName: "Deanna",
familyName: "Troi",
commsMethod: .telepathy)
crew[.securityOfficer] = Person(givenName: "Tasha",
familyName: "Yar",
commsMethod: .subSpaceRelay)
crew[.chiefMedicalOfficer] = Person(givenName: "Beverly",
familyName: "Crusher",
commsMethod: .tachyons)
You will also need to change all the other uses of crew so that they use the new enum-based key.
Let's take a look at how and why this works. We created Role as a String-based enum:
enum Role: String {
//...
}
Defining it in this way has two benefits:
- We intend to display these roles to the user, so we will need a string representation of the Role enum, regardless of how we defined it.
- Enums have a little bit of protocol and generics magic in them, which means that if an enum is backed by a type that implements the Hashable protocol (as String does), the enum also automatically implements the Hashable protocol. Therefore, defining Role as being String-based satisfies the dictionary requirement of a key being Hashable without us having to do any extra work.
With our crew dictionary now defined as having a Role-based key, all subscript operations have to use a value in the role enum:
crew[.captain] = Person(givenName: "Jean-Luc",
familyName: "Picard",
commsMethod: .phone)
let cmo = crew[.chiefMedicalOfficer]
The compiler enforces this, so it's no longer possible to use an incorrect role when interacting with our employee directory. This pattern of using Swift's constructs and type system to enforce the correct use of your code is something we should strive to do, as it can reduce bugs and prevent our code from being used in unexpected ways.
See also
Further information about dictionaries can be found in Apple's documentation on the Swift language at http://swiftbook.link/docs/collections.