willSet and didSet

In case anyone else was wondering about some of these things…

We implemented the methods:

[code]func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
}

func removeObjectFromEmployeesAtIndex(index: Int) {
}
[/code]

…which through the miracle of KVC will execute when the Array Controller calls the methods:

employees.insertObject(_:, atIndex:)
employees.removeObjectAtIndex(_:)[/code]

But for edits to an employee's [i]name[/i] or [i]raise[/i], the book says we have to intercept the Array controller's machinations with the observers willSet() and didSet().  The book uses this code:

[code]
    var employees: [Employee] = []  {
        willSet {
            for employee in employees {
                stopObserving(employee)
            }
        }
        didSet {
            for employee in employees {
                startObserving(employee)
            }
        }
    }
[/code]
...which made me wonder: when do willSet() and didSet() get called?  To me, the book made it seem like the Array Controller will do something like this:

[code]let new_arr = [Employee()]
document.employees = new_arr[/code]

....which in turn will call the observers willSet() and didSet().  That is, the book made it seem like the employees variable has to be assigned a new array in order for willSet and didSet to be called.  Yet, when the Array Controller changes the employees array, the methods we implemented get called:

[code]func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
}

func removeObjectFromEmployeesAtIndex(index: Int) {
}[/code]

and inside those methods we are changing the employees array by calling:

[code]employees.append(employee)
employees.removeAtIndex(index)[/code]

But the append(_:) and removeAtIndex(_:) methods don’t sound like they create new arrays.  In fact, in a playground if you write something like:

[code]var arr: [String] = ["hello"]
arr.append("goodbye")
println(arr)

--output:--
[a, b, c]

…then option click on the append() method, a popup will open saying append() is a mutating func, i.e. append() does not create a new array. So, if our methods don’t assign a new array to the employees variable, how do willSet() and didSet() get called?

It turns out that willSet() and didSet() also get called when an Employee is inserted or removed from the employees array–as well as when a new array is assigned to the employees variable. Here’s the proof:

[code]import Foundation

class Bird: NSObject {
var arr: [String] = [] {
willSet {
println(“willSet: arr will change from (arr) to: (newValue)”)
}
didSet {
println(“didSet: arr did change from: (oldValue) to: (arr)”)
}
}
}

let b = Bird()
b.arr.append(“hello”)
println(b.arr)
b.arr.removeAtIndex(0)
println(b.arr)

b.arr = [“hello”, “world”]
println(b.arr)

–output:–
willSet: arr will change from [] to: [hello]
didSet: arr did change from: [] to: [hello]
[hello]
willSet: arr will change from [hello] to: []
didSet: arr did change from: [hello] to: []
[]
willSet: arr will change from [] to: [hello, world]
didSet: arr did change from: [] to: [hello, world]
[hello, world]
[/code]

Note that newValue and oldValue are default parameter variable names for some strange arguments that are passed to their respective observers. If you want, you can rename them like this:

class Bird: NSObject { var arr: [String] = [] { willSet(shinyNewValue) { println("willSet: arr will change from \(arr) to: \(shinyNewValue)") } didSet(crummyOldValue) { println("didSet: arr did change from: \(crummyOldValue) to: \(arr)") } } }

…which is a little different than the other methods we’ve implemented in this chapter-- because you don’t (and can’t) specify the type of the parameter variable.

Next, note that if the employees array contains 100 Employee’s, and subsequently the Array Controller adds an Employee to the array, then the willSet method will step through each Employee in the employees array and STOP observing it, then didSet will step through each Employee again and Start observing it–which is terribly inefficient when all we want to do is to START observing the newly added Employee. That was something else in the book that made me think that willSet and didSet only would be called if a new array was assigned to the employees var–otherwise you wouldn’t have to step through the whole employees array.

It would be more efficient to just grab the newly created Employee and START observing it and avoid stepping through the Employees array twice to accomplish the same thing. Unfortunately, willSet and didSet don’t tell you whether an Employee was added or whether an Employee was removed. You could examine the before and after employees array and determine which has a bigger count to figure out whether an employee was added or removed, but then you would additionally need to figure out which employee was added or removed. If only we had an easy way of knowing whether an Employee was added or removed AND which Employee was added or removed. But wait, the insert and remove methods we implemented know which Employee is going to be added or removed, so why don’t we use those methods to setup the name and raise observers? Like this:

    func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
        println("Inserting \(employee) at index \(index)")
        
        let undo: NSUndoManager = undoManager!
        undo.prepareWithInvocationTarget(self).removeObjectFromEmployeesAtIndex(index)
        
        if !undo.undoing {
            undo.setActionName("Add Employee")
        }
        
        employees.append(employee)
        startObserving(employee)  //*****HERE*****
    }

    func removeObjectFromEmployeesAtIndex(index: Int) {
        println("Removing Employee at positon \(index)")
        let employee = employees[index]
        
        let undo: NSUndoManager = undoManager!
        undo.prepareWithInvocationTarget(self).insertObject(employee, inEmployeesAtIndex: index)
        
        if !undo.undoing {
            undo.setActionName("Remove Employee")
        }
        
        employees.removeAtIndex(index)
        stopObserving(employee)  //******AND HERE******
        
    }

It seems to work. With that code, you have to change windowWillClose() to this:

    func windowWillClose(notification: NSNotification) {
        for employee in employees {
            stopObserving(employee)
        }
    }[/code]

As a result, the only time you step through the whole employees array is when you want to STOP observing all the remaining Employee’s in the employees array.  

Finally, why does the insert method we implemented:

[code]    func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
        println("Inserting \(employee) at index \(index)")
        
        let undo: NSUndoManager = undoManager!
        undo.prepareWithInvocationTarget(self).removeObjectFromEmployeesAtIndex(index)
        
        if !undo.undoing {
            undo.setActionName("Add Employee")
        }
        
        employees.append(employee)  
    }

completely ignore the index and append the new Employee to the end of the employees array? Letting the Array Controller decide where to insert a new Employee in the employees array seems to work:

[code] func insertObject(employee: Employee, inEmployeesAtIndex index: Int) {
println(“Inserting (employee) at index (index)”)

    let undo: NSUndoManager = undoManager!
    undo.prepareWithInvocationTarget(self).removeObjectFromEmployeesAtIndex(index)
    
    if !undo.undoing {
        undo.setActionName("Add Employee")
    }
    
    //employees.append(employee)
    employees.insert(employee, atIndex: index)  //*****CHANGE HERE****
    startObserving(employee)
}

[/code]

I thought that maybe the book used append() because you can’t insert() something at the end of an array because to do so, you would have to use an out of bounds index, for example:

var arr = ["a", "b"] //valid indexes are 0, 1 arr.insert("c", atIndex: 2) //index 2 is out of bounds println(arr)

But that code works–apparently you can insert() one beyond the last, but you can’t use an index of say 9 for that insert().