Strongly Typed Notifications in Swift
While working on rewriting Spotijack in Swift, I started to
feel dissatisfied with
Foundation’s notification API. It’s a stringly typed
API that makes heavy use of Any
and as someone who loves their types this
makes me sad. To cheer myself up, I set about writing a more strongly typed
notification system.
The end result is a small library1—TypedNotification—that provides a set of protocols defining a more descriptive type system for notifications. Check out the GitHub project if you’re interested. There’s a Playground in it demoing the protocols. The rest of this blog post will cover them in a bit more detail.
Features
Before I show you the protocols, let me run you through what I wanted to achieve.
Closure Based API
First, I wanted to have a closure based API that closely mirrors the foundation API. Swift has a concise syntax for closures that makes it easy to create short pieces of code. For longer blocks of code, Swift has function references that let functions be used as closures. Furthermore, closures encapsulate information on the types of their arguments which selectors lack. The foundation API already contains a block based function for notifications so my protocol can simply mimic that API.
Notifications as Types
The second requirement is for notifications to convey as much information as
possible using their types. That means no identifying notifications by a string
(at least, not to the user) and no userInfo
dictionary to attach data to a
notification. A notification’s identifier should be inherent to its type and any
data attached to the notification should be declared as properties.
Automatic Removal of Observers
Finally, I wanted to get rid of the need to think about the lifetime of observers. The block based foundation API returns an opaque object that users must remember to unregister before it is deallocated. Forgetting to manage these objects creates bugs that are difficult to pass off as features.
The TypedNotifcation Protocol
Overview
This is the core protocol of TypedNotifications. Types that conform to this protocol can be posted as notifications. The protocol declaration is simple:
protocol TypedNotification: Namespaced {
associatedtype Sender
/// The name of the notification to be used as an identifier.
static var name: String { get }
/// The object sending the notification.
var sender: Sender { get }
}
All types conforming to TypedNotification
have a name
property that’s used
to identify the notification and a sender
property that’s used to identify the
sender. The sender
property has an associated type that can be used to
constrain senders to a subset of types.
This protocol reduces the chances of making a mistake with the stringly typed
notification system by only having to declare the name of the notification
once. It also adds some information about the contents of the notification by
providing an associated type for the sender
property.
To reduce the chances of a notification name collision, I’ve made the
TypedNotification
protocol conform to a Namespaced
protocol which looks
like:
protocol Namespaced {
static var namespace: String { get }
}
A protocol extension on TypedNotifcation
uses the Namespaced
protocol to
provide a default implementation for the name
property:
extension TypedNotification {
static var name: String {
return "\(Self.namespace).\(Self.self)"
}
}
This will generate a notification name using the namespace
property and the
name of the type conforming to TypedNotifcation
.
Usage
For each notification that your application posts, create a type that conforms
to TypedNotification
and implement the required properties. Thanks to the
aforementioned protocol extension, only the sender
and namespace
properties
need to be implemented. You can write a protocol extension on Namespaced
to
reduce the implementation down to just the sender
property. As an example:
extension Namespaced {
static var namespace: String { return "org.alexj" }
}
struct ExampleNotification: TypedNotification {
let sender: ExampleClass
let newValue: Double
}
Here, ExampleClass
is the only class that’s responsible for sending instances
of ExampleNotification
. If multiple types can post a notification, consider
constraining the sender
property using a protocol. If worst comes to worst,
you can make sender
an instance of Any?
at the expense of some type safety.
The TypedNotificationCenter Protocol
Overview
To post instances of TypedNotifcation
, the library provides another protocol
called TypedNotificationCenter
. This declares three methods to post
notifications, add observers and remove observers:
protocol TypedNotificationCenter {
/// Post a `TypedNotification`
func post<T: TypedNotification>(_ notification: T)
/// Register a block to be executed when a `TypedNotification` is posted.
func addObserver<T: TypedNotification>(forType type: T.Type, object obj: T.Sender?,
queue: OperationQueue?, using block: @escaping (T) -> Void) -> NotificationObserver
/// Deregister a `NotificationObserver`.
func removeObserver(observer: NotificationObserver)
}
Aside from a different type signature, these methods mirror the Foundation
NotificationCenter
APIs. TypedNotification includes an extension on
NotificationCenter
that adds conformance to the TypedNotificationCenter
protocol. You can use the protocol when writing tests.
The NotifcationObserver Class
The addObserver
method returns an instance of NotificationObserver
rather
than the NSObjectProtocol
conforming object returned by the Foundation
API. NotificationObserver
is a lightweight class that stores an
NSObjectProtocol
conforming object. When a NotificationObserver
is
deallocated, removeObserver
is automatically called. There’s no need to
manually remove observers any more, just store2
a strong reference to the NotificationObserver
.
Usage
Usage is almost identical to using the Foundation API. Building on the previous
example, here’s how to use a TypedNotification
conforming type with
NotificationCenter
:
class ExampleClass {
private let center = NotificationCenter.default
private var _valueObserver: NotificationObserver? = nil
init() {
_valueObserver = center.addObserver(forType: ExampleNotification.self, object: self, queue: nil) { (noti) in
print("New value: \(noti.newValue)")
}
}
var value = 0.0 {
didSet {
center.post(ExampleNotification(sender: self, newValue: value))
}
}
}
Note that the closure parameter noti
is of type ExampleNotification
so you
can directly access the newValue
property without any downcasting. Also note
that the observer is tied to the lifetime of ExampleClass
. When an instance of
ExampleClass
is deallocated, _valueObserver
will remove itself as an
observer.
Conclusions
I think the advantages of this library compared to the Foundation API are
clear. Representing notifications as types improves the safety of your code by
eliminating manually managed string identifiers and weakly typed userInfo
dictionaries. A further advantage of typed notifications is
self-documentation. As data attached to a notification is part of the type,
there’s no need to document the keys of a userInfo
dictionary. Users can look
at the public interface of a TypedNotification
conforming type to see what
data it provides.
The TypedNotificationCenter
protocol goes hand in hand with the
TypedNotification
protocol. It improves run time safety by automatically
removing observers when they are deallocated, eliminating an entire class of
bugs. Furthermore, it provides a starting point for writing tests for
notifications.
The TypedNotification library is available from GitHub under an MIT license. It is compatible with the Swift Package Manager and Carthage.