12

I created a class which contains an array. I added an observer to that array in a view controller and performed some modifications to that array.

The problem is that when I print the change dictionary returned by the observeValueForKeyPath() method I can only see changes of kind NSKeyValueChangeSetting. In other words, the method tells me the array has changed, provides me with the old and new arrays (containing all elements) but I would like to receive the information of which specific items were added or removed.

Here is some example code.

This is the class whose array will be observed.

private let _observedClass = ObservedClass()

class ObservedClass: NSObject {
    dynamic var animals = [String]()
    dynamic var cars = [String]()

    class var sharedInstance: ObservedClass {
        return _observedClass
    }
}

And this is the code at my view controller.

class ViewController: UIViewController {
    var observedClass = ObservedClass.sharedInstance

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        observedClass.addObserver(self, forKeyPath: "animals", options: .New | .Old, context: nil)
    }

    deinit {
        observedClass.removeObserver(self, forKeyPath: "animals")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        observedClass.animals.insert("monkey", atIndex: 0)
        observedClass.animals.append("tiger")
        observedClass.animals.append("lion")
        observedClass.animals.removeAtIndex(0)
    }

    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
        println(change)
    }
}

When I run the above code, I get this result on the console:

[kind: 1, old: (
), new: (
    monkey
)]
[kind: 1, old: (
    monkey
), new: (
    monkey,
    tiger
)]
[kind: 1, old: (
    monkey,
    tiger
), new: (
    monkey,
    tiger,
    lion
)]
[kind: 1, old: (
    monkey,
    tiger,
    lion
), new: (
    tiger,
    lion
)]

Shouldn't, on this example, the change dictionary show each new item as it is added to the array, using the change kind NSKeyValueChangeInsertion?

Felipe Ferri
  • 3,488
  • 2
  • 33
  • 48
  • I got the same result as yours when I use `options: .Old | .New`. – gabbler Mar 17 '15 at 03:53
  • You're right, I used both options but posted the code with only one option. – Felipe Ferri Mar 17 '15 at 14:54
  • It is the desired behaviour, isn't it? With this option, both new and old dictionary are included. – gabbler Mar 17 '15 at 15:08
  • 1
    I understood that the observeValueForKeyPath should also display the items that were added or removed, along with the complete old and new lists. With the output of the example, I have to compare both lists and find out by myself which elements were added or removed. I saw some examples in ObjectiveC where, when an array was modified, only the added and removed elements were shown, using as "Change kind" the value 2, which corresponds to NSKeyValueChangeInsertion. – Felipe Ferri Mar 17 '15 at 15:37
  • Can you find the source? – gabbler Mar 17 '15 at 16:11
  • Check this out: http://www.appcoda.com/understanding-key-value-observing-coding/. There is an execution result (just below the text "Time to test it… Here are the results displayed on the console") where, upon adding an item to an array, the KVO observer received a change dictionary with kind = 2 and containing only the added item (as opposed to the complete array). Is this just an Objective C thing? – Felipe Ferri Mar 18 '15 at 19:45

1 Answers1

13

According to the Swift guide:

For arrays, copying only takes place when you perform an action that has the potential to modify the length of the array. This includes appending, inserting, or removing items, or using a ranged subscript to replace a range of items in the array.

Take as an example an append operation. Under the hood, when you append to your array, Swift creates a new array in memory, copies the items from the existing animals array into this new array - plus the new item - then assigns this new array to the animals variable. This sleight-of-hand is why you only ever get the kind of 1 (Setting), because in fact each 'edit' actually results in a new array being created.

It's different with NSMutableArray because the behaviour is a bit more intuitive - edits are made to the existing array (there's no behind-the-scenes copying to a new array), so the array that exists after the edit is the same array that existed before it - hence the value stored in the change dictionary with the key NSKeyValueChangeKindKey can be one of .Insertion, .Removal, etc.

Even that isn't the whole story however, because the way you make an KVO compliant changes to an NSMutableArray varies depending on whether you're using Swift or Objective-C. In Objective-C, Apple strongly recommend implementing what they call the optional mutable indexed accessors. These are just methods with a standard signature that make KVO-compliant changes in a very efficient way.

// Mutable Accessors ///////////////////////////////////////////

// This class has a property called <children> of type NSMutableArray

// MUST have this (or other insert accessor)
-(void)insertObject:(NSNumber *)object inChildrenAtIndex:(NSUInteger)index {
    [self.children insertObject:object atIndex:index];
}

// MUST have this (or other remove accessor)
-(void)removeObjectFromChildrenAtIndex:(NSUInteger)index {
    [self.children removeObjectAtIndex:index];
}

// OPTIONAL - but a good idea.
-(void)replaceObjectInChildrenAtIndex:(NSUInteger)index withObject:(id)object {
    [self.children replaceObjectAtIndex:index withObject:object];
}

Swift doesn't appear to have these, so the only way to make KVO-compliant changes is via the mutable proxy object - described in the NSKeyValueCoding page. Here's a very quick example:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    dynamic var children: NSMutableArray = NSMutableArray()


    ////////////////////////////////////

    func applicationDidFinishLaunching(aNotification: NSNotification) {

        addObserver(self,
            forKeyPath: "children",
            options: .New | .Old,
            context: &Update)

        // Get the KVO/KVC compatible array
        var childrenProxy = mutableArrayValueForKey("children")

        childrenProxy.addObject(NSNumber(integer: 20)) // .Insertion

        childrenProxy.addObject(NSNumber(integer: 30)) // .Insertion

        childrenProxy.removeObjectAtIndex(1)  // .Removal
    }
}
Paul Patterson
  • 6,840
  • 3
  • 42
  • 56
  • Awesome answer. I will try adding an observer to an NSMutableArray and see what happens then. Thanks! – Felipe Ferri Apr 04 '15 at 17:38
  • I've updated my answer to include info about how to make KVO-compliant changes to ``NSMutableArray`` in *Swift*. – Paul Patterson Apr 04 '15 at 19:28
  • 1
    Thanks for the example. After I saw your first answer I tried to use KVO just by replacing the original array by a NSMutableArray but the observeValueForKeyPath function was never called. Using the method mutableArrayValueForKey did the trick. But what do you think would be the best way of checking for specific changes in an array in Swift? Falling back to NSMutableArray, though it works exactly as I wanted, seems to be like a hack, because I am not using Swift data structures. – Felipe Ferri Apr 05 '15 at 16:21
  • 1
    You can still use the `mutable indexed accessors` that Apple suggests, just make sure to wrap the `insertObject:atIndex:` in appropriate calls to `willChange:valuesAtIndexes:forKey:` / `didChange:valuesAtIndexes:forKey:` on your object. Not sure if that's better than using the proxy object, but it's an option. – Patrick May 05 '15 at 18:52