This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We refine the observation capabilities of our new data type.

00:06 In a previous episode, we built a struct/class hybrid type called Var. Today we'll continue with its experimental implementation.

00:20 The Var class holds a struct, and we can use key paths to look into the struct. If we have a people array inside a Var and we want to take the first Person out of the array, then we get another Var with that Person. Updating this Person modifies the original array, and in this way we've given Var reference semantics. But we also still get the copying behavior of structs if we need it: we can take the struct value out of the Var and have a local copy.

00:52 We also get deep observing. Whenever anything changes, the root variable will know about it. We still have a somewhat awkward API because we initialize Var with an observe closure, which means we can only add one observer at the root level at the time of initialization. We'd like to improve this API with an addObserver method and use it if we want to observe the root struct or any other property.

Adding Observers

01:44 We remove the observer closure from the initializer and set up a new addObserver method. Because we're going to use the observer closure a lot, we can create a type alias for it:

final class Var<A> {
    // ...

    init(initialValue: A) {
        var value: A = initialValue {
            didSet {
                
            }
        }
        _get = { value }
        _set = { newValue in value = newValue }
    }
    
    typealias Observer = (A) -> ()
    func addObserver(_ observer: @escaping Observer) {
        
    }
    // ...
}

02:21 Previously, we hooked up an observer closure to the struct value in the initializer, but now we don't have access to an observer there. We need to store all observers in one place, beginning with an empty array, and hook up the observers and the struct value:

final class Var<A> {
    // ...

    init(initialValue: A) {
        var observers: [Observer] = []
        var value: A = initialValue {
            didSet {
                for o in observers {
                    o(value)
                }
            }
        }
        _get = { value }
        _set = { newValue in value = newValue }
    }
    // ...
}

03:13 Now we still need a way to add an observer to the array. We repeat the trick we did with get and set and turn addObserver into a property instead of a method:

final class Var<A> {
    let addObserver: (_ observer: @escaping Observer) -> ()
    
    // ...
    
    init(initialValue: A) {
        var observers: [Observer] = []
        var value: A = initialValue {
            didSet {
                for o in observers {
                    o(value)
                }
            }
        }
        _get = { value }
        _set = { newValue in value = newValue }
        addObserver = { observer in observers.append(observer) }
    }
    // ...
}

04:30 Before we can work with addObserver, we have to set it in our other, private initializer as well. To do this, we'll pass in a closure from the outside so that we can define the closure in our subscript implementations:

fileprivate init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> ()) {
    _get = get
    _set = set
    self.addObserver = addObserver
}

05:20 In the key path subscript, we now have to define an addObserver closure, which takes an observer and calls this observer with values of type B. We only have values of type A, but we can also observe self in this closure and use the key path to get the B from a received A:

subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
    return Var<B>(get: {
        self.value[keyPath: keyPath]
    }, set: { newValue in
        self.value[keyPath: keyPath] = newValue
    }, addObserver: { observer in
        self.addObserver { newValue in
            observer(newValue[keyPath: keyPath])
        }
    })
}

06:15 No matter how deep we've nested our Vars, observers are always added to the root Var, since a child Var passes the observer on until it reaches the observers array of the root Var. This means that an observer is called whenever the root value changes — in other words: an observer of a specific property might be called even if the property itself hasn't changed.

06:52 We also pass in a similar addObserver closure in the collections' subscript:

extension Var where A: MutableCollection {
    subscript(index: A.Index) -> Var<A.Element> {
        return Var<A.Element>(get: {
            self.value[index]
        }, set: { newValue in
            self.value[index] = newValue
        }, addObserver: { observer in
            self.addObserver { newValue in
                observer(newValue[index])
            }
        })
    }
}

07:21 Let's see how this works:

let peopleVar: Var<[Person]> = Var(initialValue: people)
peopleVar.addObserver { p in
    print("peoplevar changed: \(p)")
}
let vc = PersonViewController(person: peopleVar[0])
vc.update()

07:42 This prints the peopleVar change to the console. But we can now also add an observer to the Var<Person> of the PersonViewController, and this will print an extra line with the new Person value:

final class PersonViewController {
    let person: Var<Person>
    
    init(person: Var<Person>) {
        self.person = person
        self.person.addObserver { newPerson in
            print(newPerson)
        }
    }
    
    func update() {
        person.value.last = "changed"
    }
}

Removing Observers

08:09 We can add observers now, but we have no way to remove them. This becomes a problem if the view controller goes away because its observer will still be there.

08:25 We can take an approach similar to how reactive libraries work. When adding an observer, an opaque object is returned. By keeping a reference to this object, we keep the observer alive. When we discard the object, the observer is removed.

09:02 We use a helper class, called Disposable, which takes a dispose function that it calls when the object deinits:

final class Disposable {
    private let dispose: () -> ()
    init(_ dispose: @escaping () -> ()) {
        self.dispose = dispose
    }
    deinit {
        dispose()
    }
}

09:43 We update the signature of addObserver to return a Disposable:

final class Var<A> {
    private let _get: () -> A
    private let _set: (A) -> ()
    let addObserver: (_ observer: @escaping Observer) -> Disposable

    // ...
}

10:01 If we want to remove observers, we have to change the data structure of our observers store. An array no longer works, as it's not possible to compare functions in order to find the one to remove. Instead, we can use a dictionary keyed by unique integers:

final class Var<A> {
    // ...
    
    init(initialValue: A) {
        var observers: [Int:Observer] = [:]
        // ...
    }
    // ...
}

