Next article: Friday Q&A 2015-02-06: Locks, Thread Safety, and Swift
Previous article: Friday Q&A 2014-11-07: Let's Build NSZombie
Tags: fridayqna letsbuild notifications swift
NSNotificationCenter
is a useful API that's pervasive in Apple's frameworks, and often sees a lot of use within our own code. I previously explored building NSNotificationCenter
in Objective-C. Today, I want to build it in Swift, but not just another reimplementation of the same idea. Instead, I'm going to take the API and make it faster, better, stronger, and take advantage of all the nice stuff Swift has to offer us.
NSNotifications
NSNotifications are both simple and powerful, which is why they show up so often in the frameworks. Specifically, they have a few distinct advantages:
- Loose coupling between notification senders and receivers.
- Support for multiple receivers for a single notification.
- Support for custom data on the notification using the
userInfo
property.
There are some disadvantages as well:
- Sending and registering for notifications involves interacting with a singleton instance with no clear relationship to your classes.
- It's not always clear what notifications are available for a particular class.
- For notifications which use
userInfo
, it's not always clear what keys are available in the dictionary. userInfo
keys are dynamically typed and require cooperation between the sender and receiver that can't be expressed in the language, and messy boxing/unboxing for non-object types.- Removing a notification registration requires an explicit removal call.
- It's difficult to inspect which objects are registered for any given notification, which can make it hard to debug.
My goal in reimagining notifications in Swift is to remedy these problems.
API Sketch
The public face of the API is a class called ObserverSet
. An ObserverSet
instance holds a set of observers interested in a particular notification sent by a particular object. Rather than declaring string constants and intermediating through a singleton, the existence of a notification is just a public property of the class:
class ExampleNotificationSender {
public let exampleObservers = ObserverSet<Void>()
The Void
is the type of data that's sent along with the notification. Void
denotes a pure notification with no additional data. Sending data is as simple as providing the type:
public let newURLObservers = ObserverSet<NSURL>()
This is a notification that provides a NSURL
to all observers each time it's sent.
Multiple pieces of data are no problem with the magic of tuples:
public let newItemObservers = ObserverSet<(String, Int)>()
This provides each observer with the name and index of a new item. If you want to make this more explicit, you can even give the parameters names:
public let newItemObservers = ObserverSet<(name: String, index: Int)>()
To register an observer, add a callback to the observer set:
object.exampleObservers.add{ println("Got an example notification") }
object.newURLObservers.add{ println("Got a new URL: \($0)") }
The add
method returns a token that can be used to remove the observer:
let token = object.newItemObservers.add{ println("Got a new item named \($0) at index \($1)") }
...
object.newItemObservers.remove(token)
A common case for notifications is to receive them with a method, and deregister the notification when the instance is deallocated. This is easy to accomplish using a variant add
method:
object.newItemObservers.add(self, self.dynamicType.gotNewItem)
func gotNewItem(name: String, index: Int) {
println("Got a new item: \(name) \(index)")
}
The self.dynamicType
syntax is a bit verbose and redundant, but tolerable. The observer set will hold a weak reference to self
and automatically remove the observer when the instance is deallocated, and in the meantime it will invoke gotNewItem
whenever it sends a notification.
Sending a notification involves calling notify
and passing the appropriate parameters:
exampleObservers.notify()
newURLObservers.notify(newURL)
newItemObservers.notify(name: newItemName, index: newItemIndex)
This makes for a really nice API. Going through the disadvantages listed above:
- There's no singleton involved. Each
(object, notification)
pair is represented with a separate observer set instance. - All notifications available for a class are public properties of that class.
- Explicit parameters are used to pass data to observers. They can be named in code to make it clear exactly what they are.
- Notification parameters are statically typed. The types are specified in the observer set property and notification senders and receivers are checked by the compiler. All types are supported, with no need for boxing.
- For the common case where an observer is removed when deallocated, removal can be automatic.
- Each observer set maintains a list of entries which can be inspected in the debugger.
Looks good! How, then, do we build it?
Observer Function Types
Let's assume that the parameters to an observer function are called Parameters
, which is the name I'll use for the generic type in the code. Fundamentally, an observer function's type is then Parameters -> Void
. However, for the common case where the observer function is a method, this makes it difficult to hold a weak reference to the observer object and clear the entry when the object is destroyed. When you get a method from a class, like self.dynamicType.gotNewItem
, the type of the resulting function is actually TheClass -> Parameters -> Void
. You call the function and pass it an instance of the class, and it then returns a new function for the method that applies to that instance.
In order to keep everything organized, we'll store a weak reference to the observer object, and we'll store the observer function in this longer form. For the simpler form of the add
method, the function can simply be wrapped in another function that throws away its parameter and returns the original function. Since observer objects can be different types, we'll store them as AnyObject
and the observer functions as AnyObject -> Parameters -> Void
.
Code
It's time to look at the implementation. As usual, the code is available on GitHub:
https://github.com/mikeash/SwiftObserverSet
Entries
Each entry in an observer set is a weakly held observer object and a function. This small generic class bundles the two together, allowing for arbitrary parameter types:
public class ObserverSetEntry<Parameters> {
private weak var object: AnyObject?
private let f: AnyObject -> Parameters -> Void
private init(object: AnyObject, f: AnyObject -> Parameters -> Void) {
self.object = object
self.f = f
}
}
The observer set can use the object and function to call observers, and can check the object for nil
to remove entries for deallocated objects. This class is marked public
because it will also be returned to callers to be used as the parameter to the remove
method.
Ideally, this class would be nested inside ObserverSet
. However, Swift doesn't allow nesting generic types, so it has to be a separate top-level type.
Observer Set
The ObserverSet
class also has generic Parameters
:
public class ObserverSet<Parameters> {
NSNotificationCenter
is thread safe, and this class should be as well. I chose to use a serial dispatch queue to accomplish that:
private var queue = dispatch_queue_create("com.mikeash.ObserverSet", nil)
I also wrote a quick helper function for it:
private func synchronized(f: Void -> Void) {
dispatch_sync(queue, f)
}
With this, it's as simple as writing synchronized{ ...code... }
on the code that uses shared data.
The entries are kept in an array:
private var entries: [ObserverSetEntry<Parameters>] = []
Properly speaking, this should be a set rather than an array. However, sets aren't all that nice to use in Swift yet, as there's no built-in set type. Instead, you have to either use NSSet
, or use a Dictionary
with a Void
value type. Since observer sets will typically contain a few entries at most, I decided to go for clarity instead.
Swift also insists on an explicit public initializer, even though it's empty, as the default one apparently isn't made public:
public init() {}
That takes care of setup. Let's look at the main add
method, which takes an observer object and function:
public func add<T: AnyObject>(object: T, _ f: T -> Parameters -> Void) -> ObserverSetEntry<Parameters> {
Next, construct an entry. The type of f
doesn't quite match what ObserverSetEntry
expects, as it's looking for a function that takes AnyObject
, while this one takes T
. A small adapter takes care of the mismatch with a type cast:
let entry = ObserverSetEntry<Parameters>(object: object, f: { f($0 as T) })
It's unfortunate to bypass Swift's type system in this way, but it gets the job done.
With the entry created, add it to the array:
synchronized {
self.entries.append(entry)
}
Finally, the entry is returned to the caller:
return entry
}
The other add
method is a small adapter:
public func add(f: Parameters -> Void) -> ObserverSetEntry<Parameters> {
return self.add(self, { ignored in f })
}
This passes self
as the object simply because it's a convenient pointer that's guaranteed to stay alive. Since entries with nil
objects are removed, this keeps the entry around as long as the observer set itself. The function passed in for the second parameter just ignores its parameter and returns f
.
The remove
method is implemented by filtering the array to remove the matching entry. Swift's Array
type doesn't have a remove
method, but the filter
method accomplishes the same task:
public func remove(entry: ObserverSetEntry<Parameters>) {
synchronized {
self.entries = self.entries.filter{ $0 !== entry }
}
}
The notify
method is simple in principle: for each entry, call the observer function, and remove any entry with a nil
observer object. However, it's made a bit complicated by the fact that entries
needs to be accessed from within synchronized
, but it's a bad idea to call observer functions from there, because it could easily lead to a deadlock. To avoid that problem, the strategy is to collect all of the observer functions in a local array, then iterate and call them outside of the synchronized
block. Here's the function:
public func notify(parameters: Parameters) {
The functions to call are collected in an array:
var toCall: [Parameters -> Void] = []
Note that the type of the functions doesn't include the initial AnyObject ->
. To keep things simple, we'll make that first call within the synchronized
block, so that the functions collected in the array are the final observer functions with the observer object already applied.
Iterating over the entries needs to happen in a synchronized
block:
synchronized {
for entry in self.entries {
Skip entries with a nil
object:
if let object: AnyObject = entry.object {
Calling entry.f
with the object produces the observer function to call:
toCall.append(entry.f(object))
}
}
Before we leave the synchronized
block, clean up the entries by filtering out the ones that now contain nil
:
self.entries = self.entries.filter{ $0.object != nil }
}
Now that the functions are collected and the synchronized
block is finished, we call the observer functions:
for f in toCall {
f(parameters)
}
}
That's it for the ObserverSet
class. Let's not forget the closing brace:
}
A Note on Tuples
You'll note that none of the ObserverSet
code presented addresses the case where there are multiple Parameters
, for example:
public let newItemObservers = ObserverSet<(String, Int)>()
However, it works anyway, using the code above. What's going on?
It turns out that Swift makes no distinction between a function that takes multiple parameters and a function that takes one paremeter whose type is a tuple. For example, this code calls a function using both ways:
func f(x: Int, y: Int) {}
f(0, 0)
let params = (0, 0)
f(params)
This means that the ObserverSet
code can be written for functions with one parameter, and it gets support for multiple parameters for free. There are some strong limits on what you can do in these cases (for example, it's essentially impossible to modify any of the parameters) but it works great in this case.
ConclusionNSNotificationCenter
is handy, but Swift language features allow for a much improved version. Generics allow for a simple API that still allows for all the same use cases while providing static types and type checking on both sides.
That's it for today! Come back next time for more frightening experiments. In the meantime, Friday Q&A is driven by reader ideas, so if you have something you'd like to see covered here, send it in!
Comments:
As for dynamicType, you can certainly use the class name instead. I decided not to simply because that ends up being repetitive and requires renaming a bunch of extra spots if you change the class name. Not a particularly big deal, more a matter of personal preference.
Also, if you're redoing the observer/notification mechanism anyway, why not tackle the most annoying aspect of using them, manually deregistering them. I've seen solutions for that in Objective-C but a Swift implementation would be interesting, if possible.
I use that all the time. E.g. when managing a collection of some objects that all post notifications you are interested in and that collection changes over time.
With catch all you register a single notification to watch for posts from any object in [init], then in your handler method:
if ([collection containsObject:note.object]) {
handle notification...
}
and in [dealloc] you deregister
This would messier and possibly more bug prone without catch-all -- even with the Swift version.
I'm a bit puzzled about your "manually deregistering" comment, since the code above does handle exactly that with no fuss.
Brian B.: Adding a notification observation to each object as you add it to the array, and removing it when you remove from the array, seems just as easy and no more bug-prone. Unless you add and remove from many different places, which you probably shouldn't be doing anyway.
Or did you mean that multiple classes could send the same notification for different reasons and I might not know about it? i.e. Some random object could broadcast a notification I listen for I wasn't expecting. That just seems like bad notification usage by the offending object. Apple's classes don't do that (notifications are unique to a class or framework, usually). If a third party library was, say, issuing the UIKeyboard notifications for some reason, I'd probably rethink using that framework, if I could.
If there are really times that I don't want to take action based on a notification, I rely on the info dictionary to determine what action to take. In my experience, such situations are rare, especially for notifications that are intended to be global. If the notification doesn't include a useful info dict, again, that seems like bad notification design, not a problem with the anonymous notification pattern in general.
My only point is that your solution, while interesting, completely ignores this valid and useful scenario for notifications.
The idea that you could somehow end up with multiple instances that would all process a logout event, and clobber each other's efforts, seems like a stretch. People who want to use anonymous notifications are likely to be mindful of what they're doing, and it's wrong to suggest that they should feel insecure for correctly employing bona fide Cocoa features.
From a blog called NSBlog, I would have expected a more humble and encouraging approach. Attempting to position oneself as superior to the creators of Cocoa is asinine.
David: If you expected a humble approach here then I think you may be confused. You'll get zero traction with me by proposing that I shouldn't be critical of what I see as Cocoa design mistakes. Cocoa has suffered from some mistakes during its long life, and I'm not going to be shy about pointing them out. More than that, a lot of Cocoa programmers value expediency over good design. For example, I see a ton of Cocoa apps that are chock full of singleton objects that shouldn't be there, and are used just because it's slightly easier to access a singleton than to pass instances around. Anonymous notifications look to be the same sort of problem to me. You certainly have every right to disagree, but you need to bring some sort of argument if you want to discuss it, not simply declare me to be wrong because I might disagree with Apple.
Jon: I agree with Mike, and Apple's classes definitely do this. It's like listening to UITableViewSelectionDidChangeNotification on <em>all UITableViews</em> in the app. You can do it, but it only makes sense if you know that all the UITableViews in your app should behave the same way. The same goes for any notifications you create yourself: if everything in the app deserves the same response, then it's great...but it makes it harder if you ever want to differentiate in the future.
App could have multiple contexts for internal needs and main thread could be integrating changes of data into main context. And main context should not be aware of all possible background contexts. So, we listen to all notifications and merge those changes.
You mention how it's unfortunate to bypass swifts type safety when you go from AnyObject to T when adding an entry. Why must you use AnyObject in one place and T : AnyObject in the other? Can you please explain the reasoning behind that? (Sorry if this is obvious, I'm a swift beginner).
Also, another reason to avoid anonymous notifications: testing becomes a huge pain since there may be multiple instances of those objects you thought were singletons while all those tests are running. I guess that's also a reason to avoid singletons.
Jordan: Nice suggestion about using _ instead of ignored. I think I'm going to stick with "ignored" for now as it seems more clear to me. This may just be due to the newness of the language, though. Once people have used Swift for a while I imagine that _ will look more natural there.
Pavel: Temporary brain malfunction. I pushed a quick fix. Thanks for pointing that out!
Deniska: Doesn't that assume that your app is only ever manipulating one Core Data store? That seems like a bad assumption to make.
Jason: Excellent question. It comes down to not being able to figure out a good way to have a heterogeneous collection with generics. In isolation, it would make sense for
ObserverSetEntry
to be written as ObserverSetEntry<T, Parameters>
. That way, everything would be consistently genericized and life would be good. But I couldn't figure out a way to have ObserverSet
reasonably contain and talk to a bunch of ObserverSetEntry
objects with different T
types. By having them all use AnyObject
that problem is bypassed.
Jean-Daniel: I agree, that looks like a good case for nil. In this particular case, since you'd likely never want to listen for that notification for anything but
nil
, the same functionality could be provided with a global ObserverSet
instance for all of NSBundle
. Loaded code is inherently global, of course, and it would make sense to have an explicitly global notification system to go with it.The first thing in the NSManagedObjectContextDidSaveNotification handler method is a test of the posting context to see if it's one I care about: "Is this save affecting my store?"
Given the extensive creation of scratch contexts all through the app, it's simpler than registering a listener for each context as I create it, and deregistering as I discard.
Can you explain what you mean by "(for example, it's essentially impossible to modify any of the parameters)" in the section about using named tuples as the argument to a single-parameter function?
a = b
gets you a new one.
Bill: Consider a generic passthrough function:
func call<T>(function: T -> Void, parameters: T) {
function(parameters)
}
This works for multiple parameters. Now imagine if you wanted to write a variant that would, say, add 1 any integer parameters before calling the original function. There's no reasonable way to accomplish that.
I completely agree with you on the evils of singletons, of which nil uses of
NSNotificationCenter
are certainly one. It might be possible to argue that if we are using a non-shared instance of the notification centre (ie one that we instantiated and injected everywhere it is used to subscribe / publish) then it is no longer a singleton but merely a stateful object with dynamic properties. However, almost all uses of it seem to pass through the shared instance.
Unfortunately there are several scenarios where it seems that we have no choice but to use this singleton behaviour, such as when listening for keyboard notifications (in iOS) because the objects we want to listen to are not made available to us (if anyone knows a way around this I would be interested to hear it).
I have been taking great pains to get rid of all singletons in my code (to the length of injecting
NSBundle
and UIScreen
into objects which need access to them, instead of calling mainBundle
and mainScreen
). Might I suggest an article on the best ways to avoid singletons in Cocoa?
For those interested, the most persuasive argument against singletons that I came across is this old article - http://www.object-oriented-security.org/lets-argue/singletons
It seems that so many code/application security problems would simply not exist if complete dependency injection were enforced.
Thanks again.
Regarding the use of dispatch_sync to a serial queue, if you've ever benchmarked that particular construct you'll know that it is frightfully slow. You could instead use OSSpinLock, which would be just fine for the particular application you're using:
private var lock = OS_SPINLOCK_INIT
private func synchronized(f: Void -> Void) {
OSSpinLockLock(&lock)
f()
OSSpinLockUnlock(&lock)
}
The time delay involved can be orders of magnitude shorter this way than when using dispatch_sync.
Actually, that approach works okay with multiple core data stores configuration.
Notification has sender which is a context being saved. I check that my context and sender have the same persistentStoreCoordinator.
If not - I don't need to merge changes.
Something like that.
And moreover, this works faster (in my case) than having several nested contexts.
Deniska: I'd say that indicates a design problem in the Core Data API. If save notifications affect everything related to a particular persistent store coordinator, then that should be the object posting the notifications, not the individual contexts. (If there are cases where you only care about the specific context then I'd say both should be sending a notification.)
private let f: AnyObject -> Parameters -> Void
AnyObject
and returns another function. The return type takes Parameters
and returns VoidI asked about this here:
http://stackoverflow.com/questions/34980308/how-many-is-too-many-for-create-dispatch-queues-in-gcd-grand-central-dispatch
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.
Of course, in this case I can't unsubscribe to the channels but so far it works for my use-cases.
Regarding your concept of this replacing the
NSNotificationCenter
notifications, there is one important use-case that not covered in this approach, and seems not possible indeed, is observing a notification on any object that sends it (nil for object in addObserver). In a way this is also what allows such loose coupling onNSNotifications
.A question that I do have, is why do you have to go through
dynamicType
property? You could use ClassName.method, if I'm not mistaken, too.