编辑模式下的 UITableView - 按 Delete 会使我的应用程序崩溃

Posted

技术标签:

【中文标题】编辑模式下的 UITableView - 按 Delete 会使我的应用程序崩溃【英文标题】:UITableView In Edit Mode - Pressing Delete Makes My App Crash 【发布时间】:2011-03-23 19:21:05 【问题描述】:

我的 UITableView 有一个编辑按钮,它添加了一个“插入行”(带有绿色加号)。如果我在编辑模式下尝试删除一行,我的应用程序会因“NSRangeException”而崩溃。在不处于编辑模式时删除一行(使用滑动删除)很好。

我知道这与表格视图认为它有多少行有关 - 让插入行和滑动删除都在同一个表格中工作是一场噩梦(对于像我这样的初学者),因为两者都滑动并按下编辑按钮将表格置于“编辑模式”,但编辑按钮会添加一行,而滑动则不会。

我一直在尝试使用几个 Ivar 来克服这个问题,但显然我在某个地方出错了。有经验的人能告诉我哪里出错了吗? (我敢肯定我的方法太复杂了!)。

编辑 - 应 Chiefly Izzy 的要求,我已将整个未经编辑的 .m 文件放在这里。

//
//  ChecklistsViewController.m
//  Check Box
//
//  Created by Ric on 18/03/2011.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "ChecklistsViewController.h"
#import "Checklist.h"

@interface ChecklistsViewController (private)
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
- (void)addingView;
@end


@implementation ChecklistsViewController

@synthesize category, managedObjectContext, fetchedResultsController;


- (id)initWithStyle:(UITableViewStyle)style

    self = [super initWithStyle:style];
    if (self) 
        editingFromSwipe = NO;
        tableIsEditing = NO;
        NSLog(@"tableIsEditing = NO (init) \n");
        NSLog(@"Not Editing From Swipe (init) \n");
    
    return self;


- (void)dealloc

    [category release];
    [managedObjectContext release];
    [fetchedResultsController release];
    [super dealloc];


- (void)didReceiveMemoryWarning

    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];

    // Release any cached data, images, etc that aren't in use.


#pragma mark - View lifecycle

- (void)viewDidLoad

    [super viewDidLoad];

    editingFromSwipe = NO;
    tableIsEditing = NO;
    NSLog(@"tableIsEditing = NO (viewDidLoad) \n");
    NSLog(@"Not Editing From Swipe (viewDidLoad) \n");

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    self.navigationItem.rightBarButtonItem = self.editButtonItem;    
    self.tableView.allowsSelectionDuringEditing = YES;



- (void)viewDidUnload

    [super viewDidUnload];
    // Relinquish ownership of anything that can be recreated in viewDidLoad or on demand.
    // For example: self.myOutlet = nil;



- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation

    // Return YES for supported orientations
    return (interfaceOrientation == UIInterfaceOrientationPortrait);



#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

    return [[self.fetchedResultsController sections] count];


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

    NSLog(@"numberOfRowsInSection HAS BEEN CALLED");
    id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
    int rows = [sectionInfo numberOfObjects];

    if (self.editing) 
        if (!editingFromSwipe && tableIsEditing) 
            NSLog(@"Returning extra row - editing not from swipe \n");
            return rows +1;
        
        NSLog(@"Returning normal rows - editing from swipe \n");
        return rows;
    
    NSLog(@"Returning normal rows - not editing - and setting tableIsEditing back to NO \n"); 
    tableIsEditing = NO;
    return rows;


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) 
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
    

    // Configure the cell...
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;


    NSLog(@"Should go into if statement here! \n");

    if (tableView.editing)  //
        if ((indexPath.row == 0) && (!editingFromSwipe)) 
            NSLog(@"Configuring Add Button Cell while editing \n");
            cell.textLabel.text = @"Add New Checklist";
            cell.detailTextLabel.text = nil;
        
        else 
            NSLog(@"Configuring other cells while editing \n");
            [self configureCell:cell atIndexPath:indexPath];
        

    
    else 
        NSLog(@"Configuring Cell Normally While Not Editing \n");
        [self configureCell:cell atIndexPath:indexPath];
    


    return cell;



// Override to support conditional editing of the table view.
/*
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath

    // return (indexPath.row != 0);
    return YES;

*/


// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath

    if (editingStyle == UITableViewCellEditingStyleDelete)
    
        // Delete the managed object for the given index path
        NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];        
        [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

        // Save the context.
        NSError *error = nil;
        if (![context save:&error])
        
            /*
             Replace this implementation with code to handle the error appropriately.

             abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
             */
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        
        
    else if (editingStyle == UITableViewCellEditingStyleInsert) 
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        [self addingView];

       



/*
// Override to support rearranging the table view.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath


*/

/*
// Override to support conditional rearranging of the table view.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath

    // Return NO if you do not want the item to be re-orderable.
    return YES;

*/

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath 
    int row = indexPath.row;

    if (self.editing && row == 0) 
        if (!editingFromSwipe && tableIsEditing) 
            NSLog(@"Not from swipe so making first row style insert button \n");
            return UITableViewCellEditingStyleInsert;
        
        else if (editingFromSwipe)  
            NSLog(@"Is from swipe so making first row delete rather than insert \n");
            return UITableViewCellEditingStyleDelete;
        

    
    NSLog(@"Not editing or not first row so making cell style normal \n");
    return UITableViewCellEditingStyleDelete;



- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath

    editingFromSwipe = YES;
    // NSLog(@"Editing From Swipe (just swiped) \n");
    [super tableView:tableView willBeginEditingRowAtIndexPath:indexPath];


- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath

    [super tableView:tableView didEndEditingRowAtIndexPath:indexPath];
    editingFromSwipe = NO;
    // NSLog(@"Not Editing From Swipe (just unswiped) \n");



- (void)setEditing:(BOOL)editing animated:(BOOL)animated

    [super setEditing:editing animated:animated];

    NSArray *addRow = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:0 inSection:0], nil];
    [self.tableView beginUpdates];

    if (!editingFromSwipe) 
        if (editing) 
            NSLog(@"Editing called while swipe off, so adding insert row \n");
            tableIsEditing = YES;
            NSLog(@"tableIsEditing = YES (setEditing:Animated:)");
            [self.tableView insertRowsAtIndexPaths:addRow withRowAnimation:UITableViewRowAnimationLeft];
        
        else 
            [self.tableView deleteRowsAtIndexPaths:addRow withRowAnimation:UITableViewRowAnimationLeft];
         
    
    [self.tableView endUpdates];



#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

    if (indexPath.row != 0) 
        // Navigation logic may go here. Create and push another view controller.
        /*
         <#DetailViewController#> *detailViewController = [[<#DetailViewController#> alloc] initWithNibName:@"<#Nib name#>" bundle:nil];
         // ...
         // Pass the selected object to the new view controller.
         [self.navigationController pushViewController:detailViewController animated:YES];
         [detailViewController release];
         */
    



#pragma mark - Data


- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath

    Checklist *aChecklist = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = aChecklist.name; // description];
    cell.detailTextLabel.text = aChecklist.category.name; // description];



- (void) addingView// :(id)sender

    // Turn off edit mode if it is on.
    //if (self.editing) 
    //    [self.navigationController setEditing:NO animated:YES];
    //

    //Create the root view controller for the navigation controller
    AddingViewController *viewController = [[AddingViewController alloc] initWithNibName:@"AddingViewController" bundle:nil];

    viewController.delegate = self;
    viewController.title = @"Add Checklist";

    // Create the navigation controller and present it modally
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    [self presentModalViewController:navigationController animated:YES];

    viewController.textLabel.text = @"Enter new checklist name";

    [navigationController release];
    [viewController release];



#pragma mark - AddingViewDelegate


- (void)addingViewController:(AddingViewController *)addingViewController didAdd:(NSString *)itemAdded

    if (itemAdded != nil) 

        // Turn off editing mode.
        if (self.editing) [self.navigationController setEditing:NO animated:NO];

        // Add the category name to our model and table view.

        // Create a new instance of the entity managed by the fetched results controller.
        NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
        NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
        Checklist *newChecklist = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];

        [category addChecklistsObject:newChecklist];

        newChecklist.name = itemAdded;        
        // [newChecklist setDateStamp:[NSDate date]];

        // Save the context.
        NSError *error = nil;
        if (![context save:&error])
        
            /*
             Replace this implementation with code to handle the error appropriately.

             abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
             */
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        


    

    [self dismissModalViewControllerAnimated:YES];



#pragma mark - Fetched results controller

- (NSFetchedResultsController *)fetchedResultsController

    if (fetchedResultsController != nil)
    
        return fetchedResultsController;
    

    // Set up the fetched results controller.

    // Create the fetch request for the entity.
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Checklist" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];
    // Set 4* the predicate so we only see checklists for this category.
    NSPredicate *requestPredicate = [NSPredicate predicateWithFormat:@"category.name = %@", self.category.name];
    [fetchRequest setPredicate:requestPredicate];    
    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];    
    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];    
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".    

    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
                                                                                                managedObjectContext:self.managedObjectContext 
                                                                                                  sectionNameKeyPath:nil 
                                                                                                           cacheName:nil];
    aFetchedResultsController.delegate = self;
    self.fetchedResultsController = aFetchedResultsController;


    [aFetchedResultsController release];
    [fetchRequest release];
    [sortDescriptor release];
    [sortDescriptors release];

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error])
    
       // handle the error properly!
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    

    return fetchedResultsController;
 


