Showing and hiding UITableViewCell with UISwitch too fast crashes - ios

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?

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.

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

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;
}

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.

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];

Related

iOS - UITableView Delete All Rows Within A Section

I have a UITableView with expandable sections. When a user goes to another view, I need all the expanded sections to collapse, which I'll need to put in the viewWillDisappear method.
I've found solutions only on how to delete all rows from a table view at once, but is there a way to delete all the rows from a specific section?
EDIT:
I have figured out a solution, but I'm not sure if it's optimal or can lead to inefficiencies in the future. Whenever a cell is expanded, it gets added to an NSMutableIndexSet. So in my viewWillDisappear method, I iterate over the expanded sections like so:
-(void)viewWillDisappear:(BOOL)animated
{
if (expandedSections.count != 0) {
NSLog(#"COLLAPSING CALLED");
[self.tableView beginUpdates];
NSUInteger section = [expandedSections firstIndex];
do
{
NSInteger rows;
NSMutableArray *tmpArray = [NSMutableArray array];
rows = [self tableView:self.tableView numberOfRowsInSection:section];
[expandedSections removeIndex:section];
for (int i=1; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
[self.tableView deleteRowsAtIndexPaths:tmpArray withRowAnimation:UITableViewRowAnimationTop];
NSIndexPath *expandableCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:expandableCellIndexPath];
cell.accessoryView = [DTCustomColoredAccessory accessoryWithColor:[self.colorHolder objectAtIndex:section] type:DTCustomColoredAccessoryTypeRight];
section = [expandedSections indexGreaterThanIndex:section];
} while (section != NSNotFound);
[self.tableView endUpdates];
}
}
Please let me know if this is a good solution or, if I'm suspecting correctly, if this will lead to slower transitions between views in the future when there are more rows in each expanded section. Any help or advice would be appreciated. Thanks.
If you want to animate changes, you will need to first update your data source (to return 0 for number of rows in the section) then remove section and add section at the same index path in one transaction between [tv beginUpdates] [tv endUpdates]
Otherwise just update the data source and reload the table on your way back to the VC (if you don't want any animations)
I did not read this in detail but surely the for loop should start at zero.
for (int i=0; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
otherwise you will only delete all but the first cells in the section.

Attempt to create two animations for cell

I would like to create cell by expand and collapse. To do this i animate the cell by reloading. When i expand it works fine. But when i collapse it crashes and the reason is Attempt to create two animations for cell. I know Ive given same array paths and it wont reload two cell at the time. Is there a way to fix this.
I am new to coding, so i would be happy to get a simple solution.
Code:
The reason am using lastSelIPath is when first cell is expanded and touch the second cell the first cell would collapse and the second cell would expand.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath* lastSelIPath = [ NSIndexPath indexPathForRow:selectedIndex_ inSection:0 ];
[ tableView deselectRowAtIndexPath:indexPath animated:YES ];
if (selectedIndex_ == indexPath.row)
{
selectedIndex_ = -1;
}
else
{
selectedIndex_ = indexPath.row;
}
NSIndexPath* ipath = [ NSIndexPath indexPathForRow:indexPath.row inSection:0 ];
NSArray* indexPathArr = [ NSArray arrayWithObjects:lastSelIPath, ipath, nil ];
[ tableView beginUpdates ];
[ tableView reloadRowsAtIndexPaths:indexPathArr withRowAnimation:UITableViewRowAnimationAutomatic ];
[ tableView endUpdates ];
}
So what i did now I set a condition and reload the one i wanted. Is it a correct method to follow please help.
Modified code:
if ( lastSelIPath.row == ipath.row )
{
[tableView reloadRowsAtIndexPaths:#[ipath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else
{
[tableView reloadRowsAtIndexPaths:#[lastSelIPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:#[ipath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Crash is right, you are trying to attempt two animation. Your indexPathArr contains two objects. Do not create this array and try this:
[tableView reloadRowsAtIndexPaths:#[lastSelIPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:#[ipath] withRowAnimation:UITableViewRowAnimationAutomatic];
Let me know, if it helps
[tableView reloadRowsAtIndexPaths:#[newIndexPath, oldIndexPath] withRowAnimation:UITableViewRowAnimationNone];
If newIndexPath is same as oldIndexPath, it will crash in iOS8, but this is fixed in iOS9 and later.
You can use third party library for expanding cell.
https://github.com/bennyguitar/CollapseClick
& implement there delegate method as
-(int)numberOfCellsForCollapseClick
{
}
-(NSString *)titleForCollapseClickAtIndex:(int)index {
}
-(UIView *)viewForCollapseClickContentViewAtIndex:(int)index {
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (selectedTag==1) {
selectedIndex=indexPath.row;
[self performSelector:#selector(Write Your Action) withObject:nil];
}
NSMutableArray *modifiedRows = [NSMutableArray array];
if ([indexPath isEqual:self.expandedIndexPath]) {
[modifiedRows addObject:self.expandedIndexPath];
self.expandedIndexPath = nil;
}
else {
if (self.expandedIndexPath)
[modifiedRows addObject:self.expandedIndexPath];
self.expandedIndexPath = indexPath;
[modifiedRows addObject:indexPath];
}
// This will animate updating the row sizes
[tableView reloadRowsAtIndexPaths:modifiedRows withRowAnimation:UITableViewRowAnimationAutomatic];
// Preserve the deselection animation (if desired)
[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
Using Selected Tag write the condition of your cell expand action. may be it helps.

How do I animate moving an object between sections in a UITableView that uses predicates?

I have a UITableView that has two sections. Each section's datasource is a single array that uses a predicate to determine its contents. I've done this because there are other objects in the array that I don't want to display in the table but need to be able to save back out to Core Data once the user has finished playing with the table (it's interactive).
One of the interactions is tapping a switch on the cell that sets a property on the object populating that row. That particular property determines which section the object is displayed in. Here's how I do it right now:
EDIT: Added viewWillLayoutSubviews code to show how predicates are generated.
- (void)viewWillLayoutSubviews {
// read and sort initial activities
_activitiesSet = [_theEvent mutableSetValueForKey:#"activities"];
_activitiesArray = [[_activitiesSet allObjects] mutableCopy];
_predIncomplete = [NSPredicate predicateWithFormat:#"(isHidden == NO) AND (isComplete == NO)"];
_predComplete = [NSPredicate predicateWithFormat:#"(isHidden == NO) AND (isComplete == YES)"];
}
- (IBAction)completeSwitchTapped:(id)sender {
UIView *currentSuperview = [sender superview];
while (![currentSuperview isKindOfClass:[UITableViewCell class]] && [currentSuperview superview] != nil) {
currentSuperview = [currentSuperview superview];
}
NSIndexPath *indexPath = [self.itineraryTableView indexPathForCell:(UITableViewCell *)currentSuperview];
switch (indexPath.section) {
case kIncompleteActivitiesSection:
[[[_activitiesArray filteredArrayUsingPredicate:_predIncomplete] objectAtIndex:indexPath.row] setValue:#YES forKey:#"isComplete"];
break;
case kCompleteActivitiesSection:
[[[_activitiesArray filteredArrayUsingPredicate:_predComplete] objectAtIndex:indexPath.row] setValue:#NO forKey:#"isComplete"];
break;
default:
break;
}
[self.itineraryTableView reloadData];
}
This works just fine, but there's no animation. When I tap the switch on a row, it disappears from its section and appears in the other (due to the reloadData). This is really not very elegant.
How can I animate the row leaving the one section and sliding into the other?
I can't seem to wrap my brain around how I should formulate the deleteRowsAtIndexPaths and addRowsAtIndexPaths methods and still maintain the single, predicate filtered array in the background.
Thanks so much!!
Instead of sending reloadData to the table view, send it a moveRowAtIndexPath:toIndexPath: message.
I've been able to figure this out, mostly. Here's what I've come up with:
- (IBAction)completeSwitchTapped:(id)sender {
UIView *currentSuperview = [sender superview];
while (![currentSuperview isKindOfClass:[UITableViewCell class]] && [currentSuperview superview] != nil) {
currentSuperview = [currentSuperview superview];
}
NSIndexPath *indexPath = [self.itineraryTableView indexPathForCell:(UITableViewCell *)currentSuperview];
NSIndexPath *newIndexPath = [[NSIndexPath alloc] init];
switch (indexPath.section) {
case kIncompleteActivitiesSection: {
Activity *theActivity = [[_activitiesArray filteredArrayUsingPredicate:_predIncomplete] objectAtIndex:indexPath.row];
[[[_activitiesArray filteredArrayUsingPredicate:_predIncomplete] objectAtIndex:indexPath.row] setValue:#YES forKey:#"isComplete"];
unsigned long newIndex = [[_activitiesArray filteredArrayUsingPredicate:_predComplete] indexOfObject:theActivity];
newIndexPath = [NSIndexPath indexPathForRow:newIndex inSection:kCompleteActivitiesSection];
[self.itineraryTableView beginUpdates];
[self.itineraryTableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
[self.itineraryTableView endUpdates];
break;
}
case kCompleteActivitiesSection: {
Activity *theActivity = [[_activitiesArray filteredArrayUsingPredicate:_predComplete] objectAtIndex:indexPath.row];
[[[_activitiesArray filteredArrayUsingPredicate:_predComplete] objectAtIndex:indexPath.row] setValue:#NO forKey:#"isComplete"];
unsigned long newIndex = [[_activitiesArray filteredArrayUsingPredicate:_predIncomplete] indexOfObject:theActivity];
newIndexPath = [NSIndexPath indexPathForRow:newIndex inSection:kIncompleteActivitiesSection];
[self.itineraryTableView beginUpdates];
[self.itineraryTableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
[self.itineraryTableView endUpdates];
break;
}
default: {
break;
}
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
[self.itineraryTableView reloadData];
}
I'm not sure if the run loop delay is a good choice, but it's the right timing and it looks great.

Unable to remove all UITableView cells with animation

I can's seem to be able to remove all table rows. What seems to happen is that only the cells that are in view are removed and then the ones that were out of view move up. Also checked and my data source contains only the cels that are in view at the time of deletion. Heres the code
NSMutableArray *left = [[NSMutableArray alloc]init];
NSMutableArray *right = [[NSMutableArray alloc]init];
for(int i=0;i<dataSource.count;i++){
[dataSource removeObjectAtIndex:i];
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
if(i%2==0)
[left addObject:ip];
else
[right addObject:ip];
}
[_view.tableView beginUpdates];
[_view.tableView deleteRowsAtIndexPaths:[NSArray arrayWithArray:left]
withRowAnimation:UITableViewRowAnimationLeft];
[_view.tableView deleteRowsAtIndexPaths:[NSArray arrayWithArray:right]
withRowAnimation:UITableViewRowAnimationRight];
[_view.tableView endUpdates];
What happens now is that only the cells in view are removed and the rest pop in after the removal (also the issue seems to pop up since the dataSource only contains the rows that are in view for some reason). What i want is to remove all the rows. Any ideas?
The problem is in the for loop condition. The dataSource.count gets smaller each iteration. Try this:
for(int i=0;i<dataSource.count;i++){
NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
if(i%2==0)
[left addObject:ip];
else
[right addObject:ip];
}
[dataSource removeAllObjects];
try something like this:
[_view.tableView beginUpdates];
UITableViewRowAnimation animation;
for(int i = dataSource.count - 1; i>= 0; i--)
{
[dataSource removeObjectAtIndex: i];
NSIndexPath* ip = [NSIndexPath indexPathForRow: i
inSection: 0];
if(i % 2 == 0)
{
animation = UITableViewRowAnimationLeft;
}
else
{
animation = UITableViewRowAnimationRight;
}
[_view.tableView deleteRowsAtIndexPaths: #[ip]]
withRowAnimation: animation];
}
[_view.tableView endUpdates];
You can do this in easy way...
[dataSource removeAllObjects];
[_view.tableView reloadData];
i think that you can put your dataSource = #[]; and [self.tableview reloadData]; and remove all cell in your tableview

Add cell/data to the top of UITableView

I'm trying to create a simple chat application for iOS. It currently looks like this:
I want to change the order that the messages is displayed, i.e. display the latest message over the older messages. My current implementation looks like this:
// Datasource for the tableView
messages = [[NSMutableArray alloc] init];
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
[...]
// messageTextView is a contentView subView.
messageTextView.text = [messages objectAtIndex:indexPath.row];
[...]
- (IBAction)sendMessage:(id)sender
{
[messages addObject:messageField.text];
messageField.text = #"";
[self.tableView reloadData];
/*
I have tried the implementation below, but I always got an exception.
[self.tableView beginUpdates];
NSIndexPath *path1 = [NSIndexPath indexPathForRow:1 inSection:0];
NSArray * indexArray = [NSArray arrayWithObjects:path1,nil];
[self.tableView insertRowsAtIndexPaths:indexArray
withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
*/
}
Any tips on how to do this would be great.
Thanks.
Simple you need to insert the new UITableViewCell at the 0 index. Also modify your Datasource otherwise your app will crash. Below I show how to modify your UITableView. Modifying your datasource is easy.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[tableView beginUpdates];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
What you are essentially doing here is inserting the new chat message cell at the 0th index position. You could use a nice animation effect to make it appear or fade in.
There are various animations that you can use here-
UITableViewRowAnimationBottom
UITableViewRowAnimationFade
UITableViewRowAnimationMiddle
UITableViewRowAnimationNone
UITableViewRowAnimationRight
UITableViewRowAnimationTop
You can try.
[messages insertObject:messageField.text atIndex:0];
instead of [messages addObject:messageField.text];.
Swift Solution
Define array like this
var arrMessage = [AnyObject]()
and Button Action
#IBAction func btnSendMessageTapped(sender: AnyObject) {
arrMessage.insert(txtTypeMessage.text!, atIndex: 0)
txtTypeMessage.text = ""
self.tblMessage.reloadData()
}

Resources