Swift Talk #61
Mutable Shared Structs (Part 1)30:10
We build an experimental data type that combines the advantages of both structs and classes. It allows shared mutable state while preserving the features of structs to make easy copies and observe changes.
0: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.
1: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
1: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:
1: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:
2: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:
3: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 given away by
the fact that we've defined people with a let.
Using a Struct
4: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
4: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
5: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
5: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.last="new"// prints "people didSet [Person(first:\"Joe\", last: \"new\"), ...]"
6: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:
7: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
7: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.
8: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:
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: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>:
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:
14:06 Let's try this out. We create a Var for the first name of
personVar and change its value:
letfirstNameVar: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:
letfirstNameVar:Var<String>=personVar[\.first]personVar.value.first="new first name"// firstNameVar.value: "Jo"
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:
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
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
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
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:
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
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:
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
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:
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.
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.