moroz.dev

<< Back to index

Learning iOS and Swift. Day 4: A thing on dictionaries

Abstract

Today I explore dictionaries, the Swift-flavored unique key-value collections.

Today I am going to write about dictionaries. Dictionaries are collections of key-value pairs. In dictionaries, a given key can only have one corresponding value. In Elixir, there is another key-value collection called a “keyword list,” which is basically just a list of key-value tuples. In keyword lists, a given key can occur multiple times within the same collection, but all keys must be atoms.

Most programming languages seem to pride themselves in inventing new names for their key-value collections. In JavaScript, this data type is called object, in Ruby–Hash, in Elixir and Erlang–map. In Rust, it’s a portmanteau: HashMap. In PHP, it’s an “associative array.” In Swift, similarly to Python’s dict, it is called a “dictionary.”

Dictionary literals

In Swift, dictionary literals are delimited by square brackets ([ and ]) and key-value pairs are associated using colons (:). Any value implementing the Hashable protocol (trait) can be a key. Common Hashables include numbers and strings.

With integer keys:

let aDictionary = [
    1: "a",
    2: "b"
]

print(aDictionary)
// [2: "b", 1: "a"]

Note that the key-value pairs are printed out in a different order. Unlike Ruby and JavaScript, which preserve the order in which their key-value pairs were declared, and unlike Elixir, whose maps are always sorted, Swift does not guarantee any deterministic order of keys in dictionaries. Most of the time, this does not matter, because dictionaries are usually accessed by their keys.

With Double keys:

let doubleKeys = [
    21.37: "Pope John Paul II",
    420: "Blaze it"
]

print(doubleKeys)
// [21.37: "Pope John Paul II", 420.0: "Blaze it"]

Not that even though the number 420 has no decimal part, it is still inferred as a Double, 420.0.

With String keys, remember to wrap all keys in double quotes:

let stringKeys = [
    "a": 1,
    "b": 2
]

print(stringKeys)
// ["a": 1, "b": 2]

If you omit the double quotes, the keys will be treated as expressions:

let someKey = "I am a string"
let otherKey = "I am another string"

let exprKeys = [
    someKey: 123,
    otherKey: 456
]

print(exprKeys)
// ["I am a string": 123, "I am another string": 456]

Mixing key types

If you mix and match key types, their value cannot be automatically inferred:

let mixedKeys = [
    1: "a",
    "b": 2
]
$ swift dict/mixed_keys.swift
dict/mixed_keys.swift:1:17: error: heterogeneous collection literal could only be
inferred to '[AnyHashable : Any]'; add explicit type annotation if this is intentional
let mixedKeys = [
                ^

The solution to this problem is included in the compiler error: you need to annotate the mixed-key dictionary with the type [AnyHashable: Any]:

let mixedKeys: [AnyHashable: Any] = [
    "a": 1,
    2: "b"
]

print(mixedKeys)
// [AnyHashable(2): "b", AnyHashable("a"): 1]

Empty dictionary literal

An empty dictionary literal is denoted as [:]. You need to explicitly annotate its type.

var empty: [String: Any] = [:]
print(empty)
// [:]

You can append values to the empty dictionary by assigning to its indices:

empty["An integer value"] = 123
empty["A double value"] = 21.37
empty["A string value"] = "Lo and behold"

print(empty)
// ["A double value": 21.37, "An integer value": 123, "A string value": "Lo and behold"]

Enum keys

Simple enums (without associated) seem to be Hashable by default.

// define an enum for common currencies in my part of the world
enum Currency {
    case eur
    case pln
    case czk
}

let beerPriceIn2022: [Currency: Double] = [
    .czk: 35,
    .pln: 14,
    .eur: 5
]

To iterate on this dictionary, you can use a regular for-in clause:

for (currency, price) in beerPriceIn2022 {
    // convert enum case to String and convert it to upper case
    let printable = String(describing: currency).uppercased()
    print("Price of beer in 2022: \(price) \(printable)")
}
$ swift dict/enum_keys.swift
Price of beer in 2022: 45.0 CZK
Price of beer in 2022: 5.0 EUR
Price of beer in 2022: 14.0 PLN

Serializing dictionaries to JSON

Serializing maps into JSON is fairly easy. The Foundation framework includes the class JSONEncoder.

import Foundation

let myDict = [
    "PL": "PLN",
    "CZ": "CZK",
    "SK": "EUR"
]

let encoder = JSONEncoder()

// enable pretty printing
encoder.outputFormatting = .prettyPrinted

let jsonData = try encoder.encode(myDict)
if let json = String(data: jsonData, encoding: .utf8) {
    print(json)
}