vib
vib

Reputation: 2274

Delete NSManagedObject from a tableView and correctly re-load view

I am trying to suppress a NSManagedObject from a table view.

I have a TableView controller that subclasses UITableViewController and that owns an additional property of type myContainer as well as an array of items.

@property (nonatomic, strong) myContainer * parentContainer;
@property(nonatomic, strong) NSArray * allItems;

myContainer is a NSManaged object that contains a NSSet * of objects of type myItem :

@interface myContainer : NSManagedObject
@property (nonatomic, strong) NSSet *subItems;

I have implemented the delete function like this :

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {

    NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
    myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
    [context deleteObject:selectedItem];
    [self viewWillAppear:NO];
    [self.view setNeedsDisplay];
}

}

(note that the instruction [context deleteObject:selectedItem] gives me a warning "Incompatible pointer types sending 'myObject *' to parameter of type 'NSManagedObject ' ; but I doubt this is relevant for what follows)

The method viewWillAppear is like this :

- (void)viewWillAppear:(BOOL)animated {
    self.allItems = [self.parentContainer.subItems allObjects];
    NSLog(@"%lu objects",(unsigned long)[self.allItems count]);
    [super viewWillAppear:animated];
    [self.tableView reloadData];
}

For the purpose of understanding what happens I also subclassed cellForRowAtIndexPath

- (UITableViewCell *)tableView:(UITableView *)tableView
     cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    // .. //
    myItem * selectedItem = self.allItems[indexPath.row];
    NSLog(@"%lu : %@",indexPath.row,[selectedItem description]);
}

Now assume I first load my table and it contains 3 objects. I receive the log :

3 objects
0 : item0
1 : item1
2 : item2

I swipe and delete the second item. The view updates but the corresponding cell does not disappear; it just becomes empty. The log reads like this :

3 objects
0 : item0
1 : (null)
2 : item2

Now I do anything else that will reload the view : either go the previous view and come back, go to a further view and come back, or delete a second object. Then everything is fine as expected and the log is :

2 objects
0 : item0
1 : item2

I have seen the two following questions that seem strongly related but they did not receive any answer that seemed to solve the problem : tablerow delete but still showon in table view and commitEditingStyle: delete data but continue to display row

EDIT I also know that there is a better way to suppress cell calling [tableView deleteRowsAtIndexPath ..] (and it works here) but I would really like to understand the output that I get and what coreData is doing with the objects behind my back...

I can't get my head around what is happening here. I tried several variations on the previous code but nothing seemed to solve my problem.

Upvotes: 1

Views: 238

Answers (2)

Ryan McLeod
Ryan McLeod

Reputation: 596

From what I see, I assume myContainer has a to-many relationship with myItem. That's good, because Core Data will automatically remove objects from that relationship after myItems objects are deleted, but not immediately. Unlike some on here have said, you don't have to save the context or refresh anything for these changes to be reflected in the relationship, you just need to call processPendingChanges on the context after your deletion, like so:

[context deleteObject:selectedItem];
[context processPendingChanges];

processPendingChanges will cause Core Data to do any relationship updates it still needs to do and will also fire any core data related notifications that might still be pending.

You do also need to make sure your allItems array is re-populated when these changes occur (which you do not currently appear to be doing) as Core Data will not deallocate objects that have been deleted or remove them from ordinary arrays. It'll just mark it as deleted and leave it taking up space in your array. Similar to what Erakk said in his answer, you should probably encapsulate this array re-population into the same method as your table view reload. You could also call processPendingChanges here instead of after your deletion to make sure that any and all pending changes are reflected in the relationship before pulling the objects out of the relationship. You don't need to manually update the subItems relationship in myContainer. That's Core Data's job.

- (void)reloadTableView {
    [self.parentContainer.managedObjectContext processPendingChanges];
    self.allItems = [[NSMutableArray alloc] initWithArray:[self.parentContainer.subItems allObjects]];
    [self.tableView reloadData];
}

As some others have said, you definitely don't want to be calling viewWillAppear: directly. You should move your code to a helper that is called from both places.

This line is also a potential issue:

 myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];

subItems is an unordered, to-many relationship, which means that you're not guaranteed to get the item that you really expect out of the array here, as the "allItems" method on NSSet could theoretically come back in any order. You should either store these objects in a (presumably sorted) array and access that instead, or you can change your subItems relationship into an ordered relationship and then change the property to an NSOrderedSet.

Hope this helps!

Upvotes: 2

Erakk
Erakk

Reputation: 922

To me, it looks like you are not updating the data array. You deleted item 1, but the array still has a reference to that NSManagedObject, which probably was tagged by coreData as "Deleted". That happens because the only part of the code you set the array is on viewWillAppear.

Instead, you should re-fetch the array whenever you delete an item and then reload the data.

This is also the reason whenever you go back to the previous/further view and come back, it works. because viewWillAppear is being called and updating the array.

UPDATE:

It seems to me that you get the objects from your parentContainer. this could mean that the parentContainer needs to be aware that you deleted an item to update the array too. I suggest you create a delegate, send a notification to it, or just create a public method in your parentContainer, and let this viewController call in whenever you reload the tableView.

The way i'd do is to encapsulate the logic to reload the tableView:

- (void)reloadTableView {
    [self.parentContainer reloadSubItems];
    self.allItems = [self.parentContainer.subItems allObjects];
    [self.tableView reloadData];
}

then change viewWillAppear to call it:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self reloadTableView];
}

and instead of explicitly calling the viewWillAppear in the commitEditingStyle:

 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
        myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
        [context deleteObject:selectedItem];
        [self reloadTableView];
        //you dont need to call setNeedsDisplay here to update the
        //tableView unless you do other stuff related to view frames
        //and the like, for example, you changed the tableView's
        //frame. in this case you would call setNeedsLayout.
    }
}

You would create the reloadSubItems in your parentContainer and add the fetching logic into it.

UPDATE 2:

instead of editing your parentContainer, you could just remove the item being deleted from the array. First you need to change allItems to be of type NSMutableArray instead of NSArray.

your reloadTableView method would look like this:

 - (void)reloadTableView {
    [self.parentContainer reloadSubItems];
    self.allItems = [[NSMutableArray alloc] initWithArray:[self.parentContainer.subItems allObjects]];
    [self.tableView reloadData];
}

and the commitEditingStyle:

 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
        myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
        [self.allItems removeObject:selectedItem];
        [context deleteObject:selectedItem];
        [self reloadTableView];
    }
}

This way you don't need to tell the parentContainer to update the items, since you will be deleting the item from the array yourself.

UPDATE 3:

according to the documentation on deleteObjects:, "When changes are committed, object will be removed from the uniquing tables. If object has not yet been saved to a persistent store, it is simply removed from the receiver."

So you need to save the context for your deletion to be committed to the store, or else the object will simply be flagged as "deleted"

Upvotes: 0

Related Questions