11:18 A cool way to generate these integers is to use Swift's lazy collections. We create an iterator with a range from 0 to infinity, and each time we need an id, we can call next() on this iterator. This returns an optional, but we can force-unwrap it because we know it can't be nil:

final class Var<A> {
    // ...
    
    init(initialValue: A) {
        var observers: [Int:Observer] = [:]
        // ...
        var freshInt = (0...).makeIterator()
        addObserver = { observer in
            let id = freshInt.next()!
            observers[id] = observer
            // ...
        }
    }
    // ...
}

12:09 We're now storing observers in a dictionary. All that's left to do is discard the observers when we no longer use them. We return a Disposable with a dispose function that removes the observer from the dictionary:

final class Var<A> {
    // ...
    
    init(initialValue: A) {
        var observers: [Int:Observer] = [:]
        // ...
        var freshInt = (0...).makeIterator()
        addObserver = { observer in
            let id = freshInt.next()!
            observers[id] = observer
            return Disposable { observers[id] = nil }
        }
    }
    // ...
}

12:33 Lastly, we have to fix the addObserver signature in the private initializer, which still states to return void instead of a Disposable:

fileprivate init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> Disposable) { /*...*/ }

13:25 Now our code compiles again, but we do get a compiler warning about the fact that we're ignoring the returned Disposable in the view controller. This explains why we're no longer getting the print statement with a changed Person value, since we should keep a reference to the observer's Disposable in order for it to stay alive:

final class PersonViewController {
    let person: Var<Person>
    let disposable: Any?
    
    init(person: Var<Person>) {
        self.person = person
        disposable = self.person.addObserver { newPerson in
            print(newPerson)
        }
    }
    
    func update() {
        person.value.last = "changed"
    }
}

14:12 We're now retaining the observer and we get the print statement after updating the Person. To reiterate what happens here: the moment the view controller is released, its properties are cleared, the Disposable deinits, and this calls the code that takes the observer out of the dictionary by setting its id to nil.

14:49 Note: if we want to use self in an observer, we have to make it a weak reference to avoid creating a reference cycle.

Comparing Old and New Values

15:09 An important aspect of our implementation is that observers are triggered not only when the observed Var changes, but also whenever anything changes in the entire data structure.

15:25 If the PersonViewController wants to be sure that its Person changed, it should be able to compare the new value to the old value. So we'll change our Observer type alias to deliver both the new and old values:

typealias Observer = (A, A) -> ()

15:54 This means observers are called with a new and an old version of A. To make this explicit, we should probably wrap the values in a struct with two fields describing what they are, but we're skipping that part.

16:14 Where we call observers inside Var, we have to now pass in the old value too:

init(initialValue: A) {
    // ...
    var value: A = initialValue {
        didSet {
            for o in observers.values {
                o(value, oldValue)
            }
        }
    }
    // ...
}
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
    return Var<B>(get: {
        self.value[keyPath: keyPath]
    }, set: { newValue in
        self.value[keyPath: keyPath] = newValue
    }, addObserver: { observer in
        self.addObserver { newValue, oldValue in
            observer(newValue[keyPath: keyPath], oldValue[keyPath: keyPath])
        }
    })
}

16:55 And in the MutableCollection subscript, we should also pass the old value to observers:

extension Var where A: MutableCollection {
    subscript(index: A.Index) -> Var<A.Element> {
        return Var<A.Element>(get: {
            self.value[index]
        }, set: { newValue in
            self.value[index] = newValue
        }, addObserver: { observer in
            self.addObserver { newValue, oldValue in
                observer(newValue[index], oldValue[index])
            }
        })
    }
}

17:12 The observer in PersonViewController can compare the new and old versions to see whether its model has indeed changed:

final class PersonViewController {
    // ...
    init(person: Var<Person>) {
        self.person = person
        disposable = self.person.addObserver { newPerson, oldPerson in
            guard newPerson != oldPerson else { return }
            print(newPerson)
        }
    }
    // ...
}

17:33 Finally, we need to fix the observer of the peopleVar:

peopleVar.addObserver { newPeople, oldPeople in
    print("peoplevar changed: \(newPeople)")
}

17:47 By making a change to a person other than the one in the view controller, we test that the view controller's observer ignores it:

peopleVar[1].value.first = "Test"

Discussion

18:33 We've made an interesting combination of reactive programming — with the ability to observe changes — and object-oriented programming.

18:52 There's still a surprise waiting in this code. We're handing over the first Person from an array to a view controller. If we then delete the first element of the people array, the view controller suddenly has a different Person:

peopleVar.value.removeFirst()

19:49 The first element is deleted from the array, but the Var in the PersonViewController is still pointing to peopleVar[0] because we're using a dynamically evaluated subscript. In most cases this is undesired behavior. An example that would improve this behavior is to have a first(where:) method that would allow us to select an element by an identifier.

20:40 We're excited about what we've built so far. Perhaps it could change the way we write apps. Or, maybe it's still too experimental: we managed to compile the code, but we're not sure where and how the technique will break.

21:37 Even if we won't use Var in practice, we've combined many interesting features that together demonstrate the power of Swift very well: generics, key paths, closures, variable capture, protocols, and extensions.

22:15 In the future, it might be cool to experiment with only partially applying aspects of Var. Let's say we have a database interface that reads a person model from the database and returns it as a Var<Person>. We could use it to automatically save changes to the struct back to the database. It seems there would be examples, like this one, in which the Var technique can be useful.

Resources

  • Playground

    Written in Swift 4

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

40 Episodes · 15h47min

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes