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 }) | |
} | |
} |
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.