Swift Talk # 27

Typed Notifications (Part 1)

This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

A lightweight generic wrapper around Foundation's notification API lets us avoid boilerplate code and provides a type-safe API.

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.

Resources

  • Playground

    Written in Swift 3

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes