00:06 Today we'll build a new type in between a struct and a class that
incorporates positive aspects of both of these things, including the shared
state of objects, the possibility to know when something changes anywhere in the
structure, and the ease of making a private copy at any point.
01:01 This is a bit of an experiment. We don't know for sure if the end
result will be useful, but at least we'll get to play with some nice Swift
features and push the limits of the language.
Using a Class
01:29 Let's first take a look at an example that shows the challenge
we're facing. We have a Person
class, and we created an array with some
instances:
final class Person {
var first: String
var last: String
init(first: String, last: String) {
self.first = first
self.last = last
}
}
let people = [
Person(first: "Jo", last: "Smith"),
Person(first: "Joanne", last: "Williams"),
Person(first: "Annie", last: "Williams"),
Person(first: "Robert", last: "Jones")
]
01:46 Let's say we're using this in an iOS app with a table view
controller that allows us to view individual items in a detail view controller,
and maybe the detail view controller wants to update the object:
final class PersonViewController {
var person: Person
init(person: Person) {
self.person = person
}
func update() {
person.last = "changed"
}
}
02:38 When we initialize a PersonViewController
and pass in the first
element of the people array and then call the update method, we're changing the
Person
that's held by the detail view controller. The benefit of using objects
is that the change to the Person
in the detail view controller will be
reflected in the first Person
of the people array:
let vc = PersonViewController(person: people[0])
vc.update()
dump(people[0])
03:24 We don't have to worry about communicating changes back unless we
want to observe and react to changes. The array itself never changes because it
only holds references to objects. The Person
instances themselves may change,
but their references in the people array stay the same. This is already hinted
at by the fact that we've defined people
with a let
.
Using a Struct
04:02 If Person
was a struct, we would've defined people
with a
var
, and then we'd be able to observe changes. But since Person
is a class,
a didSet
of people
isn't called, because the value of the people
variable
isn't changed:
var people = [
Person(first: "Jo", last: "Smith"),
Person(first: "Joanne", last: "Williams"),
Person(first: "Annie", last: "Williams"),
Person(first: "Robert", last: "Jones")
] {
didSet {
dump("people didSet \(people)")
}
}
04:42 When working with structs, we can take advantage of the value
semantics if we want to observe our data structure: by observing the variable,
we can be notified of changes anywhere in the structure. But if we want to
observe an array of objects, we either have to observe each object or we have to
be very disciplined about communicating back any changes we make to any of the
objects.
05:10 We convert Person
into a struct and see which changes we have to
make to our code. A nice side effect is that we no longer need to write the
standard initializer:
struct Person {
var first: String
var last: String
}
05:25 Now the didSet
of people
would be called if we'd actually
change its value. But we're passing a copy of the first person, and not a
reference, to the detail view controller, so the detail view controller is
updating its own copy of the Person
. The observer of people
is only called
if we make the change directly in the array:
people[0].last = "new"
06:36 When you create a new variable for a struct, you're creating a
copy. Changes to a copy don't affect the people
array:
var personZero = people[0]
personZero.last = "new"
07:03 Now that we're working with structs, we have to somehow
communicate this change back to the original people
array, e.g. by using a
delegate or a callback.
Var
07:32 We'll create a class that's basically a box containing a struct.
We name the class Var
, for lack of a better name. Inside Var
, we can observe
the struct's value with didSet
. This way we can use references to the box
anywhere and still have a central didSet
to keep track of its changes.
08:08 The Var
class is generic over its contained value, and it takes
an observer closure, which we hook up to the value's didSet
:
final class Var<A> {
var value: A {
didSet {
observer(value)
}
}
var observer: (A) -> ()
init(initialValue: A, observe: @escaping (A) -> ()) {
self.value = initialValue
self.observer = observe
}
}
09:10 We create a Var
with the people array and try to update the
array's first element. This results in the array being dumped to the console:
let peopleVar = Var(initialValue: people) { newValue in
dump(newValue)
}
peopleVar.value[0].last = "Test"
10:25 Next we want to be able to take out a part of the struct. Let's
say we want to focus on the first Person
, like in our original example with
the detail view controller. From peopleVar
, we want to create another Var
that references only the first Person
. When we mutate the value of this new
Var
, it should update the original peopleVar
.
11:08 Ultimately, we want to index into an array of people, but we'll
start by using a Swift 4 keypath subscript to extract a Var
for first or last
name from a Var<Person>
. When that works, we'll add the array subscripting
based on the key path implementation.
11:27 So we begin with a Var
for a single Person
:
let personVar: Var<Person> = Var(initialValue: people[0]) { newValue in
dump(newValue)
}
11:50 In order to extract a Var
for the first name from this
personVar
, we add a subscript on the Var
class that takes a key path. The
key path will be a WritableKeyPath
because we want to read and write to it.
Additionally, the key path takes two generic parameters: the base type we're
subscripting on (our generic type A
), and the return value (which we'll call
B
). The subscript will return a new Var<B>
:
final class Var<A> {
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
}
}
13:09 In the body we have to return a Var<B>
, so we'll start creating
it. We set its initial value by using the standard key path subscript. In the
observer, we take the new value and write it back to our own value using the
same key path:
final class Var<A> {
var value: A
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
return Var<B>(initialValue: value[keyPath: keyPath]) { newValue in
self.value[keyPath: keyPath] = newValue
}
}
}
14:06 Let's try this out. We create a Var
for the first name of
personVar
and change its value:
let firstNameVar: Var<String> = personVar[\.first]
firstNameVar.value = "new first name"
14:54 Changing the value triggers the original observer of personVar
and we see the new first name printed out. So it almost seems to work, but it
doesn't work the other way around; changing the first name via the value of
personVar
doesn't update the value of firstNameVar
:
let firstNameVar: Var<String> = personVar[\.first]
personVar.value.first = "new first name"
15:36 Our subscript implementation is wrong. We're capturing the initial
value, but we should be capturing a reference to the value. We'll do a trick to
hide the value
and the observer
of Var
inside its initializer:
final class Var<A> {
init(initialValue: A, observe: @escaping (A) -> ()) {
var value = initialValue {
didSet {
observe(value)
}
}
}
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
}
}
16:45 Now we can only specify the initial value and observer inside the
initializer. We still want to get or set the value later, so we'll use a
computed property for value
with a getter and setter, which will both be
stored properties:
final class Var<A> {
private let _get: () -> A
private let _set: (A) -> ()
var value: A {
get {
return _get()
}
set {
_set(newValue)
}
}
init(initialValue: A, observe: @escaping (A) -> ()) {
var value = initialValue {
didSet {
observe(value)
}
}
_get = { value }
_set = { newValue in value = newValue }
}
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
}
}
18:10 This code's a bit tricky. The moment we define the getter in the
initializer, the assigned closure captures a reference to the value
variable.
So when we call the getter through the computed property, we're actually using a
reference to retrieve the value.
19:23 Now we can write a private initializer that takes a getter and a
setter. We use this initializer for our subscript implementation, and for the
getter and setter we call on the computed property value
with the given key
path:
final class Var<A> {
private let _get: () -> A
private let _set: (A) -> ()
private init(get: @escaping () -> A, set: @escaping (A) -> ()) {
_get = get
_set = set
}
subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
return Var<B>(get: {
self.value[keyPath: keyPath]
}, set: { newValue in
self.value[keyPath: keyPath] = newValue
})
}
}
22:02 Let's see this in action. If we change the first name through the
personVar
, we also see the change reflected in firstNameVar
:
let personVar: Var<Person> = Var(initialValue: people[0]) { newValue in
dump(newValue)
}
let firstNameVar: Var<String> = personVar[\.first]
personVar.value.first = "new first name"
firstNameVar.value
22:23 The other way around it works as well:
firstNameVar.value = "test"
personVar.value.first
22:41 We've achieved a way to create a reference to an observable
struct value and we can create a reference to a part of it. In a strange way,
we've reinvented the concept of objects and added a built-in observing
mechanism.
Index Subscript
23:16 In order to make our original example work with Var
, we need
the ability to subscript into an array. So we need another subscript that works
with collection values. In theory, our key path subscript should support
collections too, but that part of Swift 4's key paths isn't yet implemented.
23:59 Instead we'll create a workaround and add a subscript to Var
where it contains a collection. We don't need to append or remove elements; we
only need to get and set elements by an index. So we can constrain the subscript
to the MutableCollection
protocol:
extension Var where A: MutableCollection {
}
24:40 The new subscript takes an index, for which we can use the
collection's index type, and it returns a Var
containing an element of the
collection:
extension Var where A: MutableCollection {
subscript(index: A.Index) -> Var<A.Element> {
}
}
25:16 The implementation of this subscript is very similar to our other
subscript method, so we can follow the same approach and replace the key path
subscript with a call to the index subscript of the collection. We need to widen
the access level of the getter/setter initializer to fileprivate
in order to
use it from the extension:
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
})
}
}
26:04 Now we're able to define a peopleVar
and take a personVar
out
of it:
let peopleVar: Var<[Person]> = Var(initialValue: people) { newValue in
dump(newValue)
}
let personVar: Var<Person> = peopleVar[0]
26:41 Changes to the personVar
now trigger the observer on the
peopleVar
. Regarding the other direction, mutations to the peopleVar
are
also seen in the personVar
.
Using Var
27:10 We go back to our original code and apply Var
in the
PersonViewController
. This way, the view controller can update its model and
these changes are reflected back to the people variable:
final class PersonViewController {
let person: Var<Person>
init(person: Var<Person>) {
self.person = person
}
func update() {
person.value.last = "changed"
}
}
let peopleVar: Var<[Person]> = Var(initialValue: people) { newValue in
dump(newValue)
}
let vc = PersonViewController(person: peopleVar[0])
vc.update()
28:14 Let's recap what happens here. We create a peopleVar
with an
array of Person
structs. We pass the first person into a view controller. The
view controller's update of the value is referenced back to the original
peopleVar
array. In the end, we see "changed" dumped in the console as a last
name.
28:44 If the view controller still needs an independent copy of its
model, it can easily get it by using the Var
's value:
let independentCopy = person.value
29:15 The PersonViewController
doesn't know anything about a people
array; it just receives a Var<Person>
that it can read from and write to.
That's all it needs.
To Do
29:34 One missing piece is the ability to observe a variable. Right
now, we only have the initializer of the root variable where we can define an
observer. It'll be useful for the PersonViewController
to observe and react to
changes to its person
. Adding that functionality will be a bit of work, but
we'll continue with it in another episode.