Cool Swift Tricks 3: The Garden of Forking Key Paths

This is the third of four posts about random little Swiftisms for you to amaze your friends and confound your enemies. All of these came up in my real code recently, and they really did surprise one or more of my co-workers. The Swift language has a lot of cool features hidden away in its nooks and crannies, but most real developers are too busy getting real work done to keep up with all of them; so every once in a while I've submitted a pull request that made someone say, "Hey, I didn't know about that!" If you already knew about these features, you can feel smug, and if you didn't, now you do, so you can feel smug anyway.

Swift key paths are the Swift analog to Objective-C key paths, and are usually used just because Cocoa uses them. For instance, the Swift KVO registration method observe(_:options:changeHandler:) takes a key path as its first parameter, just because the Cocoa method addObserver(_:forKeyPath:options:context:) takes a key path as its second parameter.

In my own code, I haven't had much call to use Swift key paths for their own sake. Here's a situation, though, where they served an elegant purpose:

Our app uploads documents. Each document is a PDF file that sits on disk waiting to be uploaded. At the same time, there has to be another file that describes each document. So we have a value struct, LocalDocument, a simple collection of properties that conforms to Codable so that it can be saved to disk.

Whenever we do something that affects the description of a document, we have to find its corresponding encoded LocalDocument on disk and update that description. For instance, when the document's name is determined, we need to set the name property of the LocalDocument on disk; when the document is uploaded, we need to set the uploadedDate property of the LocalDocument on disk; and so on. The only way to do that is to read the LocalDocument from disk, decode it, set the property in question, and encode and save the LocalDocument again.

But I don't want code scattered all over the app to have to enact that dance: read, decode, set a property, encode, save. Clearly there needs to be some single general method that anyone can call. And what are the parameters to this method? They are:

  • The unique identifier of the LocalDocument that needs to be updated.

  • The property that needs to be changed.

  • The value that needs to be assigned to that property.

Well, how can we specify the general notion "the property that needs to be changed"? As a key path! Different properties have different types, but that's not a problem; we can take care of it with a generic. So here's the declaration of the method in question (it's a static method of a LocalDocumentHelper type):

static func update<T>(guid: String, value: T, keyPath path: WritableKeyPath<LocalDocument, T>) throws {

The outcome is that code elsewhere in the app can talk like this:

try? LocalDocumentHelper.update(guid: doc.guid, value: Date(), keyPath: \.uploadedDate)

That was enough to impress a colleague, who had never thought of doing it this way and didn't know about the literal key path notation \.uploadedDate.

Check out more:
Cool Swift Trick 1: Mirror, Mirror
Cool Swift Trick 2: No Escape

#generics #keypaths #Swift