In this post we will take a look how can we implement alternative mechanism for observing model changes and reacting to those changes using @propertyWrapper‘s introduced in Swift 5 and make quick comparison against existing and popular approaches.
First let’s take a quick overview of existing popular solutions and quickly analyse them.
1. Using ‘didSet’
class AnyObjectType {
var property: String {
didSet { /* do something */ }
}
}
struct AnyStructType {
var property: String {
didSet { /* do something */ }
}
}
let anyOject = AnyObjectType(propery: "anyString")
anyOject.property = "newAnyString"
var anyStruct = AnyStructType(property: "anyString")
anyStruct.property = "newAnyString"
Although this solution works both on structs and classes but has one major downside. When we want to react on changes we need to add concrete implantation into our class / struct. This forces us to make strong dependencies to our implementations inside those models.
How does this scale?
I my opinion not so well. Why? Let’s say that on ‘didSet’ we update our model to storage. After that we decide to add some logging as well and maybe something else too.
What starts to happening is that we introduce more and more dependencies into our model. And every time we make changes to those dependencies we need to update our model. Even our git history becomes more messy, since we see changes in our model, but model has nothing to do with our dependencies. Not to mention that our model tests brake as well constantly.
2. KVO
import Foundation
class AnyObjectType: NSObject {
@objc
dynamic var property: String
}
let anyOject = AnyObjectType(property: "anyString")
anyOject.observe(\.property) { (object, change) in
/* Do something */
}
anyOject.property = "newAnyString"
We could use KVO but, as we can see it forces us to use NSObject which unfortunately automatically removes option to use structs and use classes only. So this solution doesn’t fit our needs.
3. Delegates
protocol AnyObjectTypeDelegate {
func propery1Changed(_ property: String)
func propery2Changed(_ property: Int)
}
class AnyObjectType {
var delegate: AnyObjectTypeDelegate
var property1: String {
didSet {
delegate.propery1Changed(property1)
}
}
var property2: Int {
didSet {
delegate.propery2Changed(property2)
}
}
}
If you wonder why delegate is not marked as ‘weak’ please read this post as well.
We could use delegation pattern but if we want to follow ISP then we would need to break each method from delegate into its own interface which would lead into multiple delegates as well. A lot of boilerplate code for each property.
All delegates could be swapped with callbacks but at the end of the day we would have basically same functionality.
There are also other options, but analysing all of them is out of scope for this post.
Solution
Let’s introduce ‘Observable’. We will use power of @propertyWrapper’s and use it for our use case. Purpose of ‘Observable’ is to enable design models ands expose observing mechanism for properties to clients that want them observe. Mechanism should allow one or more observers at once.
Our basic ‘Observeble’ would looks like this example bellow. As we can see we expose public method ‘observe’ which caches ‘closure’ for later usage. When ‘set’ is being triggered by the system we iterate through all observers and notify them about change.
@propertyWrapper
class Observable {
private var observers: [(String) -> Void] = []
var value: String
init(wrappedValue initialValue: String) {
self.value = initialValue
self.wrappedValue = initialValue
}
var wrappedValue: String {
get { value }
set {
/* Notify all observers about change */
value = newValue
observers.forEach { $0.value(wrappedValue) }
}
}
var projectedValue: Observable { return self }
func observe(_ closure: @escaping (String) -> Void) {
observers.append(closure)
}
}
Example above is limited to be used only with ‘String’s. It would be inconvenient to make ‘Observable’ for each type that we want to observe. But with power of generics we can do quick refactor and achieve that ‘Observable’ works with any type we desire.
@propertyWrapper
class Observable<T> {
private var observers: [(T) -> Void] = []
var value: T
init(wrappedValue initialValue: T) {
self.value = initialValue
self.wrappedValue = initialValue
}
var wrappedValue: T {
get { value }
set {
/* Notify all observers about change */
value = newValue
observers.forEach { $0.value(wrappedValue) }
}
}
var projectedValue: Observable<T> { return self }
func observe(_ closure: @escaping (T) -> Void) {
observers.append(closure)
}
}
Although this works, there are still pieces that are missing. Clients can’t manually stop observing changes. That is why we need to add ‘ObservableToken’ so that we enable clients removing themselves when they’re not interested in changes anymore.
@propertyWrapper
class Observable<T> {
class ObservableToken {
private let cancellationClosure: () -> Void
init(_ cancellationClosure: @escaping () -> Void) {
self.cancellationClosure = cancellationClosure
}
func cancel() {
cancellationClosure()
}
}
private var observers: [UUID: (T) -> Void] = [:]
var value: T
init(wrappedValue initialValue: T) {
self.value = initialValue
self.wrappedValue = initialValue
}
var wrappedValue: T {
get { value }
set {
/* Notify all observers about change */
value = newValue
observers.forEach { $0.value(wrappedValue) }
}
}
var projectedValue: Observable<T> { return self }
@discardableResult
func observe(_ closure: @escaping (T) -> Void) -> ObservableToken {
let id = UUID()
observers[id] = { closure($0) }
return ObservableToken { [weak self] in self?.observers.removeValue(forKey: id) }
}
}
We could expand our ‘Observable’ even more with delivering old and new value to clients, making pre-check that new and old value is actually different etc., but this is out of scope for this post.
Last but not least lets have a look how it would look to use ‘Observable’ in action:
class AnyObjectType {
@Observable var property: String
init(property: String) {
self.property = property
}
}
struct AnyStructType {
@Observable var property: String
}
let anyOject = AnyObjectType(property: "anyString")
anyOject.$property.observe { /* Do something */ }
var anyStruct = AnyStructType(property: "anyString")
anyStruct.$property.observe { /* Do something */ }
As we can see we ended up with solution that doesn’t limit us to use only classes. We can also easily have one or more observers observing model’s property which allows us to implement logic that has nothing to do with our model outside of it. This keeps our object clean and without knowledge of external dependencies which was our primary goal as well.
Thank you for reading!
In case of any questions or comments feel free to contact me or leave it comments section bellow.
Architectural Patterns – Decorator
In this article I will present you powerful architectural pattern that I have been using it a lot called Decorator.
1. What is Decorator
...Detect user’s country without accessing their location
Sometimes we are faced with challenge where we would like to improve user experience of our app based on where in the world or better said in which country ...