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 recap the tradeoffs between classes and structs and start implementation of our new type, leveraging Swift 4's keypaths.

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])
// - last: "changed"

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"
// prints "people didSet [Person(first:\"Joe\", 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"
// people[0].last: "Smith"

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"
// 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:

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 // "new first name"

22:23 The other way around it works as well:

firstNameVar.value = "test"
personVar.value.first // "test"

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.

Resources

  • Playground

    Written in Swift 4

  • Episode Video

    Become a subscriber to download episode videos.

Related Blogposts

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