00:25 Let's talk about notifications. Everybody uses Notifications
(they used to be called NSNotifications). The system sends notifications we're
interested in, and sometimes we want to send our own notifications (we use
NotificationCenter for this).
00:43 Everybody has written this kind of code; it's a mechanism that's
used a lot in Cocoa/UIKit. In this API, you often work with a userInfo
dictionary that's on Notification. You always have to refer to the
documentation to see which keys and values are present in this dictionary.
01:09 Often, this code is spread out throughout your application. For
example, when dealing with the .UIKeyboardDidShow and .UIKeyboardDidHide
notifications, you have to go into the documentation to find the right keys for
accessing values from the userInfo dictionary. Once you have the keys, you
have to access the values and force cast them to the right types. This code
usually ends up in your view controller, which isn't the best place.
01:44 Reading the userInfo always involves some force casting, and
it'd be nice to package this code up in a central place so that we can force
cast once per notification instead of once per observer. Doing so allows us to
reuse the conversion code everywhere.
02:02 Here we have some sample code in which we use a notification
center and add an observer for the
PlaygroundPageNeedsIndefiniteExecutionDidChangeNotification. We'll ignore the
queue and object parameters in this episode and discuss them in a future
episode. The final parameter is a completion function that gets called whenever
a notification arrives:
let center = NotificationCenter.default
center.addObserver(forName: Notification.Name("PlaygroundPageNeedsIndefiniteExecutionDidChangeNotification"), object: nil, queue: nil, using: { note in
print(note.object)
print(note.userInfo)
})
02:43 If we set needsIndefiniteExecution on the current playground
page, we'll see that as the notification's object, we get a PlaygroundPage,
and in the dictionary, we get the new value of needsIndefiniteExecution.
A NotificationDescriptor Struct
02:55 We'll wrap these two pieces of information up in a nicer way.
We'll start by defining a struct, NotificationDescriptor. This struct will
have two properties — a name, of type Notification.Name; and a function,
convert, which takes a notification and produces an A (the generic
parameter):
struct NotificationDescriptor<A> {
let name: Notification.Name
let convert: (Notification) -> A
}
03:55 Now we can use the struct to describe this playground page
notification and how we extract the information we're interested in from that
notification. We'll create a new value of the struct and use the memberwise
initializer. We'll pass in the notification name and write the convert
function. Inside that function, we'll do the force casting. We get the page
out of the notification's object and force cast it. Likewise, we get the
needsIndefiniteExecution value out of the userInfo dictionary and force cast
it to a Bool:
let playgroundNotification = NotificationDescriptor(name: Notification.Name("PlaygroundPageNeedsIndefiniteExecutionDidChangeNotification"), convert: { note -> (PlaygroundPage, Bool) in
let page = note.object as! PlaygroundPage
let needsIndefiniteExecution = note.userInfo!["PlaygroundPageNeedsIndefiniteExecution"] as! Bool
return (page, needsIndefiniteExecution)
})
06:04 This is nice, but we can't really use our notification descriptor
yet. We need to find a way to observe notifications, given the descriptor. We'll
add an addObserver method to the notification center, similar to the existing
addObserver(:object:queue:using:). We'll do this in an extension, and instead
of taking a name, the method gets a descriptor as the parameter. The block
parameter is now of type (A) -> () instead of (Notification) -> ():
extension NotificationCenter {
func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, using block: @escaping (A) -> ()) -> NSObjectProtocol {
return addObserver(forName: d.name, object: nil, queue: nil, using: { note in
block(d.convert(note))
})
}
}
08:10 To see if the code works, we'll add an observer using our
playgroundNotification descriptor and print the result. Now, instead of a
notification, we get the playground page and the boolean value. We only get the
information we care about; we don't have to worry about parsing the
notification's userInfo. Putting this in your view controller will clean up
your code a lot, and you can reuse the playgroundNotification descriptor. It
might feel a bit weird that it's a global variable, but we'll discuss this in a
future episode:
let token = center.addObserver(forDescriptor: playgroundNotification, using: {
print($0)
})
Refactoring
09:15 There are still some other refactoring steps we can take to
improve the code. Currently, we return a tuple, and it'd be nicer to have a
struct value with named properties. To do that, we'll introduce a payload struct
for this specific notification, called PlaygroundPagePayload. In the payload,
there'll be a playground page and the needsIndefiniteExecution boolean. We can
modify our descriptor to return a value of PlaygroundPagePayload instead of a
tuple:
struct PlaygroundPayload {
let page: PlaygroundPage
let needsIndefiniteExecution: Bool
}
let playgroundNotification = NotificationDescriptor<PlaygroundPayload>(name: Notification.Name("PlaygroundPageNeedsIndefiniteExecutionDidChangeNotification"), convert: { note in
let page = note.object as! PlaygroundPage
let needsIndefiniteExecution = note.userInfo!["PlaygroundPageNeedsIndefiniteExecution"] as! Bool
return PlaygroundPayload(page: page, needsIndefiniteExecution: needsIndefiniteExecution)
})
10:52 In the console, we now see that the struct value gets printed
instead of the tuple, which is a bit nicer to work with.
11:05 We can take another refactoring step and move the parsing code
into the struct. We can write an extension on the PlaygroundPagePayload and
add an initializer that takes a Notification:
extension PlaygroundPayload {
init(note: Notification) {
page = note.object as! PlaygroundPage
needsIndefiniteExecution = note.userInfo!["PlaygroundPageNeedsIndefiniteExecution"] as! Bool
}
}
let playgroundNotification = NotificationDescriptor<PlaygroundPayload>(name: Notification.Name("PlaygroundPageNeedsIndefiniteExecutionDidChangeNotification"), convert: PlaygroundPayload.init)
11:59 We should probably make the initializer fileprivate, or
something similar, because it's only safe when dealing with this specific
Notification instance.
Removing Observers
12:10 One thing we've omitted so far is the fact that our addObserver
method should return a token for deregistering, just like the original API. The
token is of type NSObjectProtocol, and we can simply return whatever the
addObserver method returns. We can use this token to remove observers:
extension NotificationCenter {
func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, using block: @escaping (A) -> ()) -> NSObjectProtocol {
return addObserver(forName: d.name, object: nil, queue: nil, using: { note in
block(d.convert(note))
})
}
}
13:14 To stop receiving notifications, we'll have to call
removeObserver on the notification center:
let token = center.addObserver(forDescriptor: playgroundNotification, using: { print($0) })
center.removeObserver(token)
13:22 However, it's easy to forget this in regular code, and it's not
very robust against refactoring. If you add your observers in one place, you'll
often have to make sure to update the removeObserver as well. There's a nice
trick to make this a little bit easier, especially when adding observers in view
controllers. We'll make use of deinit to hook up the lifetime of an observer
to the lifetime of an object. This is often what you'll end up doing anyway.
14:20 We'll add a class, Token, in which we'll store the token and the
notification center that the token belongs to. In the deinit, we can simply
remove the observer from the center:
class Token {
let token: NSObjectProtocol
let center: NotificationCenter
init(token: NSObjectProtocol, center: NotificationCenter) {
self.token = token
self.center = center
}
deinit {
center.removeObserver(token)
}
}
15:05 Now, as long as the Token instance exists, the observer won't be
removed, and once the Token instance becomes nil, the observer will get
removed. Let's see how we can change our code to use our Token class:
extension NotificationCenter {
func addObserver<A>(forDescriptor d: NotificationDescriptor<A>, using block: @escaping (A) -> ()) -> Token {
let t = addObserver(forName: d.name, object: nil, queue: nil, using: { note in
block(d.convert(note))
})
return Token(token: t, center: self)
}
}
var token: Token? = center.addObserver(forDescriptor: playgroundNotification, using: { print($0) })
token = nil
16:00 Usually, you wouldn't manually set the token to nil, but it'd
happen once the view controller gets deallocated. If you store the token
property on your view controller, once the view controller goes away, the
token will be set to nil and the corresponding deinit will get called.
This is especially important if you use the block-based API, because if you
register an observer with the target/action pattern, it's no longer necessary to
deregister the observer yourself.
16:48 Interestingly, the NotificationDescriptor is very similar to
the Resource struct in episode
#1, though instead of a URL,
we have a name, and instead of a parse function, we have a convert function.
It's a useful pattern when you're working with untyped APIs and want to make
them type safe.
17:26 We can also extend this pattern. Sometimes, you want to post your
own notifications as well, and you can build on top of this so that you don't
just decode system notifications, but also encode your own data when posting.
Additionally, there are several other things we could build on top of this, and
we'll talk more about them in the next episode.