Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Swift Cookbook

You're reading from   Swift Cookbook Proven recipes for developing robust iOS applications with Swift 5.9

Arrow left icon
Product type Paperback
Published in Jun 2024
Publisher Packt
ISBN-13 9781803239583
Length 422 pages
Edition 3rd Edition
Languages
Tools
Arrow right icon
Authors (4):
Arrow left icon
Chris Barker Chris Barker
Author Profile Icon Chris Barker
Chris Barker
Daniel Bolella Daniel Bolella
Author Profile Icon Daniel Bolella
Daniel Bolella
Nathan Lawlor Nathan Lawlor
Author Profile Icon Nathan Lawlor
Nathan Lawlor
Keith Moon Keith Moon
Author Profile Icon Keith Moon
Keith Moon
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

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

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.

You have been reading a chapter from
Swift Cookbook - Third Edition
Published in: Jun 2024
Publisher: Packt
ISBN-13: 9781803239583
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime