PonyExpress

2.0.0

Type-safe NotificationCenter alternative for Swift
adamwulf/PonyExpress

What's New

2.0.0

2023-01-18T03:58:34Z

Update Mail to be either VerifiedMail or UnverifiedMail. All VerifiedMail must define a sender type and must be posted by an object of that type. UnverifiedMail can be posted by any sender type.

PonyExpress

PonyExpress provides a type-safe alternative to NotificationCenter.

CI

Documentation

View the documentation for PonyExpress. Documentation for Xcode can be built with the included builddocs.sh script.

$ ./builddocs.sh

The jq tool is also needed to format the docc json files that are generated.

$ brew install jq

Installation

PonyExpress is available as a Swift package.

.package(url: "https://github.com/adamwulf/PonyExpress.git", .branch("main"))

https://github.com/adamwulf/PonyExpress.git

Quick Start

Any object or value can be sent as a notification. The recipient registers a handler method for the type of object to receive.

An example:

struct ExampleNotification: Mail {
    var info: Int
    var other: Float
}

class ExampleRecipient {
    init() {
        PostOffice.default.register(self, ExampleRecipient.receive)
    }

    func receive(notification: ExampleNotification) {
        // ... process the notification
    }
}

// Send the notification ...
let recipient = ExampleRecipient()
PostOffice.default.post(ExampleNotification(info: 12, other: 15))

Posting notifications

Any object that implements the empty Mail protocol can be sent as a notification, and only recipients registered for that notification type will receive it.

// Send a struct with the above example, or send an enum, or any other type.
enum ExampleEnum: Mail {
    case fumble
    case mumble(bumble: Int)
}

PostOffice.default.register { (mail: ExampleEnum) in
    // ... process the notification
}

PostOffice.default.post(ExampleEnum.mumble(bumble: 12))

Observing notifications

There are multiple ways to receive notifications. All observers define the type of notification that they want to receive, and only notifications matching those types will be received. If a sender is specified, the type of that sender must also match.

Option 1: Register an object and method

Just as in NotificationCenter, the object is held weakly, and does not need to be explicitly unregistered when the object deallocs.

class MyClass {
    func init() {
        PostOffice.default.register(self, MyClass.receive) 
    }
    
    func receive(notification: ExampleNotification, sender: ExampleSender) {
        // process the notification
    }
}

Option 2: Register a block

A block or method can be passed into the PostOffice to observe notifications. Blocks are held strongly inside the PostOffice, and must be unregistered explicitly.

class MyClass {
    var token: RecipientId? 
    
    func init() {
        PostOffice.default.register { [weak self] (notification: ExampleNotification) in
            // process the notification
        }
    }
}

Unregistering

Every register() method will return a RecipientId, which can be used to unregister the recipient.

let recipient = ExampleRecipient()
let id = PostOffice.default.register(recipient)
...
PostOffice.default.unregister(id)

Senders

Sending a notification can optionally include a sender as well. This is similar to NotificationCenter, where recipients can optionally register for notifications sent only from a specific sender. In PonyExpress, both the notification and sender are strongly typed.

Recipients can choose to include or exclude the sender parameter from the receiving block or method.

class ExampleRecipient {
    init() {
        PostOffice.default.register(self, ExampleRecipient.receiveWithOptionalSender)
        PostOffice.default.register(self, ExampleRecipient.receiveWithSender)
        PostOffice.default.register(self, ExampleRecipient.receiveWithoutSender)
    }

    // An optional sender will require that the sender of the notification either
    // a) match the type of the `sender`, or b) be `nil`
    func receiveWithOptionalSender(notification: ExampleNotification, sender: ExampleSender?) {
        // ... process the notification
    }

    // An non-optional sender will require that the sender of the notification either match
    // the `sender` type
    func receiveWithSender(notification: ExampleNotification, sender: ExampleSender) {
        // ... process the notification
    }

    // Omitting a `sender` parameter will receive notifications for senders of any type, even nil senders
    func receiveWithoutSender(notification: ExampleNotification) {
        // ... process the notification
    }
}

// recipients can also register to receive notifications from a singular exact-match sender
let sender = ExampleSender()
let recipient = ExampleRecipient()
PostOffice.default.register(sender: sender, recipient, ExampleRecipient.receiveWithSender) 
PostOffice.default.register(sender: sender, recipient, ExampleRecipient.receiveWithoutSender) 

When posting a notification, a sender can optionally be provided.

PostOffice.default.post(ExampleNotification(info: 12, other: 15), sender: sender)

DispatchQueues

When registering with a PostOffice, the recipient can choose which DispatchQueue to be notified on. If no queue is specified, the notificaiton is sent synchronously on the queue that posts the notification. If a queue is specified, the notification is sent asynchronously on that queue.

PostOffice.default.register(queue: myDispatchQueue, recipient, MyClass.receive) 

Motivation

Notifications using NotificationCenter are sent through a [String: Any] userInfo property of the notification. This means that any observesr for that notification need to decode the userInfo using something like guard let myStuff = notification.userInfo["someProperty"] as? MyStuff.

This provides a number of problems:

  • "someProperty" could contain a typo. Using a constant is susceptible to copy/paste errors.
  • Notifications are verbose - they require a notification name, the userInfo keys, and the actual values
  • Values are not typesafe. Sending a Float and attempting to decoding a CGFloat will silently fail (or runtime error).
  • When recieving unexpected data, observers either siliently fail or crash at runtime.

In PonyExpress, the goal is to reduce verbosity and move errors from runtime to compile time.

  • Observers always receive the exact types they expect
  • Any errors are provided at compile time, guaranteeing runtime type safety
  • No extra String names or keys - only the actual data is sent without any extra boiler plate

Thanks! ❤️

Enjoying PonyExpress? Say thanks and buy me a coffee ☕️!

Description

  • Swift Tools 5.7.0
View More Packages from this Author

Dependencies

Last updated: Sun Jan 22 2023 02:31:56 GMT-0500 (GMT-05:00)