Corbin Dunn
Redwood Monkey

NSTableView Tips: Doing Animations with Core Data

Cocoa, Coding

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.

Previous Post
NSTableView Tips: View Controller per row
Next Post
Accelerometer woes

3
Leave a Reply

avatar
500
3 Comment threads
0 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
corbinDanielJimmy Recent comment authors
  Subscribe  
Notify of
Jimmy
Guest
Jimmy

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

Daniel
Guest
Daniel

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?

(c) 2008-2018 Corbin Dunn

Privacy Policy

Subscribe to RSS feeds for entries.

72 queries. 0.368 seconds.

Log in