Reputation: 472
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
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
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
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
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
Reputation: 80271
Simply set the enabled
property of your switch to NO
until the updates are done.
Upvotes: 2