Observers as an alternative to delegates

Recently I started studying Ray Wenderlich’s tutorial book RxSwift. Early on, the book suggests using Rx as an alternative to delegates and their associated protocols. I though, great, but, for the task at hand, that’s a rather heavy handed solution. (Yes, I know it is just an example designed to teach me the basics.) So I tried to imagine how I could achieve something similar to the functionality they built for their initial examples, without the overhead of importing a big library like RxSwift.

It quickly occured to me that the concept of a light-weight observable was a good avenue to explore. So I jotted down the following:

protocol Observable {
    associatedtype Observation

    var observer: Observer<Observation>? { get set }
}

Objects that want to be observable should conform to the Observable protocol. The thing they offer for observation is defined by typealiasing its associated type Observation. They must also implement a property holding an observer which will be triggered whenever the observable wants to convey an update.

Ideally, I would want an observer to be any object conforming to an Observer protocol. Like this:

protocol Observer {
    associatedtype Observation

    func observe(_ thing: Observation) {
        // …handle thing…
    }
}

Just like in the delegate pattern, the observer—the object interested in observing an Observable—would pass itself as the observer, defining a function named observe, to be called by the Observer, and which processes the thing it receives.

But you can’t do that in Swift. Protocols with associated types can only be used as generic constraints, not as first class types. If you try to implement this, the compiler will very kindly present you with an error:

class MyClass: Observer {
    typealias Observation = Thing

    func observe(_ thing: Observation) {
        // …handle thing…
    }
}

// error: Protocol 'Observer' can only be used as a generic constraint
// because it has Self or associated type requirements.

So, instead, we use an intermediate object with a generic type:

struct Observer<Observation> {
    let handler: (Observation) -> Void

    func observe(_ thing: Observation) {
        handler(thing)
    }
}

The Observable object defines an observer property, to be called into action when something is to be oserved. The observing object creates anObserver and passes it to the Observable. The Observer holds a handler, defined by the object that is requesting the observation. To be able to write the handler we need to know the type of Observation, the actual thing we are observing. To that end we typealias Observation in the Observable object to whatever we offer for observation:

class ObservableViewController: UIViewController, Observable {
    typealias Observation = Int

    var observer: Observer<Observation>?
	
}

In this case observing object has to provide an observer whose generic type is an Int. You could also typealias Observation = Void, and provide an observer whose handler doesn’t take any arguments.

But here is a very cool thing you can do: rather than define some delegate protocol, which, in (pure) Swift, can’t have optional functions, we can use an enum that contains all the actions we want to offer for observation, and, since it is an enum, on the observing side, we do not need to handle all cases. Only the ones we are interested in. Here’s a trivial example:

class ObservableViewController: UIViewController, Observable {
    enum Action {
        case button1Tapped
        case button2Tapped(message: String)
    }

    typealias Observation = Action

    var observer: Observer<Observation>?
	
}

This has a downside, of course, since the observable might want to require certain actions to be implemented. But this construct can be a handy tool in our toolbox, for when we want to define a set of optional functions.

When the observable has something to convey to the observer, it will call its handler: observer?.observe(.button1Tapped), or observer?.observe(.button2Tapped(message: "Remote action on button 2")).

How does that look on the observing side?

class ObservingViewController: UIViewController {
    var history = [String]()
    
    @IBAction func showObservationViewController() {
        guard let vc = setupObservableViewController() else { return }
        self.navigationController?.pushViewController(vc, animated: true)
    }

    private func setupObservableViewController() -> UIViewController? {
        guard let vc = self.storyboard?.instantiateViewController(withIdentifier: "ObservableViewController") as? ObservableViewController else { return nil }

        vc.observer = Observer<ObservableViewController.Action> { [weak self] (action) in
            guard let self = self else { return }

            switch action {
            case .button1Tapped:
                self.history.append("\(self.history.count + 1) Remote action on button 1")
            case .button2Tapped(let message):
                self.history.append("\(self.history.count + 1) " + message)
            }
        }

        return vc
    }
}

When we set up the observable view controller, we provide it an Observer, including a handler that receives the Observable’s action and switches over it, handling the cases it is interested in. Then we push the observable view controller and, whenever it calls our handler, our code processes the changes.

This pattern can also be used to turn UIControls into lightweight reactive objects. You can read all about that in the next post.

You can find the full source code here (including a MultiObservable protocol, for observables that can service multiple observers, and a few observable UIControl derivatives).

Published on 7 July, 2019