Paul Wagener
Paul Wagener

Reputation: 472

Showing and hiding UITableViewCell with UISwitch too fast crashes

I've got a UITableView that presents some settings to the user. Some cells are hidden unless a UISwitch is in the 'On' position. I've got the following code:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return switchPush.on ? 6 : 1;
}

// Hooked to the 'Value Changed' action of the switchPush
- (IBAction)togglePush:(id)sender {
    NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:0];
    for(int i = 1; i <= 5; i++) {
        [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
    }

   [tableView beginUpdates];
    if(switchPush.on) {
        [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
    } else {
        [tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
    }
   [tableView endUpdates];
}

This works as expected, until I switch the UISwitch twice in rapid succession (by double tapping), in which case the application crashes with a

Invalid table view update. The application has requested an update to the table view that is inconsistent with the state provided by the data source.

I know that it is caused by the wrong return value of numberOfRowsInSection as the switch is back in its original position while the cell animation is still playing. I've tried disabling the toggle and hooking the code under other event handlers but nothing seems to prevent the crash. Using reloadData instead of the animation also solves to problem but I would prefer the nice animations.

Does anybody know a correct way of implementing this?

Upvotes: 6

Views: 2311

Answers (5)

Murmeltier
Murmeltier

Reputation: 613

So I also had this issue. You have to disable the UISwitch as soon as change your value. Then you do your updates inside the performBatchUpdates(iOS 11 and above!). As soon as your updates are finished you enable the UISwitch in the callback.

   //...method with value change or other control event:
    
    yourUISwitch.enabled = NO;
    
    //...method with your tableview inserts or deletes:
    
    [self.tableView beginUpdates];
    [self.tableView performBatchUpdates:^{
        [self.tableView insertRowsAtIndexPaths:yourIndexPath withRowAnimation:UITableViewRowAnimationFade];
    } completion:^(BOOL finished) {
        if(finished){
            yourUISwitch.enabled = YES;
        }
    }];
    
    [self.tableView endUpdates];

Upvotes: 0

Tom Abraham
Tom Abraham

Reputation: 400

UIControlEventValueChanged events occur even when a control's value doesn't actually change. so togglePush gets called even when the value of the switch doesn't change. when you quickly toggle the switch, you might not always go from on > off > on > off, etc. it's possible to go off > on > on > off.

so what's happening is that you're getting two ons in a row causing two insertSections one after the other. which is obviously bad.

to fix this, you need to remember what the previous state of the button was (in an ivar, maybe) and only perform the insert (or delete) if the new value (source.on) is different from the previous value.

Upvotes: 0

Giorgio
Giorgio

Reputation: 2220

Another (more elegant) solution at the problem is this:

I modified the Alan MacGregor - (IBAction)SwitchDidChange:(id)sender method in this way:

- (IBAction)SwitchDidChange:(UISwitch *)source {
     if (_showRows != source.on) {
         NSArray *aryTemp = [[NSArray alloc] initWithObjects:
                    [NSIndexPath indexPathForRow:1 inSection:0],
                    [NSIndexPath indexPathForRow:2 inSection:0],
                    [NSIndexPath indexPathForRow:3 inSection:0],
                    [NSIndexPath indexPathForRow:4 inSection:0],nil];
         [_tblView beginUpdates];
         _showRows = source.on;
         if (_showRows) {
             [_tblView insertSections:aryTemp withRowAnimation:UITableViewRowAnimationFade];
         }
         else {
             [_tblView deleteSections:aryTemp withRowAnimation:UITableViewRowAnimationFade];
         }
         [_tblView endUpdates];
     }
}

The other parts stay unchanged.

Upvotes: 3

Alan MacGregor
Alan MacGregor

Reputation: 511

I had this issue on mine and the way to avoid the crash is to not explicitly use the uiswitch, instead relay the information into a boolean, heres how I did it.

Add a boolean to the top of your implementation file

bool _showRows = NO;

Update your uiswitch code

- (IBAction)SwitchDidChange:(id)sender {

NSArray *aryTemp = [[NSArray alloc] initWithObjects:[NSIndexPath indexPathForRow:1 inSection:0],
                    [NSIndexPath indexPathForRow:2 inSection:0],
                    [NSIndexPath indexPathForRow:3 inSection:0],
                    [NSIndexPath indexPathForRow:4 inSection:0],nil];

if (_showRows) {
    _showRows = NO;
    _switch.on = NO;
    [_tblView  deleteRowsAtIndexPaths:aryTemp withRowAnimation:UITableViewRowAnimationTop];
}
else {
    _showRows = YES;
    _switch.on = YES;
    [_tblView insertRowsAtIndexPaths:aryTemp withRowAnimation:UITableViewRowAnimationBottom];
}
}

And finally update your numberOfRowsInSection

- (NSInteger)tableView:(UITableView *)tableView 
 numberOfRowsInSection:(NSInteger)section
{
if (section == 0) {

    if (_showRows) {
        return 5;
    }
    else {
        return 1;

    }      
}
return 0;  
}

Upvotes: 2

Mundi
Mundi

Reputation: 80271

Simply set the enabled property of your switch to NO until the updates are done.

Upvotes: 2

Related Questions