PonyExpress
provides a type-safe alternative to NotificationCenter
.
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
PonyExpress
is available as a Swift package.
.package(url: "https://github.com/adamwulf/PonyExpress.git", .branch("main"))
https://github.com/adamwulf/PonyExpress.git
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))
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))
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.
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
}
}
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
}
}
}
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)
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)
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)
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 aCGFloat
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
Enjoying PonyExpress
? Say thanks and buy me a coffee ☕️!