Making a property observable from outer scope using generic class Observable

We will look into how to make a property observable using a separate class managing the observers. It is an alternative and simple way of observing property changes without using ReactiveSwift, Key-Value Observing or anything else similar. It can be an excellent glue between Model and View Model in MVVM or between View and Presenter when using VIPER architecture.

Creating a class Observable

final class Observable<T> {
init(_ value: T) {
self.value = value
}
var value: T {
didSet {
changeHandlers.forEach({ $0.handler(value) })
}
}
typealias ChangeHandler = ((T) -> Void)
private var changeHandlers: [(identifier: Int, handler: ChangeHandler)] = []
/**
Adds observer to the value.
- parameter initial: The handler is run immediately with initial value.
- parameter handler: The handler to execute when value changes.
- returns: Identifier of the observer.
*/
@discardableResult func observe(initial: Bool = false, handler: @escaping ChangeHandler) -> Int {
let identifier = UUID().uuidString.hashValue
changeHandlers.append((identifier, handler))
guard initial else { return identifier }
handler(value)
return identifier
}
/**
Removes observer to the value.
- parameter observer: The observer to remove.
*/
func removeObserver(_ observer: Int) {
changeHandlers = changeHandlers.filter({ $0.identifier != observer })
}
}
view raw Observable.swift hosted with ❤ by GitHub

The class Observable holds a value what can be of any type. Now when the value is store by the class itself we can use Swift’s property observer and then calling change handlers. This allows creating an object with observable properties and observing those properties from other objects. Moreover, it is possible to remove any of the added observers.
Let’s take a look on an example of class “Pantry” what has a property holding array of jams. In the example we will add two observers: one reacting to changes and the other one what will also react to the initial value. When one of the observer is removed and the array of jams changes, only one of the observers is triggered.

final class Pantry {
let jams = Observable([Jam(flavour: .apple)])
func add(jam: Jam) {
jams.value.append(jam)
}
}
struct Jam {
enum Flavour: String {
case apple, orange
}
let flavour: Flavour
init(flavour: Flavour) {
self.flavour = flavour
}
}
let pantry = Pantry()
print("Adding count and contents observers.")
let observer = pantry.jams.observe { (jams) in
print("Pantry now has \(jams.count) jars of jam.")
}
pantry.jams.observe(initial: true) { (jams) in
let contents = jams.map({ $0.flavour.rawValue }).joined(separator: ", ")
print("Jams in pantry: \(contents)")
}
print("Adding jam to pantry.")
pantry.add(jam: Jam(flavour: .orange))
print("Removing count observer.")
pantry.jams.removeObserver(observer)
print("Adding jam to pantry.")
pantry.add(jam: Jam(flavour: .apple))
/*
Adding count and contents observers.
Jams in pantry: apple
Adding jam to pantry.
Pantry now has 2 jars of jam.
Jams in pantry: apple, orange
Removing count observer.
Adding jam to pantry.
Jams in pantry: apple, orange, apple
*/

Today we learned how to very easily make a property observable from outer scope of the object owning the property.
Thank you for reading.

Download ObservableProperty playground.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s