ios: UITableView reloadData not working until scroll - ios

I've noticed a few questions similar to this one on SO but they all seem to end in the person was calling
[self.tableView reloadData]
Outside of the main thread.
My problem is, I'm calling this on the main UI thread and I still don't get the new rows loaded until I like tap the screen or scroll a little bit. Here's what I'm doing: When I enter my ViewController the table displays fully and correctly. After pressing a button on the view I call my external db and load in some new rows to be added to the table. After adding these to my data source, I call reloadData and all of the new rows are blank. The couple rows that still fit on the screen that were already part of the table still show, but nothing new. When I touch the screen or scroll a little bit all of the new cells pop up.
Here's my code starting from when my call to the server finishes:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if(connection == self.searchConnection) {
..... some code ......
if(successful) {
// Adding new data to the datasource
[self.locations removeObjectAtIndex:[[NSNumber numberWithInt:self.locations.count - 1] integerValue]];
[self.cellTypeDictionary removeAllObjects];
for(int i = 0; i < [jsonLocations count]; ++i) {
NSDictionary *locationDictionary = jsonLocations[i];
BTRLocation *location = [BTRLocation parseLocationJSONObject:locationDictionary];
[self.locations addObject:location];
}
if(self.currentNextPageToken != nil && ![self.currentNextPageToken isEqual:[NSNull null]] && self.currentNextPageToken.length > 0) {
BTRLocation *fakeLocation = [[BTRLocation alloc] init];
[self.locations addObject:fakeLocation];
}
[self determineCellTypes];
if([self.locations count] < 1) {
self.emptyView.hidden = NO;
} else {
... some code .....
if(self.pendingSearchIsNextPageToken) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[NSNumber numberWithInt:countBefore] integerValue]
inSection:[[NSNumber numberWithInt:0] integerValue]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
} else {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[NSNumber numberWithInt:0] integerValue]
inSection:[[NSNumber numberWithInt:0] integerValue]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
}
}
} else {
if(invalidAccessToken) {
// TODO: invalidAccessToken need to log out
}
}
You can see I even added
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
Even though I know connectionDidFinishLoading is called on the main thread. But I figured I'd just try it anyway.
I can see no reason for why this isn't working.

the proper way is:
[self.tableView reloadData];
__weak MyViewController* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong MyViewController *strongSelf = weakSelf;
[strongSelf.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
Sometimes I found that you add after the reloadData:
[self.tableView layoutIfNeeded];

Related

How can I make a method act as a tap on a MKAnnotation?

I have two views. One is a map view and another is a table view of all of the titles of the MKAnnotations. Right now I am able to call a method on the table view from the tap of the MKAnnotation. But what I want to also do is call a method from the tap of an object on the table view that acts as a tap on the MKAnnotation. Help please?
Here is the table view code that the MKAnnotation tap uses to select an object on the table view.
- (void)highlightCellForPost:(PAWPost *)post {
// Find the cell matching this object.
NSUInteger index = 0;
for (PFObject *object in [self objects]) {
PAWPost *postFromObject = [[PAWPost alloc] initWithPFObject:object];
if ([post isEqual:postFromObject]) {
// We found the object, scroll to the cell position where this object is.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:PAWWallPostsTableViewMainSection];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
return;
}
index++;
}
Here's is the code for the MKAnnotation that calls the above method.
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
id<MKAnnotation> annotation = [view annotation];
if ([annotation isKindOfClass:[PAWPost class]]) {
PAWPost *post = [view annotation];
[self.wallPostsTableViewController highlightCellForPost:post];
} else if ([annotation isKindOfClass:[MKUserLocation class]]) {
// Center the map on the user's current location:
CLLocationAccuracy filterDistance = [[NSUserDefaults standardUserDefaults] doubleForKey:PAWUserDefaultsFilterDistanceKey];
MKCoordinateRegion newRegion = MKCoordinateRegionMakeWithDistance(self.currentLocation.coordinate,
filterDistance * 2.0f,
filterDistance * 2.0f);
[self.mapView setRegion:newRegion animated:YES];
self.mapPannedSinceLocationUpdate = NO;
}
}
So I essentially want to do what I've done above but in reverse order.
You can simply call selectAnnotation on your map view in didSelectRowAtIndexPath.
Firstly, I would create a new NSArray property postObjects which contains all of the PFObjects in self.objects already converted to PAWPost objects rather than having to continually iterate the objects array and create new PAWPost objects. Once you do this, your first method becomes the much simpler -
- (void)highlightCellForPost:(PAWPost *)post {
// Find the cell matching this object.
NSUInteger index = [self.posts indexOfObject:post];
if (index != NSNotFound) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:PAWWallPostsTableViewMainSection];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
Then, your tableview method is
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == PAWWallPostsTableViewMainSection) {
PAWPost *post=self.posts[indexPath.row];
[self.mapView setCenterCoordinate:post.coordinate animated:YES];
[self.mapView selectAnnotation:post animated:YES];
}
}

Add rows to the beginning of a tableview without animation

