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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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. We'll change our Observer type alias to deliver both the new and old values:
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:
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:
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:
19:49 The first element is deleted from the array, but the Var in the PersonViewController is still pointing to peopleVar 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.