PonyExpress: Type-safe notifications in Swift

I’m very excited to be releasing my type-safe notification Swift package PonyExpress. I’m really enjoying the type-safety of Swift, and all of the compiler warnings and errors that it provides. Each compiler error from a type issue is a runtime crash that’s been saved.

The Swift Package

By default, the provided NotificationCenter inherited from Objective-C let’s us send notifications in Swift as well. Unfortunately, it’s not type-safe in a number of ways. First, observers are registered with a #selector which may or may not have it’s input argument correctly typed – the compiler won’t complain.

Second, notification objects are sent in an untyped userInfo: [String: Any]? property of the Notification. Any additional information sent along with the notification needs to be inspected. That String key isn’t compiler checked, and if that key changes, or if new keys are added or removed, there’s no compile-time checks to prevent bugs sneaking in.

As a brief example, you can see below the code to register, send, and receive a notification using the untyped NotificationCenter apis provided by Foundation in Swift.

// Register an observer, I hope the `documentUpdated()` method is typed correctly!
let obj = MumbleBumble()
NotificationCenter.default.addObserver(obj, selector: #selector(documentUpdated), name: .DocumentUpdated, object: nil)

class MumbleBumble {
    // Process incoming notifications
    @objc func documentUpdated(notification: Notification) {
        // I hope we're using the correct key name and Type!
        guard let scopeId = notification.userInfo?["scopeId"] as? LogScope.Id else { return }
        ...
    }
}

// Send the notification, I hope the observers are looking for the correct key names!
let scopeId: LogScope.Id = LogScope.Id()
NotificationCenter.default.post(Notification(name: .DocumentUpdated, object: nil, userInfo: ["scopeId": scopeId]))

There’s lots of hoping in the above code, and not a lot of static compiler-time checks. This is the problem I’m aiming to solve (or at least make better) in PonyExpress.

In PonyExpress, any object that implements the empty Mail protocol can be sent as a notification. Then, any recipient can request to be notified of a specific Mail type (and optionally a specific sender and type too).

Rewriting the above in PonyExpress, we get:

// Register an observer, with compiler-time checks that `documentUpdated` accepts a `Mail` subtype
let obj = MumbleBumble()
PostOffice.default.register(obj, MumbleBumble.documentUpdated)

class MumbleBumble {
    // Process incoming strongly typed `notification`
    func documentUpdated(notification: DocumentUpdatedNotification) {
        let scopeId: LogScope.Id = notification.scopeId
        ...
    }
}

// Any type can implement the empty `Mail` protocol to be able to be sent as a notification
struct DocumentUpdatedNotification: Mail {
    let scopeId: LogScope.Id
}

// Send the notification, no magic strings or key names required
PostOffice.default.post(DocumentUpdatedNotification(scopeId: LogScope.Id()))

Renaming the DocumentUpdateNotification.scopeId to some other name is now compiler checked and verified, unlike changing the name of the Notification.userInfo key. Much safer!

Initially, I’d wanted to be able to constraint the notification type both when registering for a notification, and also when creating a PostOffice to send other specific type besides Mail. Unfortunately, Swift can’t yet constraint a type based on another generic type – I’ve written my findings on the topic here.

The Documentation

This is also the first time I’ve used DocC to generate documentation for a Swift package. All of the comments in the PonyExpress codebase are used to autogenerate documentation for the package. The best part, the documentation is both published on GitHub and can also be imported directly into Xcode. Overall, I’m very happy with the result. There was a bit to learn about how GitHub expects the documentation files to be formatted vs the .doccarchive that Xcode expects.

Thankfully, I was able to script the documentation build process in the included builddocs.sh script. One fun bit was using jq to format the json files generated by docc. Without the auto-formatting, each run of the build script could reorder keys in the json dictionaries, leading to unnecessary source changes between commits.

Conclusion

I’m excited to be using PonyExpress in my future projects, and I hope others find it helpful as well! 🙌