BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

How Swift keypaths let us write more natural code

Combine keypaths, associated types, and generics in one

Paul Hudson       @twostraws

Swift 4.0 introduced new keypath types as part of SE-0161, but it’s fair to say a lot of folks either don’t understand them or don’t yet understand the benefits they can deliver.

Keypaths are designed to allow you to refer to properties without actually invoking them – you hold a reference to the property itself, rather than reading its value. Their use in Swift is still evolving, and in many ways they are influenced by keypath support in Objective-C, but already some clear design patterns are emerging.

I cover a selection of uses for Swift keypaths in my book Swift Design Patterns, but here I want to demonstrate how invaluable they are as a way of letting us reference different types in a natural way.

Try creating these two structs in your playground:

struct Person {
    var socialSecurityNumber: String
    var name: String
}

struct Book {
    var isbn: String
    var title: String
}

I’ve purposefully made them completely different so you can see how this pattern works.

Both Person and Book have an identifier that is unique: socialSecurityNumber and isbn respectively. If we wanted to be able to work with identifiable objects in general, we could try to write a protocol like this:

protocol Identifiable {
    var id: String { get set }
}

That would work well enough for Person and Book, but it would struggle for anything that didn’t store its identifier as a string – a WebPage might use a URL, and a File might use a UUID, for example.

Worse, it locks us into using id as the unique identifier for all our data, which isn’t a descriptive property name – you need to remember that id is actually a social security number for people and an ISBN for books.

Keypaths can help us solve this problem, allowing us to use them as adapters for very different data types – i.e., allow them to be treated the same even though they aren’t the same. The Gang of Four book has a conceptually similar approach called the adapter pattern: something that allows incompatible data types to be used together by wrapping an interface around them.

What we’re going to do is create an Identifiable protocol that uses an associated type (a hole in the protocol), then use that as a keypath. What this means is that every conforming type will be asked to provide a keypath that points towards whatever property identifies it uniquely – socialSecurityNumber and isbn for our two example structs.

First, add this protocol:

protocol Identifiable {
    associatedtype ID
    static var idKey: WritableKeyPath<Self, ID> { get }
}

WritableKeyPath is one of several variants of Swift's keypath types that let us store keypaths for later. In this case we’re saying that the keypath must refer to whichever type conforms to the protocol (Self) and it will have the same value as whatever is used to fill the ID hole in our protocol.

Now let’s update both Person and Book so they conform to the protocol. This means having adding Identifiable to their list of conformances, then defining idKey to point to whichever of their properties is their unique identifier:

struct Person: Identifiable {
    static let idKey = \Person.socialSecurityNumber
    var socialSecurityNumber: String
    var name: String
}

struct Book: Identifiable {
    static let idKey = \Book.isbn
    var isbn: String
    var title: String
}

Swift’s type inference is extremely clever here. When it looks at Person it will:

  1. Remember socialSecurityNumber is a String.
  2. Store that idKey points to Person.socialSecurityNumber, which is a string.
  3. Match idKey in Person with the same property in Identifiable.
  4. Resolve WritableKeyPath<Self, ID> to WritableKeyPath<Self, String>.
  5. Understand that associatedtype ID is a hole that is being filled by a string.

I know that Swift code can sometimes take a long time to compile, but you have to admit it’s pretty darn amazing.

What we’ve achieved here is that totally disparate data types – structs that are designed to store properties in whichever way works best for them rather than following some arbitrary names imposed by a protocol – are able to be used together. All we care about is that they conform to Identifiable: once we know that we also know it has an idKey keypath that points to where its identifying property is.

Putting all this together we can print the identifier of any Identifiable type like this:

func printID<T: Identifiable>(thing: T) {
    print(thing[keyPath: T.idKey])
}

let taylor = Person(socialSecurityNumber: "555-55-5555", name: "Taylor Swift")
printID(thing: taylor)

Yes, that code leverages generics, keypaths, and associated types all in one, and with surprisingly little code – Swift is an extremely powerful language when you really lean on it. The end result is that we’ve been able to separate our architecture from implementation details – we’ve let types be expressed naturally, then used protocols to overlay an adapter on top to allow those types to be used together.

This was an excerpt from my book Swift Design Patterns – if you'd like to learn more about keypaths, along with delegation, protocols, associative storage, and more, you should check it out!

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.