Callbacks, Part 1: Delegate, NotificationCenter, and KVO
This article kicks off the series of posts about callback techniques in Cocoa, their comparison & benchmarking. If you’re familiar with the concepts of delegation
, NotificationCenter
and Key-Value Observing
, you might want to skip the introduction and go straight to the Pros & Cons of each. Happy reading!
Callbacks, Part 2: Closure, Target-Action, and Responder Chain
Callbacks, Part 3: Promise, Event, and Stream (Functional Reactive Programming)
delegate
delegate
is the most common type of loose connection between entities is Cocoa, but nonetheless, this approach can be used in other languages and platforms as this is basically a delegation design pattern.
The idea is simple - instead of interacting with an object through its real API we can instead omit the details of which exact type we talk to and instead use protocol
with a subset of methods that we need. We define the protocol ourselves and only list those methods we’re going to call at our opponent. A real object then would need to implement this protocol, and probably other methods - but we won’t know about it - and that’s good! Loose coupling, remember?
Let’s see an example. Suppose we have a Pizzeria, with a man who makes the pizza (pizzaiolo), and a customer who orders the pizza. The pizzaiolo
should not know all the details about who exactly is the customer
(could it be Queen Elizabeth, or Snoop Dogg, or just a dog…), the only thing that matters is the customer’s ability to take the pizza when it is ready. This is how we would implement this problem with delegate
in Swift:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// The protocol the Pizzaiolo is using for "calling back" to customer
protocol PizzaTaker {
func take(pizza: Pizza)
}
class Pizzaiolo {
// We store a reference to a PizzaTaker rather than to a Customer
var pizzaTaker: PizzaTaker?
// The method a customer can call to ask Pizzaiolo to make a pizza
func makePizza() {
...
// As soon as the pizza is ready the `onPizzaIsReady` function is called
}
private func onPizzaIsReady(pizza: Pizza) {
// Pizzaiolo provides the pizza to a PizzaTaker
pizzaTaker?.take(pizza: pizza)
}
}
// Class Customer implements the protocol PizzaTaker - the only requirement
// which allows him to get a pizza from Pizzaiolo
class Customer: PizzaTaker {
// Some details related to Customer class which Pizzaiolo should not know about
var name: String
var dateOfBirth: Date
// Implementation of the PizzaTaker protocol
func take(pizza: Pizza) {
// yummy!
}
}
Advantages of using delegate
- Loose coupling. Indeed, with delegate pattern, we have interacting parties decoupled from each other without exposure of who exactly they are. We can easily implement the protocol
PizzaTaker
in other classDumpster
and make all fresh pizzas end up in a trash can instead of being eaten, withoutPizzaiolo
even knowing something has changed. - IDE (such as Xcode) is able to check the connection for correctness with static analysis. This is a great advantage because connections between objects can break when you refactor things, and IDE will point you out the problem even before you try to build the project.
- Ability to get a non-
void
result from calling thedelegate
. Unlike many other callback techniques, with the delegate, you can not only notify but also ask for data. In Cocoa, you can often seeDataSource
protocols, which stand exactly for this purpose. - Speed. Since calling a method on a delegate is nothing more than the direct call of a function, with
delegate
you can achieve absolutely best performance among other callback techniques, which have to spend time on more sophisticated delivery of the call.
Disadvantages of using delegate
- Single recipient. Even with our Pizzeria example above,
pizzaiolo
is simply unable to serve multiple pizzas at a time for more than onecustomer
, and moreover if anothercustomer
comes while the pizza for the previouscustomer
is still being cooked, things will totally mess up: the newcustomer
will overwrite thepizzaTaker
reference onpizzaiolo
to himself and the previouscustomer
will never get his pizza! Awful service… - Distribution of closely related business logic (aka low cohesion). Since all the callback methods have to be implemented as separate functions inside the recipient we cannot use anonymous functions and get the action and the callback for the action nested near each other. So sometimes it’s hard to reason about under which conditions the callback is called.
- Cumbersome infrastructure. In order to use
delegate
we need to do quite a few steps:- define a new protocol
- define a weak property for the delegate
- implement the protocol in the target type
- assign the delegate reference.
- [Obj-C] check if the delegate can handle the message with
respondsToSelector:
- Promotes the emergence of Massive View Controller. Because of the popularity of this pattern in Cocoa your classes quickly become a convention of delegates if you do nothing so split them out.
NotificationCenter
NotificationCenter
(or NSNotificationCenter
for Objective-C) is a class in Cocoa which provides the publish – subscribe functionality, and as you can guess from its name, it deals with Notifications. The notifications it sends around are objects of Notification
class, which can carry an optional payload, but more often are used just as notices. The NotificationCenter
itself is a data bus, it doesn’t send notifications on its own, only when someone askes it to send one. The main feature of this pattern is that the sender
and recipients
(there can be many) do not talk directly, like with delegate
pattern. Instead, they both talk to NotificationCenter
- the sender calls method postNotification
on the NotificationCenter
to send a notification, while recipients opt-in for receiving the notifications by calling addObserver
on NotificationCenter
. They can later opt-out with removeObserver
. Important note though is that NotificationCenter
does not store notifications for future subscribers - only present subscribers receive the notifications.
The NotificationCenter
also provides a global access point defaultCenter
, however, this class is not implemented as a pure Singleton - you can create your own instances of NotificationCenter
(and you probably should).
Implementation of the same case with Pizzeria:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// First step is to declare new notification type - to be identified by sender and recipients
extension NSNotification.Name {
static let PizzaReadiness = NSNotification.Name(rawValue: "pizza_is_ready")
}
class Pizzaiolo {
func makePizza() {
...
}
private func onPizzaIsReady(pizza: Pizza) {
// Pizzaiolo notifies all interested parties that the pizza is ready:
NotificationCenter.default.post(name: NSNotification.Name.PizzaReadiness, object: self, userInfo: ["pizza_object" : pizza])
}
}
class Customer {
// If a customer wants to get a pizza he needs to register as an observer at NotificationCenter
func startListeningWhenPizzaIsReady() {
// A customer subscribes for all notifications of type NSNotification.Name.PizzaReadiness
NotificationCenter.default.addObserver(self, selector: #selector(pizzaIsReady(notification:)), name: NSNotification.Name.PizzaReadiness, object: nil)
}
// The customer should opt-out of notifications when he's not interested in them anymore
func stopListeningWhenPizzaIsReady() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.PizzaReadiness, object: nil)
}
dynamic func pizzaIsReady(notification: Notification) {
if let pizza = notification.userInfo?["pizza_object"] as? Pizza {
// Got the pizza!
}
}
}
Advantages of using NotificationCenter
- Multiple recipients.
NotificationCenter
transparently delivers aNotification
to all subscribers, could there be one, or thousand, or none - this class will take care of the delivery for you. What’s also notable - thesender
cannot know how many subscribers he has. Not to say it’s good or bad, it’s just how this tool is designed. - Loose coupling. When using
NotificationCenter
the only thing that couplessender
andreceivers
together is the name of the notification they use for their communication. If you include a payload to the notification, both parties would need to have symmetric boxing and unboxing of the data. - Global access point. When you don’t need (or just don’t care about) the dependency injections in your code, the singleton-style method
defaultCenter
allows you to connect two random objects together with ease.
Disadvantages of using NotificationCenter
- Global access point. Yes, this is also a disadvantage. Any globally accessible stuff breaks the testability of your code and even
singleton
as a design pattern many people consider as an anty-pattern. - Inability to step-in with a debugger. The only way you can debug
NotificationCenter
is by placing breakpoints - Non-obvious control flow. If you are trying to understand the business logic of the program and see a place where a
Notification
is sent - the only way to continue exploration is by finding all the recipients manually with a text search in the entire project - because they can be anywhere! - Transferring data is very error-prone because of boxing and unboxing with a
Dictionary
(NSDictionary
). Even when used from type-safe Swift, the compiler cannot check the types as well as the structure of theDictionary
. - Recipients must unsubscribe when they are about to deallocate or the app may crash. This requirement has been removed only in iOS 8, but this will haunt iOS developers in their nightmares for years.
- The sender cannot get a non-
void
result, as opposed to thedelegate
andclosure
- Third-party libs may rely on the same notifications as your code and interfere with each other. Great example is the
NSManagedObjectContextDidSaveNotification
from CoreData framework - every party should properly handle this notification or the app can crash. - There is no control over who is eligible of sending the particular notification. A junior developer on your team may come up to send a system notification like
UIApplicationWillEnterForegroundNotification
to fix a weird bug in his code, and the entire system can get screwed up. Funny, huh? - When overused,
NotificationCenter
can turn your project into hell because of the previous points
Key Value Observing
There is a dedicated post I’ve written about KVO in Swift 5 and Objective-C.
KVO
is a traditional observer pattern built-in any NSObject
out of the box. With KVO
observers can be notified of any changes of a @property
values. It leverages Objective-C runtime for automated notifications dispatch, and because of that for Swift
class, you’d need to opt into Objective-C dynamism by inheriting NSObject
and marking the var
you’re going to observe with modifier dynamic
. The observers should also be NSObject
descendants because this is enforced by the KVO
API.
Sample code for Key-Value Observing
a property value
of the class ObservedClass
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ObservedClass : NSObject {
@objc dynamic var value: CGFloat = 0
}
class Observer {
var kvoToken: NSKeyValueObservation?
func observe(object: ObservedClass) {
kvoToken = object.observe(\.value, options: .new) { (object, change) in
guard let value = change.new else { return }
print("New value is: \(value)")
}
}
deinit {
kvoToken?.invalidate()
}
}
Advantages of using Key-Value Observing
- Observation pattern in a few lines of code. Normally you would need to implement it yourself, but in
Cocoa
you have this feature coming standard for everyNSObject
. - Multiple observers - there is no limitation on the number of subscribers.
- There is no need to change the source code of the class under observation
- Because of the aforementioned you can observe objects of any class (including those from system frameworks, to which sources we don’t have access thus cannot modify)
- Very low coupling (connascence of name) - the observed party doesn’t even know it’s being observed, the observing party’s knowledge is bounded by the name of the
@property
- Notification can be configured to deliver not only the most recent value of the observed
@property
but also the previous value.
Disadvantages of using Key-Value Observing
- One of the worst APIs across Cocoa. This point could be easily broken up in several - so bad it is. Good for us there are nicer alternative implementations of the
KVO
. - keyPath used for subscription is a string, and it cannot be statically verified. Luckily this has a solution in Swift (
#keyPath
directive), but in Objective-C, if the observed party changes the name of the@property
- there won’t be a compiler warning about this, so the app will just crash in runtime. - Each observer has to explicitly unsubscribe on
deinit
- otherwise crash is unavoidable. - We have to call the
super
implementation ofobserveValueForKeyPath
callback function to make sure we don’t break the implementation of the superclass. - Relatively slow performance. Even when compiled with
-Os
optimization, forObjective-C
oneKVO
notification is 30 times slower than a function call, forSwift
it is 200 times slower. I have used this project for benchmarking.
Continuation: Callbacks, Part 2: Closure, Target-Action, and Responder Chain
Let's connect!
Subscribe to RSS feed or follow my Twitter for the new articles alerts. And let's connect on LinkedIn as well!