NSUndoManager For Custom UITableViewCell Deletes

Discussion in 'iOS Programming' started by Darkroom, Sep 3, 2009.

  1. Darkroom Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #1
    i have custom UITableViewCells composed of their own delete buttons. i'm currently attempting to install undo with unlimited (or at least 20ish) levels with hackish results.

    this is so far the best i've come up with, but it's designed to only support one level of undo AND the code is super ghetto:

    Code:
    - (IBAction)deleteMeasurement:(id)sender
    	{
    	[COLOR="SeaGreen"]//Obtain Index Path Of Deleting Row[/COLOR]
    	NSIndexPath *indexPath = [self.measurementsTableView indexPathForCell:(UITableViewCell *)[[[sender superview] superview] superview]];
    	NSUInteger row = [indexPath row];
    	
    	[COLOR="SeaGreen"]//Backup Deleting Dictionary Entry As String Variables[/COLOR]
    	NSMutableDictionary *item = [self.measurementsDataArray objectAtIndex:row];
    	self.deletedDictionaryName = [NSString stringWithFormat:@"%@", [item objectForKey:@"name"]];
    	self.deletedDictionaryMeasurement = [NSString stringWithFormat:@"%@", [item objectForKey:@"measurement"]];
    	self.deletedDictionaryUnit = [NSString stringWithFormat:@"%@", [item objectForKey:@"unit"]];
    	
    	[COLOR="SeaGreen"]//Delete Dictionary Entry From Measurements Data Array[/COLOR]	
    	[self.measurementsDataArray removeObjectAtIndex:row];
    	[self.measurementsTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
    	
    	[[undoManager prepareWithInvocationTarget:self] undoDeleteMeasurementAtIndexPath:indexPath];
    	[undoManager setActionName:NSLocalizedString(SMConstTableViewDeleteButtonLabel, nil)];
    	}
    	
    - (void)undoDeleteMeasurementAtIndexPath:(NSIndexPath *)indexPath
    	{
    	NSMutableDictionary *dictionaryItem = [[NSMutableDictionary alloc] init];
    	[dictionaryItem setObject:[NSString stringWithFormat:@"%@", self.deletedDictionaryName] forKey:@"name"];
    	[dictionaryItem setObject:[NSString stringWithFormat:@"%@", self.deletedDictionaryMeasurement] forKey:@"measurement"];
    	[dictionaryItem setObject:[NSString stringWithFormat:@"%@", self.deletedDictionaryUnit] forKey:@"unit"];
    	[measurementsDataArray addObject:dictionaryItem];
    		
    	NSUInteger row = [indexPath row];
    	[measurementsDataArray insertObject:dictionaryItem atIndex:row];
    	[measurementsDataArray removeObjectAtIndex:row];
    	[dictionaryItem release];
    	}
    
    additionally, this code inserts the undone delete at the bottom of the table, whereas it would be more ideal to place it back to it's original index. tried some stuff with insertRowsAtIndexPaths but it made me crazy (crashed all the time!). what is the best way to achieve this functionality?
     
  2. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #2
    It's been a while since I used NSUndoManager but you seem to be missing some of the idea. The data pushed onto the undo stack should be complete. So you don't just push an indexPath on the undo stack. You push an object that includes all of the description of the data to be added. In your case you should push something like an NSDictionary that contains all of the data for the relevant row, including the indexPath. If you do that then you can have multiple levels of undo.

    Also, the code in your second method seems confused about where the row data goes. It goes at the row index in the array. Just put it there once and stop messing around with it.
     
  3. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #3
    my understanding is that NSUndoManager is only pointer to another method, and it's the code in the other method that manages the undo. so at this point, i'm thinking that i'll create an undo stack that is another array of dictionaries of deleted entries, which can be undone the the opposite order which they were deleted.

    whenever i wrote the following it would throw an exception. i believe it was caused by the array not having enough rows to place it, but i wasn't able to figure it out:

    Code:
    NSUInteger row = [indexPath row];
    [self.measurementsTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    
    please help.
     
  4. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #4
    Look at the Undo Architecture guide again.

    Sort of. The undo manager manages the undo stack. You tell it what data are to be used and what selector. It keeps your data in its stack. It calls you back with the data at a later time. In your case you should push a dictionary that represents all the data in the row and also holds the indexPath for the row. Your method should then add that row to your data model and to the table.

    There's no such thing as an array not having enough rows. You probably need to wrap this code in beginUpdates/endUpdates. If that doesn't work show the exception that's thrown.
     
  5. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #5
    ok. i have it now working, but there's bizarre, unexpected behavior with the method to undo-delete the tableview cell. everything get's called correctly, except it doesn't refresh the tableview (eventhough the tableview refresh method is being called).

    the reason i refreshing the tableview is so the cells becomes centered within the tableview. depending on how many entries there are, the header changes. interestinly, while the tableview will reload after deleting and re-undoing a delete, it will not refresh after an undo-delete.

    i haven't yet tried to refactor this code, so appoligies in advance:

    Code:
    - (IBAction)deleteMeasurement:(id)sender
    	{
    	[self deleteOrRedoDelete:sender ofDictionaryItem:nil atIndex:nil];
    	}
    
    - (void)deleteOrRedoDelete:(id)sender ofDictionaryItem:(NSMutableDictionary *)dictionaryItem atIndex:(NSIndexPath *)indexPath
    	{
    	if (sender != nil)
    		{
    		//Original Delete
    		NSIndexPath *indexPath = [self.measurementsTableView indexPathForCell:(UITableViewCell *)[[[sender superview] superview] superview]];
    		NSUInteger row = [indexPath row];
    		NSMutableDictionary *dictionaryItem = [measurementsDataArray objectAtIndex:row];
    		
    		[[undoManager prepareWithInvocationTarget:self] undoDeleteMeasurement:dictionaryItem atIndex:indexPath];
    		[undoManager setActionName:[NSString stringWithFormat:NSLocalizedString(SMConstTableViewDeleteButtonLabel, nil)]];
    		
    		[self.measurementsDataArray removeObjectAtIndex:row];
    		[self.measurementsTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
    		}
    		else
    		{
    		//Redo Delete
    		NSUInteger row = [indexPath row];
    		NSMutableDictionary *dictionaryItem = [measurementsDataArray objectAtIndex:row];
    		
    		[[undoManager prepareWithInvocationTarget:self] undoDeleteMeasurement:dictionaryItem atIndex:indexPath];
    		[undoManager setActionName:[NSString stringWithFormat:NSLocalizedString(SMConstTableViewDeleteButtonLabel, nil)]];
    		
    		[self.measurementsDataArray removeObjectAtIndex:row];
    		[self.measurementsTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];		
    		}
    	
    	//If Data Array Is Empty, Disable Delete Button And Display No Measurements Label
    	if ([measurementsDataArray count] == 0)
    		[self toggleNoMeasurementsLabel:YES];
    		
    	[self performSelector:@selector(reloadTableViewData:) withObject:(measurementsTableView) afterDelay:kAnimationShowHideInfoAndDeleteAndNoMeasurementsLabel];
    	}
    
    - (void)undoDeleteMeasurement:(NSMutableDictionary *)dictionaryItem atIndex:(NSIndexPath *)indexPath
    	{
    	NSUInteger row = [indexPath row];
    	[self.measurementsDataArray insertObject:dictionaryItem atIndex:row];
    	[self.measurementsTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
    
    	[[undoManager prepareWithInvocationTarget:self] deleteOrRedoDelete:nil ofDictionaryItem:dictionaryItem atIndex:indexPath];
    	[undoManager setActionName:[NSString stringWithFormat:NSLocalizedString(SMConstTableViewDeleteButtonLabel, nil)]];
    		
    	//If Data Is Not Empty, Enable Delete Button And Hide No Measurements Label
    	if ([measurementsDataArray count] > 0)
    		[self toggleNoMeasurementsLabel:NO];
    	
    [COLOR="Red"]//THIS GETS CALLED, BUT DOESN'T REPOSITION THE TABLE VIEW CELLS[/COLOR]
    	[self performSelector:@selector(reloadTableViewData:) withObject:(measurementsTableView) afterDelay:kAnimationShowHideInfoAndDeleteAndNoMeasurementsLabel];
    	}
    	
    - (void)reloadTableViewData:(UITableView *)tableView
    	{
    	[tableView reloadData];
    	}
    
    any thoughts?
     
  6. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #6
    Use beginUpdates/endUpdates around your insertRowsAtIndexPaths:/deleteRowsAtIndexPaths calls.
     
  7. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #7
    didn't change anything, unfortunately. still not reloading the tableview after undo-delete. :confused:
     
  8. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #8
    Is the tableView variable correct?

    Do any of the tableview delegate/datasource methods get called when you call reloadData?
     
  9. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #9
    the reload data method is getting called - i've included an NSLog as a test, but it's just not repositioning the entries. it's the exact same line of code used for deleting and redo-delete, but delete doesn't seem to work. the only difference i can tell between the ones that do work and the one that doesn't is they are in 2 separate methods. that must have something to do with it.
     
  10. Darkroom thread starter Guest

    Darkroom

    Joined:
    Dec 15, 2006
    Location:
    Montréal, Canada
    #10
    ok. to make it work, i had to remove the animation that triggered the manual resetting of the table view's content inset from the (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath (probably shouldn't have been in there in the first place) and set it's own method that would be called from within beginUpdates/endUpdates, instead of reloading the table's data after every delete/undo/redo.

    Code:
    [measurementsTableView beginUpdates];
    [measurementsTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
    [self tableViewContentInsetAnimation];
    [measurementsTableView endUpdates];	
    
    thanks again for your help. :)
     

Share This Page