Corbin's Treehouse - Corbin Dunn, Santa Cruz, CA
Plug Bug
Treehouse
Photography
Videos
Projects
Unicycling
About

NSTableView Tips: Doing Animations with Core Data


For my “Cyr Wheel Pattern Editor” app I am using CoreData and an NSTableView. However, I’m sort of “manually” doing bindings for the array content itself to get animations in the View Based NSTableView.

Here’s what my model looks like:

Screen Shot 2014-04-29 at 9.09.53 PM.png

The CDPatternSequence has a children array of CDPatternItems:

@interface CDPatternSequence : NSManagedObject

@property (nonatomic, retain) NSOrderedSet *children;

@end

Now, if you have Xcode generate the standard code you might have noticed the standard NSOrderedSet accessors aren’t implemented for you; this is a bug in CoreData, but here is a good post on stack overflow about it.

The minimal implementation I needed was this (note: CDPatternChildrenKey == @”children”):

- (void)removeChildrenAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
    NSMutableOrderedSet *tmpOrderedSet =
       [NSMutableOrderedSet orderedSetWithOrderedSet:[self mutableOrderedSetValueForKey:CDPatternChildrenKey]];
    [tmpOrderedSet removeObjectsAtIndexes:indexes];
    [self setPrimitiveValue:tmpOrderedSet forKey:CDPatternChildrenKey];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
}

- (void)insertObject:(CDPatternItem *)value inChildrenAtIndex:(NSUInteger)idx {
    NSIndexSet* indexes = [NSIndexSet indexSetWithIndex:idx];
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
    NSMutableOrderedSet *tmpOrderedSet =
      [NSMutableOrderedSet orderedSetWithOrderedSet:[self mutableOrderedSetValueForKey:CDPatternChildrenKey]];
    [tmpOrderedSet insertObject:value atIndex:idx];
    [self setPrimitiveValue:tmpOrderedSet forKey:CDPatternChildrenKey];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
}

- (void)insertChildren:(NSArray *)values atIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
    NSMutableOrderedSet *tmpOrderedSet =
       [NSMutableOrderedSet orderedSetWithOrderedSet:[self mutableOrderedSetValueForKey:CDPatternChildrenKey]];
    [tmpOrderedSet insertObjects:values atIndexes:indexes];
    [self setPrimitiveValue:tmpOrderedSet forKey:CDPatternChildrenKey];
    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:CDPatternChildrenKey];
}

The main reason to do this is to get the proper KVO notifications to know what is happening. My NSWindowController subclass is the delegate and datasource for my tableview. Each row is managed by an NSViewController, and as I previously I simply have an array of NSViewControllers that starts out with NSNull placeholders and lazily load them when the table requests them:

- (void)_resetPatternViewControllers {
    _patternViewControllers = NSMutableArray.new;
    for (NSInteger i = 0; i < self.document.patternSequence.children.count; i++) {
        [_patternViewControllers addObject:[NSNull null]]; // placeholder
    }
    [_tableView reloadData];
}

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return _patternViewControllers.count;
}

- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn
                                                              row:(NSInteger)row {
    CDPatternItemViewController *vc = [self _patternViewControllerAtIndex:row];
    // don't set the identifier so it isn't reused...
    return vc.view;
}

- (CDPatternItemViewController *)_patternViewControllerAtIndex:(NSInteger)index {
    id currentObject = [_patternViewControllers objectAtIndex:index];
    CDPatternItemViewController *result;
    if (currentObject == [NSNull null]) {
        CDPatternItem *item = [self.document.patternSequence.children objectAtIndex:index];
        result = [CDPatternItemViewController new];
        result.patternItem = item;
        [_patternViewControllers replaceObjectAtIndex:index withObject:result];
    } else {
        result = (CDPatternItemViewController *)currentObject;
    }
    return result;
}

In my CDPatternItemViewController XIB, I bind stuff to the “patternItem”, which is my CoreData model object.

So, here’s the fun part. I simply observe the children of my container model object and do updates to the table based on that. It makes it *really easy* to simply modify your model and have the table automatically reflect the changes you made. Again, I have a window controller subclass, so I do it here:

