00:25 Let's talk about notifications. Everybody uses Notification
s
(they used to be called NSNotification
s). 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.