I want to add rows to a tableview with begin/endUpdates to prevent the jump of the tableview when i do reloadData
this is my code
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
[UIView setAnimationsEnabled:NO];
[self.messagesTableView beginUpdates];
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows =
heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
[self.messagesTableView insertRowsAtIndexPaths:rowsInsertIndexPath
withRowAnimation:UITableViewRowAnimationNone];
tableViewOffset.y += heightForNewRows;
[self.messagesTableView endUpdates];
[UIView setAnimationsEnabled:YES];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
}
And sometimes (not everytime) I get this error
invalid number of rows in section 0.
The number of rows contained in an existing section after
the update (2) must be equal to the number of rows contained in that section
before the update (2), plus or minus the number of rows inserted or deleted
from that section (2 inserted, 0 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out).
How do i prevent this error ?
if you dont want animation then use directly
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows =
heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
tableViewOffset.y += heightForNewRows;
[self.messagesTableView reloadData];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
}
If you want to quickly jump to a selected indexPath (with or without animation), you can use this function:
//My function to populate
//tableView is synthesized
-(void)setNewMessage:(NSString*)message{
// ...
NSIndexPath *selectedIndexPath = [tableView indexPathForSelectedRow];
// if your selectedIndexPath==nil, the table scroll stay in the same position
[self reloadData:selectedIndexPath animated:NO];
}
//My reload data wrapper
-(void)reloadData:(NSIndexPath *)selectedIndexPath animated:(BOOL)animated{
[tableView reloadData];
// atScrollPosition can receive different parameters (eg:UITableViewScrollPositionMiddle)
[tableView scrollToRowAtIndexPath:selectedIndexPath
atScrollPosition:UITableViewScrollPositionTop
animated:animated];
}
Once I needed that same flexibility in a tableView and this function has met my needs. You don`t need the "setAnimationsEnabled", just use:
[tableView scrollToRowAtIndexPath:nil
atScrollPosition:UITableViewScrollPositionNone
animated:NO];
];
I hope it helped.
Try this, I am not sure whether this will work or not.
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
[UIView setAnimationsEnabled:NO];
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows = heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
tableViewOffset.y += heightForNewRows;
[UIView setAnimationsEnabled:YES];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.messagesTableView beginUpdates];
[self.messagesTableView insertRowsAtIndexPaths:rowsInsertIndexPath
withRowAnimation:UITableViewRowAnimationNone];
[self.messagesTableView endUpdates];
});
}
Note: You might require to update rowsInsertIndexPath reference to use inside the block (__block type).

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

UITextField becomeFirstResponder not showing cursor for text

I have two UITextFields on each UITableViewRow, and for some reason when I press return on the UIKeyboard one particular rows neither of the two UITextFields will reaspond (the cursor is not visible).
I have a custom UITableViewCell that I am using, I can show you the code for this if you like however I dont think that is the problem as the return key works for 95% of the UITableViewCells. So I was thinking maybe it was how I was handling the delegate methods for the UITextFields?
This is the code I am using for the delegate methods.
-(BOOL)textFieldShouldBeginEditing:(UITextField*)textfield {
int height = self.finishingTableView.frame.size.height;
self.finishingTableView.frame= CGRectMake(self.finishingTableView.frame.origin.x, self.finishingTableView.frame.origin.y, self.finishingTableView.frame.size.width, 307);
// select correct row
if (textfield.tag > 999999) {
int adjustTag = textfield.tag-1000000; // remove a million so that you have the text fields correct position in the table. (this is only for height textfields)
NSIndexPath *indexPath =[NSIndexPath indexPathForRow:adjustTag inSection:0];
[finishingTableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[self tableView:finishingTableView didSelectRowAtIndexPath:indexPath];
[self.finishingTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
return YES;
} else {
NSIndexPath *indexPath =[NSIndexPath indexPathForRow:textfield.tag inSection:0];
[finishingTableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[self tableView:finishingTableView didSelectRowAtIndexPath:indexPath];
[self.finishingTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionNone animated:YES];
return YES;
}
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSLog(#"%i", textField.tag+1);
[[self.view viewWithTag:textField.tag+1] becomeFirstResponder];
// this means there has been a change in the UItextfield
NSLog(#"%#", selectedItemDictionary);
if (textField.tag < 999999) {
tempFinishingObjectDictionary = [selectedItemDictionary mutableCopy];
if (![textField.text isEqualToString:[selectedItemDictionary objectForKey:#"mMM"]]) {
tempUpdatedRow = #"T";
// remove kevalues
[tempFinishingObjectDictionary removeObjectForKey:#"updatedRow"];
[tempFinishingObjectDictionary removeObjectForKey:#"new_mMM"];
// update keyvalues
[tempFinishingObjectDictionary setValue:tempUpdatedRow forKey:#"updatedRow"];
[tempFinishingObjectDictionary setValue:textField.text forKey:#"new_mMM"];
}
} else {
if (![textField.text isEqualToString:[selectedItemDictionary objectForKey:#"hMM"]]) {
tempUpdatedRow = #"T";
// remove kevalues
[tempFinishingObjectDictionary removeObjectForKey:#"updatedRow"];
[tempFinishingObjectDictionary removeObjectForKey:#"new_hMM"];
// update keyvalues
[tempFinishingObjectDictionary setValue:tempUpdatedRow forKey:#"updatedRow"];
[tempFinishingObjectDictionary setValue:textField.text forKey:#"new_hMM"];
}
}
NSLog(#"%#", tempFinishingObjectDictionary);
[coreDataController editSelectedFinishing:htmlProjID UpdatedNSD:tempFinishingObjectDictionary SelectedRow:selectedItemIndexPathRow];
[SVProgressHUD dismiss];
NSLog(#"%#", selectedItemDictionary);
return YES;
}
I don't have any idea on where to look or how to find this error as it seems so random, but happens on the same UITextFields every time.
The code above is where I think the problem could lie; however, having logged everything and debugged for several hours, I am starting to think it's a bug with UITextFields in UITableViewCells.
is [[self.view viewWithTag:textField.tag+1] canBecomeFirstResponder] == YES?
also you need to resign the current UITextField before setting the next as the first responder
[textField resignFirstResponder];

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

Resources