- (void)windowDidLoad {
    [super windowDidLoad];

    [self _resetPatternViewControllers]; // This is shown above

    // Watch for changes
    [self.document.patternSequence.children addObserver:self
              forKeyPath:CDPatternChildrenKey options:0 context:nil];

    // Drag and drop registration below… (snipped out)
}

Then, all the real work is done in the KVO callback.

- (void)_observeValueForChildrenChangeOfObject:(id)object change:(NSDictionary *)change
                                       context:(void *)context {
    NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
    switch (changeKind) {
        case NSKeyValueChangeSetting: {
            [self _resetPatternViewControllers];
            break;
        }
        case NSKeyValueChangeInsertion: {
//            if (_draggedRowIndexes != nil) break; // Done manually in the drag operation
            NSIndexSet *indexes = [change objectForKey:NSKeyValueChangeIndexesKey];
            if (indexes) {
                NSMutableArray *emptyObjects = NSMutableArray.new;
                for (NSInteger i = 0; i < indexes.count; i++) {
                    [emptyObjects addObject:[NSNull null]];
                }
                [_patternViewControllers insertObjects:emptyObjects atIndexes:indexes];
//                [NSAnimationContext beginGrouping];
//                [NSAnimationContext.currentContext setDuration:.3];
                [_tableView insertRowsAtIndexes:indexes withAnimation:NSTableViewAnimationEffectFade];
//                [NSAnimationContext endGrouping];
            } else {
                [self _resetPatternViewControllers];
            }
            break;
        }
        case NSKeyValueChangeRemoval: {
//            if (_draggedRowIndexes != nil) break; // Done manually in the drag operation
            NSIndexSet *indexes = [change objectForKey:NSKeyValueChangeIndexesKey];
            if (indexes) {
                [_patternViewControllers removeObjectsAtIndexes:indexes];

                [_tableView removeRowsAtIndexes:indexes withAnimation:NSTableViewAnimationEffectFade];
            } else {
                [self _resetPatternViewControllers];
            }
            break;
        }
        case NSKeyValueChangeReplacement: {
            NSIndexSet *indexes = [change objectForKey:NSKeyValueChangeIndexesKey];
            if (indexes) {
                [_patternViewControllers removeObjectsAtIndexes:indexes];
                // replace w/null
                NSMutableArray *emptyObjects = NSMutableArray.new;
                for (NSInteger i = 0; i < indexes.count; i++) {
                    [emptyObjects addObject:[NSNull null]];
                }
                [_patternViewControllers insertObjects:emptyObjects atIndexes:indexes];
                [_tableView reloadDataForRowIndexes:indexes columnIndexes:[NSIndexSet indexSetWithIndex:0]];
            } else {
                [self _resetPatternViewControllers];
            }
            break;
        }
        default: {
            NSAssert(NO, @"internal error: change not known");
            break;
        }
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:CDPatternChildrenKey]) {
        [self _observeValueForChildrenChangeOfObject:object change:change context:context];
    } else {
        NSAssert(NO, @"bad observation");
    }

}

The code above keeps the view controller array in sync with the model, and also keeps the table in sync with the model.

You can see I have some comments about drag and drop. I can write a post on how to do that, if people are interested.



3 Responses to “NSTableView Tips: Doing Animations with Core Data”

  1. Jimmy says:

    Thanks for all the NSTableView tips you have been doing lately. +1 on the drag and drop.

  2. Daniel says:

    Quick question: isn’t the (proxy) object returned by -mutableOrderedSetValueForKey: already taking care of posting the correct KVO notifications when mutating it or is there another bug in Core Data, preventing this to work as usual?

  3. corbin says:

    Hi Daniel,
    Yes, it should take care of them……but I don’t recall if it sent out the granular ones that I wanted it to send. I’d have to double check. But good question!
    crbin

(c) 2008-2012 Corbin Dunn

Corbin's Treehouse is powered by WordPress. Made on a Mac.

Subscribe to RSS feeds for entries and comments.

17 queries. 0.661 seconds.