swiftkey-value-observing

Is it possible to add an observer on struct variable in Swift?


I need to track the update in a variable of struct type. Is it possible to add an observer on struct variable in Swift?

Example:

struct MyCustomStruct {
    var error:Error?
    var someVar:String?
}

class MyClass{
  var myCustomStruct:MyCustomStruct?
}

I want to add an observer on myCustomStruct variable.


Solution

  • The standard Swift “property observers” (didSet and willSet) are designed to let a type observe changes to its own properties, but not for letting external objects add their own observers. And KVO, which does support external observers, is only for dynamic and @objc properties NSObject subclasses (as outlined in Using Key-Value Observing in Swift).

    So, if you want to have an external object observe changes within a struct, as others have pointed out, you have to create your own observer mechanism using Swift didSet and the like. But rather than implementing that yourself, property by property, you can write a generic type to do this for you. E.g.,

    struct Observable<T> {
        typealias Observer = String
    
        private var handlers: [Observer: (T) -> Void] = [:]
    
        var value: T {
            didSet {
                handlers.forEach { $0.value(value) }
            }
        }
    
        init(_ value: T) {
            self.value = value
        }
    
        @discardableResult
        mutating func observeNext(_ handler: @escaping (T) -> Void) -> Observer {
            let key = UUID().uuidString as Observer
            handlers[key] = handler
            return key
        }
    
        mutating func remove(_ key: Observer) {
            handlers.removeValue(forKey: key)
        }
    }
    

    Then you can do things like:

    struct Foo {
        var i: Observable<Int>
        var text: Observable<String>
    
        init(i: Int, text: String) {
            self.i = Observable(i)
            self.text = Observable(text)
        }
    }
    
    class MyClass {
        var foo: Foo
    
        init() {
            foo = Foo(i: 0, text: "foo")
        }
    }
    
    let object = MyClass()
    object.foo.i.observeNext { [weak self] value in   // the weak reference is really only needed if you reference self, but if you do, make sure to make it weak to avoid strong reference cycle
        print("new value", value)
    }
    

    And then, when you update the property, for example like below, your observer handler closure will be called:

    object.foo.i.value = 42
    

    It’s worth noting that frameworks like Bond or RxSwift offer this sort of functionality, plus a lot more.