There is the standard class in the Apple’s Foundation framework: NotificationCenter, which is used to deliver global notifications to anyone who subscribes to them. Sounds like a great idea, doesn’t it? I think that it brings more harm than good and is used too much in iOS applications — just like the Singleton pattern. Both have their own use cases and are useful in specific circumstances, but those are quite rare. This article is about how to decrease the dependency on the global notification center.
Prelude
But first, what triggered this is I was updating a project from swift 4 to 4.2 and that required a dozen of replacements like this:
Just a simple rename of a few system constants, however having to replace them multiple times in a project indicates at least that it’s not DRY, which in turn points to a bigger issue. That is, a number of entities are subscribed to this global event bus (notification center) and each does something when the app goes to background or foreground. Testing becomes more complicated because now your tests depend on UIKit (where the notification name is defined), the real NotificationCenter and knowing the notification names. The entity (observer) itself is also more complicated because knowing what these notifications are and where they come from and how to subscribe to them is an additional responsibility (and reason to change, as confirmed by the necessary changes during the migration). Due to the dependency on UIKit, the entity is now restricted to iOS.
Sample Downloader
Let’s take a look at some sample code (note that it’s very simplified to show the point of the post). We’ll have a class that downloads some data by an identifier, it doesn’t matter how it does it, but it does matter that an iOS app is supposed to close network connections when going to background, so the most straightforward way to handle that is:
classDownloader{typealiasCancellationHandle=()->()funchttpGet(_url:URL,completion:@escaping(Data?)->())->CancellationHandle{lettask=URLSession.shared.dataTask(with:url){(data,_,_)incompletion(data)}task.resume()return{task.cancel()}}/// The handle of the currently running request if any.privatevarhandle:CancellationHandle?init(){NotificationCenter.default.addObserver(self,selector:#selector(appDidEnterBackgroundNotification),name:UIApplication.didEnterBackgroundNotification,object:nil)}funcdownload(byIdid:Int,completion:@escaping(String?)->()){handle=httpGet(URL(string:"http://example.com/"+id.description)!){dataincompletion(data.flatMap{String(data:$0,encoding:.utf8)})}}@objcfuncappDidEnterBackgroundNotification(){handle?()handle=nil}deinit{NotificationCenter.default.removeObserver(self)}}
Here the download(byId:completion:) is the API for the user (httpGet is an implementation detail), the completion is called with the data or nil in case of an error, and the download is cancelled automatically on backgrounding by subscribing to the UIApplication.didEnterBackgroundNotification from the default NotificationCenter. The notification center is an implicit dependency, which makes it harder to reuse this downloader where it is not available, similar to this quote:
… the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
So a test for this case would look like this (I don’t show faking a slow response necessary to make this a true unit test) — we have to use the real notification center and real notifications:
123456789
functestBackgroundingShouldCancelCurrentRequest(){letsut=Downloader()sut.download(byId:42){resultin// should not get a response}NotificationCenter.default.post(name:UIApplication.didEnterBackgroundNotification,object:nil)}
One step to help with the tests a little is to inject the notification center property and use a separate one in tests. That doesn’t solve the main problem though. What does?
Applying SRP
Following the Single Responsibility Principle, the responsibility of knowing how to subscribe to the notifications should be extracted into a separate entity:
There is the protocol to be used by clients and the standard implementation — extracted from the first Downloader. Now this notifier has only one responsibility: knowing how to subscribe to the notifications and calling a completion block when they arrive. The Downloader now knows only about the two changing application states, but not where they are coming from:
12345678910111213141516171819202122232425
classDownloader{typealiasCancellationHandle=()->()funchttpGet(_url:URL,completion:@escaping(Data?)->())->CancellationHandle{// the same as before}/// The handle of the currently running request if any.privatevarhandle:CancellationHandle?privatevarnotifier:ApplicationStateNotifierProtocolpublicinit(notifier:ApplicationStateNotifierProtocol){self.notifier=notifierself.notifier.onBackgrounding={self.handle?()self.handle=nil}}funcdownload(byIdid:Int,completion:@escaping(String?)->()){handle=httpGet(URL(string:"http://example.com/"+id.description)!){dataincompletion(data.flatMap{String(data:$0,encoding:.utf8)})}}}
Testing also becomes simpler by using a fake notifier where we can send the state events very easily:
123456789101112131415
classFakeApplicationStateNotifier:ApplicationStateNotifierProtocol{varonBackgrounding:Completion?varonForegrounding:Completion?}functestBackgroundingShouldCancelCurrentRequest(){letfakeNotifier=FakeApplicationStateNotifier()letdl=Downloader(notifier:fakeNotifier)dl.download(byId:42){resultin// should not get a response}fakeNotifier.onBackgrounding?()}
Improving the interactions
I prefer going one step forward and using FRP, for example with ReactiveSwift because it provides a cleaner and more uniform interface for events. First we create an enum to represent the two events we’re interested in and define a Signal that will send them (ReactiveCocoa provides a very convenient NotificationCenter.reactive.notifications(forName:) abstraction that creates Signals for notifications):
The signal accessed with UIApplication.reactive.activityStateSignal will send the proper state events for the two notifications. I’m using the const function (see also the Haskell’s Preludeconst function) to ignore the Notification object from the .notifications(forName:)’s signal and always return the specific state. Note that since this property returns a “cold” Signal, it will only send events when the state changes, but won’t send anything on subscribing.
123456789101112131415161718192021222324252627
classDownloader{typealiasCancellationHandle=()->()funchttpGet(_url:URL,completion:@escaping(Data?)->())->CancellationHandle{// the same as before}/// The handle of the currently running request if any.privatevarhandle:CancellationHandle?publicinit(activityStateSignal:Signal<UIApplication.ActivityState,NoError>){activityStateSignal.observeValues{activityStateinswitchactivityState{case.background:self.handle?()self.handle=nildefault:break}}}funcdownload(byIdid:Int,completion:@escaping(String?)->()){handle=httpGet(URL(string:"http://example.com/"+id.description)!){dataincompletion(data.flatMap{String(data:$0,encoding:.utf8)})}}}
The Downloader now accepts a Signal of the application state events and observes it. Testing it doesn’t require a special fake anymore, just create a compatible Signal manually:
12345678910
functestBackgroundingShouldCancelCurrentRequest(){let(activityStateSignal,activityStateObserver)=Signal<UIApplication.ActivityState,NoError>.pipe()letdl=Downloader(activityStateSignal:activityStateSignal)dl.download(byId:42){resultin// should not get a response}activityStateObserver.send(value:.background)}
Conclusion
If all the classes in the project were using a Signal-based or the ApplicationStateNotifierProtocol-based abstraction, they would be simpler and contain one fewer responsibility. And updating the notification names would require changes only in one place.