The Observer Design Pattern in Swift

The Observer Design Pattern in Swift

A beginner's guide to design patterns for iOS developers

·

5 min read

In this article, we will learn what is the Observer design pattern and how to use it.

The use case

Let’s say we have a weather station that regularly takes measures of temperature and humidity:

struct WeatherData: Equatable {
    var temperature: Int
    var humidity: Int
}

class WeatherStation {

    private var timer = Timer()
    private var lastMeasurement: WeatherData? = nil

    init() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] _ in
            self?.measureWeather()
        })
    }

    private func measureWeather() {
        // Simulate new data
        let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
        if newData != lastMeasurement {
            self.lastMeasurement = newData
        }
    }
}

And two screens that are connected to the station, one displaying temperature, the other one displaying humidity:

struct TemperatureScreen {
    func display(temperature: Int) {
        print("Temperature is now \(temperature)")
    }
}

struct HumidityScreen {
    func display(humidity: Int) {
        print("Humidity is now \(humidity)")
    }
}

→ How would we allow the several screens to update their information when the weather changes?

Polling the data?

One solution that can come to mind would be that a screen can regularly ask the weather station for new data, for instance, every 100 milliseconds.

Unfortunately, this won’t be very efficient. Here is why:

  • If the weather doesn’t change for a minute, all screens would ask a lot of times the weather station for nothing, wasting energy.

  • The more screens you have, the more the weather station will receive simultaneous requests, which risks to overload the station.

So instead of making the screens asking the new data, let’s try to see the solution in the other direction: what if it’s the weather station's responsibility to inform the screens of new data?

The observer pattern

The observer pattern is based on two elements:

  • The observable (also called the subject): the object that is observed, the weather station.

  • The observers (also called subscribers): they observe the observable. The screens observe the weather station.

The responsibility of the observable is to keep a list of its observers and notify them if needed. Then, the observers can register or unregister themselves from the observable.

We can see that like a newsletter: a person can subscribe to a newsletter, and the newsletter system will notify each subscriber when new information is available.

Let’s see what the Swift code would look like.

The code

First, we have to define the two protocols: the observable and the observer.

Observable

The observable is composed of:

  1. A list of subscribers, of the type WeatherStationSubscriber (we will come to that).

  2. A register method, that we can call to add subscribers. An unregister method should also be added to stop a subscription.

  3. A notify method that will be used to notify the subscribers.

protocol WeatherStationObservable {
    // 1
    var subscribers: [WeatherStationSubscriber] { get }
    // 2
    func register(_ subscriber:  WeatherStationSubscriber)
    // 3
    func notify(_ newData: WeatherData)
}

Let’s apply that to our weather station:

  1. We add the WeatherStationObservable protocol conformance to our WeatherStation.

  2. We declare the list of subscribers.

  3. We implement the register method, by adding the new subscriber to the list of subscribers. Note that you may want to prevent the same observer to subscribe multiple times, by checking before if the subscriber is already in the subscribers, or by using a Set instead of a list.

  4. Finally, we implement the notify method: we just have to iterate through each of the subscribers and notify them with the new data.

class WeatherStation: WeatherStationObservable { // 1

    // previous code ...

    // 2
    var subscribers: [WeatherStationSubscriber] = []

    // 3
    func register(_ subscriber: WeatherStationSubscriber) {
        subscribers.append(subscriber)
    }

    // 4
    func notify(_ newData: WeatherData) {
        for subscriber in subscribers {
            // TODO: notify subscriber here !
        }
    }
}

Now we just have to modify the measureWeather method to call the notify method:

private func measureWeather() {
    // Simulate new data
    let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
    if newData != lastMeasurement {
        self.lastMeasurement = newData
        self.notify(newData)
    }
}

Observer

The observer (here we call it subscriber) is even easier: it just has to implement a method that can be called by the subscriber to notify it when new data is available:

protocol WeatherStationSubscriber {
    func onNotified(_ newData: WeatherData)
}

Let’s apply that to one of our screens:

  • Here the onNotified method will just send the temperature information to the display method.
struct TemperatureScreen: WeatherStationSubscriber {

    // Previous code...

    func onNotified(_ newData: WeatherData) {
        display(temperature: newData.temperature)
    }
}

Usage

Now let’s see how to use that in practice:

  1. We create the weather station.

  2. We create the screens.

  3. We register the screens to the weather station.

// 1
let weatherStation = WeatherStation()

// 2
let temperatureScreen = TemperatureScreen()
let humidityScreen = HumidityScreen()

// 3
weatherStation.register(temperatureScreen)
weatherStation.register(humidityScreen)

The final code

import Foundation

struct WeatherData: Equatable {
    var temperature: Int
    var humidity: Int
}

protocol WeatherStationObservable {
    var subscribers: [WeatherStationSubscriber] { get }

    func register(_ subscriber:  WeatherStationSubscriber)
    func notify(_ newData: WeatherData)
}

protocol WeatherStationSubscriber {
    func onNotified(_ newData: WeatherData)
}

class WeatherStation: WeatherStationObservable {

    private var timer = Timer()
    private var lastMeasurement: WeatherData? = nil

    var subscribers: [WeatherStationSubscriber] = []

    init() {
        self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true, block: { [weak self] _ in
            self?.measureWeather()
        })
    }

    private func measureWeather() {
        print("Measuring...")
        let newData = WeatherData(temperature: Int.random(in: 0..<10), humidity: Int.random(in: 0..<10))
        if newData != lastMeasurement {
            self.lastMeasurement = newData
            notify(newData)
        }
    }

    func register(_ subscriber: WeatherStationSubscriber) {
        subscribers.append(subscriber)
    }

    func notify(_ newData: WeatherData) {
        for subscriber in subscribers {
            subscriber.onNotified(newData)
        }
    }
}

struct TemperatureScreen: WeatherStationSubscriber {
    func onNotified(_ newData: WeatherData) {
        display(temperature: newData.temperature)
    }

    func display(temperature: Int) {
        print("Temperature is now \(temperature)")
    }
}

struct HumidityScreen: WeatherStationSubscriber {
    func onNotified(_ newData: WeatherData) {
        display(humidity: newData.humidity)
    }

    func display(humidity: Int) {
        print("Humidity is now \(humidity)")
    }
}

let weatherStation = WeatherStation()

let temperatureScreen = TemperatureScreen()
let humidityScreen = HumidityScreen()

weatherStation.register(temperatureScreen)
weatherStation.register(humidityScreen)

Now if we run that code in a playground, we can see each time the weather station gets new data, it will notify the screens that will display the new weather information:

Measuring...
Temperature is now 7
Humidity is now 3

Measuring...
Temperature is now 1
Humidity is now 9

Measuring...
Temperature is now 4
Humidity is now 8

Wrap up

In this article, we learned what is the observer design pattern and how to use it.

I hope this article has been helpful to you. If you have any questions or feedback about this article, don’t hesitate to contact me on Twitter!