Augustyniak / RATreeView

Library providing easy-to-use interface for displaying tree structures on iOS and tvOS.
MIT License
2.49k stars 464 forks source link

NSFetchedResultsController compatibility #119

Open sarn opened 9 years ago

sarn commented 9 years ago

The NSFetchedResultsControllerDelegate method didChangeObjectinforms about changes to the data source, like inserts, deletes, updates and moves that occurred in a CoreData object store.

A standard UITableView can react to those changes by providing methods which one can use to inform the UITableView. A simple way to implement this would look like

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type)
    {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

RATreeViewseems to lack 100% compatibility. I found the following two methods that support inserts and deletes quite well:

- (void)insertItemsAtIndexes:(NSIndexSet *)indexes inParent:(id)parent withAnimation:(RATreeViewRowAnimation)animation;
- (void)deleteItemsAtIndexes:(NSIndexSet *)indexes inParent:(id)parent withAnimation:(RATreeViewRowAnimation)animation;

But it does not work for the NSFetchedResultsChangeMove type. An implementation similar to the UITableView by chaining delete and insert right after each other leads to inconsistencies or crashes.

I also tried the following RATreeView method

- (void)moveItemAtIndex:(NSInteger)oldIndex inParent:(id)oldParent toIndex:(NSInteger)newIndex inParent:(id)newParent;

but unfortunately this moves a cell to the newIndex and right back to the original index, because the NSFetchedResultsChangeMove gets triggered multiple times in a run. We get informed about all moves of all cells. First you get informed that the cell should get moved to the newIndex, then you get informed that the previous cell, that was occupying newIndex, should get moved away from that. Basically moveItemAtIndex: seems not to be designed for this use case.

Do I miss something? Is there any other way to get RATreeView working with NSFetchedResultsControllerDelegate?

I would be happy to contribute to RATreeView to make this possible, if needed. But I lack knowledge of the inner design of RATreeView and would need some insight of the original developer.

Augustyniak commented 9 years ago

Hey, Thanks for report. Do you use beginUpdates and endUpdates method of the RATreeView when you perform batched updates?

sarn commented 9 years ago

Yes I do.

Augustyniak commented 9 years ago

@sarn Can you verify whether NSFetchedResultsController doesn't work correctly with version 1.0.3?

Note: RATreeView was written in a way that it should work correctly with NSFetchedResultsController. If this is still not the case, I will try to investigate the issue (despite the fact it can take some time as I am pretty busy now).

sarn commented 9 years ago

@Augustyniak Thanks for the update, I verified with 1.0.3 and still see the same thing. My test uses only a few items without any childs to make things simple. Simple inserts and deletes work fine as long as items I add or remove are at the end of the list and no NSFetchedResultsChangeMove is involved for reordering items.

Things don't work as expected if any reordering happens, even simple reorderings, like "move one item one row up". The tree is not or only partially updated and does not seem to move all items correctly. Depending on the test case, some items got moved while others are not.

My NSFetchedResultsChangeMove handling is pretty simple and looks like this:

[self.treeView deleteItemsAtIndexes:[NSIndexSet indexSetWithIndex:indexPath.row] inParent:nil withAnimation:RATreeViewRowAnimationNone];
[self.treeView insertItemsAtIndexes:[NSIndexSet indexSetWithIndex:newIndexPath.row] inParent:nil withAnimation:RATreeViewRowAnimationNone];
lostirc commented 9 years ago

I am seeing pretty good results while using core data, my handling is pretty simple as I'm only working with a single leaf tree. The only issue I'm currently working through is handling insertions and deletions to children nodes. When you add a child to a root object the change method is invoked with the parent object and an update action. Both index paths are identical as well so I'm struggling to come up with a way to figure out what type of operation happened under the parent.

[_treeView reloadRowsForItems:@[anObject] withRowAnimation:RATreeViewRowAnimationAutomatic]; does not refresh the parent objects children. The only way I can seem to get the parent to reload its children is to manually open and close the tree.

- (void) controllerWillChangeContent:(NSFetchedResultsController    *)controller {
    [_treeView beginUpdates];
}

- (void) controller:(NSFetchedResultsController *)controller
        didChangeObject:(id)anObject
        atIndexPath:(NSIndexPath *)indexPath
        forChangeType:(NSFetchedResultsChangeType)type
       newIndexPath:(NSIndexPath *)newIndexPath {

switch (type){

    case NSFetchedResultsChangeInsert:
        [_treeView insertItemAtIndex:newIndexPath.row
                            inParent:nil
                       withAnimation:RATreeViewRowAnimationAutomatic];
        break;
    case NSFetchedResultsChangeDelete:
        [_treeView deleteItemsAtIndexes:[NSIndexSet indexSetWithIndex:(NSUInteger) newIndexPath.row]
                               inParent:nil
                          withAnimation:RATreeViewRowAnimationAutomatic];
        break;
    case NSFetchedResultsChangeMove:
        [_treeView moveItemAtIndex:indexPath.row
                          inParent:nil
                           toIndex:newIndexPath.row
                          inParent:nil];
        break;
    case NSFetchedResultsChangeUpdate: {

        [_treeView reloadRowsForItems:@[anObject] withRowAnimation:RATreeViewRowAnimationAutomatic];

        break;
    }
}

}

    - (void) controllerDidChangeContent:(NSFetchedResultsController *)controller {

    [_treeView endUpdates];
}