Swift clip: Key paths and functions
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)
)
}
}