Design pattern in Swift - Observers

During this year, I have blogged quite a bit about code architecture in Swift and I’ve realized that I didn’t explain much about which design pattern to use with it. In a series of coming posts, I will cover different design patterns, starting now with observer.

Observer design pattern is characterized by two elements:

  • A value being observed, notifying in some way all observers of a change.
  • An observer, subscribing to changes of that value

In this post, I’ll get two approaches using protocols and closures to see what’s the most handy.

Observer pattern with protocols

Starting first with the observable, I am going to define what are the condition to fill with a protocol approach. Quite simply, it requires a list of observers and the ability to add, remove one and notify them.

protocol ObservableProtocol : class {
    var observers : [ObserverProtocol] { get set }

    func addObserver(_ observer: ObserverProtocol)
    func removeObserver(_ observer: ObserverProtocol)
    func notifyObservers(_ observers: [ObserverProtocol])
}

On the other side, we need an observer capable of receiving a callback when the value changed. I’m adding an identifier to it for some duplicate safety later.

protocol ObserverProtocol {

    var id : Int { get set }
    func onValueChanged(_ value: Any?)
}

A key ingredient to avoid issues later is to bound the ObservableProtocol to a class. In case you implement it to a struct, and because structs are value typed, you might not have always the right values you want to work with.

Here is what my Observable class looks. I’ve made usage of generic type to make it more reusable.

class Observable<T> : ObservableProtocol {

    var value : T {
        didSet {
            self.notifyObservers(self.observers)
        }
    }

    internal var observers : [ObserverProtocol] = []

    init(value: T) {
        self.value = value
    }

    func addObserver(_ observer: ObserverProtocol) {
        guard self.observers.contains(where: { $0.id == observer.id }) == false else {
            return
        }
        self.observers.append(observer)
    }

    func removeObserver(_ observer: ObserverProtocol) {
        guard let index = self.observers.firstIndex(where: { $0.id == observer.id }) else {
            return
        }
        self.observers.remove(at: index)

    }

    func notifyObservers(_ observers: [ObserverProtocol]) {
        observers.forEach({ $0.onValueChanged(value)})
    }

    deinit {
        observers.removeAll()
    }
}

Finishing with a small test on Playground, mocking an e-commerce app where we’ve got one Cart instance observed by a ViewModel. We want to know whenever the number of items in cart changes.

class Cart {
    var numberOfItems : Observable<Int>

    init(numberOfItems: Int) {
        self.numberOfItems = Observable(value: numberOfItems)
    }

    func addItemToCart() {
        numberOfItems.value = numberOfItems.value + 1
    }
}

class ViewModel : ObserverProtocol {

    var id = 123
    func onValueChanged(_ value: Any?) {
        // we added / remove item to cart
        print("new numbers \(value)")
    }
}

let userCart = Cart(numberOfItems: 0)
let viewModel = ViewModel()
userCart.numberOfItems.addObserver(viewModel)

userCart.addItemToCart()
// print "new numbers Optional(11)"

Great, it’s working as expected with this simple example.

If this works great, depending of your usage, you still need to handle retain cycle depending of observable and observers. Last point to note, the code will be executed whenever called. If you have a multi-threading system (background vs main), you need to be careful, especially if handle UI changes with that system.

Finally, one function to observe them all onValueChanged is pretty limited if you have multiple values observed to know which one to handle. Let’s see if we can get a swift friendly alternative.


Observer pattern with closures

Instead of using a specific function for every observer, like a delegate would do, let’s see if a closure as parameter can tidy the observer.

class Observable<T> {

    typealias CompletionHandler = ((T) -> Void)

    var value : T {
        didSet {
            self.notifyObservers(self.observers)
        }
    }

    var observers : [Int : CompletionHandler] = [:]

    init(value: T) {
        self.value = value
    }

    func addObserver(_ observer: ObserverProtocol, completion: @escaping CompletionHandler) {
        self.observers[observer.id] = completion
    }

    func removeObserver(_ observer: ObserverProtocol) {
        self.observers.removeValue(forKey: observer.id)        
    }

    func notifyObservers(_ observers: [Int : CompletionHandler]) {
        observers.forEach({ $0.value(value) })
    }

    deinit {
        observers.removeAll()
    }
}

The idea is to keep a collection of closures to execute instead of observers. It helps to reduce risk of strong references, I chose to keep it into dictionary in case we want to remove it by their identifiers.

class ViewModel : ObserverProtocol {
    var id = 123
}

let userCart = Cart(numberOfItems: 0)
let viewModel = ViewMode()

userCart.numberOfItems.addObserver(viewModel) { newNumber in
    print("Let's update the counter display")
}

userCart.emptied.addObserver(mommy) { _ in
    print("Let's show a different UI when cart emptied")
}

userCart.addItemToCart()
// print **Let's update the counter display**
userCart.emptyCart()
// print **Let's show a different UI when cart emptied**

In conclusion, we’ve seen how to implement a simple observer in Swift, using protocols and closures to notify changes of the value being observed.

Observer design pattern is very handy when you are facing a one-to-many relationship between classes and you want to broadcast changes all at once. However, like any design patterns, it comes with some limitations and it’s up to the developers to balance it and find the best solution, keeping a clean code and avoiding memory leaks.

Thanks for reading!


Where to go from here

© 2023 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

Software Engineer 🇫🇷, writing about career development, mobile engineering and self-improvement

ShopBack 💰

Singapore 🇸🇬