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 Var
s, 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.