NEW! Master Swift design patterns with my latest book! >>

How Swift keypaths let us write more natural code

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!

 

MASTER SWIFT NOW
Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Practical iOS 11 Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Was this page useful? Let me know!

Average rating: 5.0/5

Click here to visit the Hacking with Swift store >>