#pragma mark - Fetched results controller delegate


- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller

    [self.tableView beginUpdates];



- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type

    switch(type)
    
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
    



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

    UITableView *tableView = self.tableView;

    switch(type)
    

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;

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

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

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



- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller

    [self.tableView endUpdates];



/*
 // Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed. 

 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
 
 // In the simplest, most efficient, case, reload the table view.
 [self.tableView reloadData];
 
 */




@end


编辑:根据 Erik B 的建议,这是我的 'tableView:commitEditingStyle:forRowAtIndexPath' 方法中的新代码:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath

    if (editingStyle == UITableViewCellEditingStyleDelete)
    
        NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];

        int numberOfRows = [self tableView:tableView numberOfRowsInSection:indexPath.section];
        int rowBeingDeleted = indexPath.row +1;

        if (tableIsEditing && !editingFromSwipe && numberOfRows == rowBeingDeleted) 
            [context deleteObject:[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section]]];
        
        else 
            [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
        

        // context saving code    
          

应用程序不再崩溃,滑动删除仍然可以,但如果在编辑模式下尝试删除最后一行,它会删除倒数第二行。所有其他行都很好。 (顺便说一句,我的插入行在顶部)。

【问题讨论】:

Ric,你能发布整个 .m 文件吗?你的 tableView:commitEditingStyle:forRowAtIndexPath: 看起来不错。附言不要在代码中使用 abort()。 @Chiefly Izzy - 我已经用整个 .m 文件替换了上面的第一个代码段。任何帮助将不胜感激! abort() 只是 Apple 新 CoreData 文件样板的一部分——我还没有机会替换(咳咳,了解一下)错误代码。 【参考方案1】:

这就是我们所知道的:你在这一行得到NSRangeException

[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

deleteObject: 不会抛出 NSRangeException,所以我们知道

[self.fetchedResultsController objectAtIndexPath:indexPath]

导致崩溃。最可能的原因是您在添加“插入行”时没有考虑到表格视图的索引路径与fetchedResultsController 的索引路径不同。

如果我是对的,它只会在您删除最后一行时崩溃。

这就是你修复它的方法:

[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section]]

请记住,只有在“插入行”可见时才应更改索引路径。

编辑:

删除插入行正下方的行可以正常工作。我可以使用滑动删除来删除任何行,并且使用编辑按钮我可以删除除最后一行之外的任何行(这会删除它上面的行)。

if (tableIsEditing && !editingFromSwipe && numberOfRows == rowBeingDeleted) 
    [context deleteObject:[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section]]];
 else 
    [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

所以这就是导致该错误的原因:

numberOfRows == rowBeingDeleted

只有在索引路径是最后一行时才更改索引路径。这很令人困惑,但也许您不应该更改索引路径?尝试用NO 替换numberOfRows == rowBeingDeleted,看看会发生什么。

编辑 2:如果你改为这样做会发生什么?

if (tableIsEditing && !editingFromSwipe) 
    [context deleteObject:[self.fetchedResultsController objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section]]];
 else 
    [context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];

【讨论】:

再次感谢 Erik - 我学到了很多东西! :-) 我现在不在电脑旁,但我明天会试试这个。当我在这里的时候,我希望你不介意我再次强加给你一些建议。我已经使用这两个 ivars('editingFromSwipe' 和 'tableIsEditing')来跟踪我的表中有多少行,它似乎工作,但我觉得我过于复杂了(有 2 个 ivars 的 bool值正在被重置)。有没有一种更优雅的方式可以让我的插入行和滑动删除效果更好?我敢肯定只用一个 ivar 一定是可能的。 @Erik B,我尝试合并该代码(只要按下编辑按钮并且用户尝试删除最后一行时调用它)并且它已经修复了崩溃,但现在倒数第二行被删除!我已经编辑了我的问题并添加了新代码。 @Ric Levy,如果删除插入行正下方的行会怎样? @Erik B,删除插入行正下方的行可以正常工作。我可以使用滑动删除来删除任何行,并且使用编辑按钮我可以删除除最后一行之外的任何行(而是删除它上面的行)。 @Ric Levy,这听起来很奇怪,但如果它按照您描述的方式工作,那么有一种简单的方法可以解决它。我会更新我的答案给你看。

以上是关于编辑模式下的 UITableView - 按 Delete 会使我的应用程序崩溃的主要内容,如果未能解决你的问题,请参考以下文章

编辑模式下的 UITableView 在 iPad 上不显示复选标记

UITableview 编辑模式时不出现左删除图标

编辑模式下的 UITableViewCell 选择。

在编辑模式下按下删除按钮时 UITableView 崩溃

用户点击编辑按钮时重做 UITableView 布局?

vim编辑和命令模式下的实践