Weekly Swift articles, podcasts and tips by John Sundell.

Swift clip: Key paths and functions

Published on 08 May 2020

Let’s take a look at how Swift’s key paths work, and how they relate to functions — both in terms of what comes built into the language itself, and what kind of utilities that we can write ourselves to form some really nice convenience APIs.

For more on Swift’s key paths feature, check out this category page.

Sample code

Using key paths to extract property values from a Product value:

struct Product {
    var name: String
    var kind: Kind
    ...
}

let keyPath = \Product.name

var product = Product(name: "iPhone 11", kind: .phone)

product[keyPath: keyPath] // iPhone 11
product[keyPath: \.kind] // Product.Kind.phone
product[keyPath: keyPath] = "iPhone SE"

Passing a key path as a function when using map to transform an array:

let products: [Product] = [...]
let names = products.map { $0.name }
let names = products.map(\.name)

Extending the Sequence protocol with a key path-based convenience API for sorting:

extension Sequence {
    func sorted<T: Comparable>(
        by keyPath: KeyPath<Element, T>
    ) -> [Element] {
        sorted { a, b in
            a[keyPath: keyPath] < b[keyPath: keyPath]
        }
    }
}

How using our new sorted method compares to using an inline sorting closure:

// Using an inline closure:
products.sorted(by: { $0.name < $1.name })

// Using a key path:
products.sorted(by: \.name)

A CanvasViewController that currently uses an asynchronous completion handler closure to assign a UIImage to a UIImageView:

class CanvasViewController {
    private var canvas = Canvas()
    private lazy var previewImageView = UIImageView()

    ...

    func renderPreviewImage() {
        canvas.renderAsImage { [weak self] image in
            self?.previewImageView.image = image
        }
    }
}

Writing a utility function that enables us to convert a reference writable key path into a setter closure:

func setter<O: AnyObject, V>(
    for object: O,
    _ keyPath: ReferenceWritableKeyPath<O, V>
) -> (V) -> Void {
    { [weak object] value in
        object?[keyPath: keyPath] = value
    }
}

Using our new utility function:

class CanvasViewController {
    private var canvas = Canvas()
    private lazy var previewImageView = UIImageView()

    ...

    func renderPreviewImage() {
        canvas.renderAsImage(
            then: setter(for: previewImageView, \.image)
        )
    }
}