UITableViewCell not updating after location callback until scroll or selection - ios

I am trying to update a UITableViewCell as soon as the user's location is found and reverse-geocoded.
From reading lots of other answers to similar questions, it seems the tableview reload must occur on the main thread, which I have tried without any success.
All the location data gets retrieved correctly, and is correctly added to the core data object, but the tableview cell simply is not updating until the user scrolls or the cell is selected, at which point the cell is correctly updated from that point on.
Here is a selection from my code - does anyone know why the tableview cell isn't updating right away?
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations // Delegate callback method.
{
// Correctly gets currentLocation coordinates.
...
CLLocation *currentLocation = [locations lastObject];
...
// Reverse-geocode the coordinates (find physical address):
[geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error == nil && [placemarks count] > 0) {
CLPlacemark *placemark = [placemarks lastObject]; // Correctly gets placemark.
// Correctly adds placemark to core data object.
...
...
// Reload TableView:
// [self.tableView reloadData]; //Tried this, didn't work, since not on main thread.
// [self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO]; //Doesn't work.
// [self performSelector:(#selector(refreshDisplay)) withObject:nil afterDelay:0.5]; //Doesn't work.
[self performSelectorOnMainThread:#selector(refreshDisplay) withObject:nil waitUntilDone:NO]; //Doesn't work.
}
}];
}
- (void)refreshDisplay {
[_tableView reloadData];
}
Again, the underlying logic is working, since the data gets added correctly and eventually shows up on the cell, but not until user scroll or selection. I can't imagine why this doesn't refresh the tableviewcell right away. Does anyone have any idea?
UPDATE: My Solution
The missing piece was adding [cell layoutSubviews] just after the cell is created/dequeued. The detailTextLabel now updates correctly. Apparently this may be related to an iOS8 bug that doesn't update the detailText if it starts out nil (i.e. no content), and layoutSubviews makes it so it is not nil, by initializing all the subviews of the cell (as far as I understand). I got this suggestion from here:
ios 8 UITableViewCell detail text not correctly updating
The accepted answer also helped me figure out this missing piece.

You should get a reference to the cell that you want to update
Ex. UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:theRow inSection:theSection]];
Then [[cell textLabel] setText:theLocation];
If your updating multiple cells just get multiple references of the cells you want to update and update them accordingly.
Typically anything dealing with UI components that need updating should be done on the main thread. I have come across this issue before but if you want to have a solution dealing with threading, what you have done seems like it should work.
Here is the GCD solution:
dispatch_async(dispatch_get_main_queue(), ^{
[self.table reloadData];
});
But according to what you've done, it shouldn't help... It is basically the same thing just with GCD. Hopefully the solution I gave you at the beginning works...
EDIT
Within your - (UITableViewCell *)tableView:(UITableView *)tableView cellForNextPageAtIndexPath:(NSIndexPath *)indexPath
cell = [[PFTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"cell"];
Did you set the style to UITableViewCellStyleDefault?

It seems that tableview cell is not completely visible in the view.In that case You can use [cell setNeedsDisplay]; for example: dispatch_async(dispatch_get_main_queue(), ^{ [cell setNeedsDisplay];
[cell.contentView addSubview:yourView];
});
Hope this works for you.

I had the same problem. None of the answers here worked for me. I finally searched thru every aspect of the nib and saw that my controls (buttons and the table view) were not connected to outlets even though in the .h file the circle indicator was selected. But in the connections inspector they were not connected to outlets. So had to delete all the .h file controls and reconnect and low and behold everything works.

Related

UITableView crash if I delete rows too fast

I have an app with a UITableView which can delete cells using a row action. However, if I do two in quick succession, the app crashes with a BAD_EXEC.
The problem is clearly timing related. I'm asking the table to do something else before it's quite finished with the old. It could be the animations or it could be the removal of cells.
Either way, calling reloadData on the tableview before I start seems to fix it. There are two problems with this solution.
Firstly, reloadData interferes with some of the niceness of the usual row removal animations. It's no biggie but I'd prefer it with all animations intact.
Secondly, I still don't fully understand what's happening.
Can any one help me understand and/or suggest a better solution?
Here's the code...
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
[self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
removeTaskFromTableView is mostly code to work out if I need to delete an entire section or a row. I've confirmed the app makes the right choice and the bug works either way so the only relevant line from the method is...
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
Edit: I have noticed that the standard delete row action provided by the system does not allow me to move that fast. There is an in-built delay which (presumably) prevents this exact problem.
-(void) rowActionPressedInIndexPath: (NSIndexPath *)indexPath timing:(Timing) doTaskWhen
{
// [self.tableView reloadData]; // This is my current solution
[self.tableView beginUpdates];
ToDoTask *toDo = [self removeTaskFromTableViewAtIndexPath:indexPath];
toDo.timing = doTaskWhen; // Just some data model updating. Has no effect on anything else here.
[self.tableView endUpdates];
}
Or try to reload table view in main thread.

UITableView prevent reloading while scrolling

I have implemented an UITableView with load more functionality. The tableView loads big images from a sometimes slow server. I'm starting an URLConnection for each image and reload the indexPath corresponding to the URLConnection (saved with the connection object). The connections themselves call -reloadData on the tableView.
Now when clicking the load more button, I scroll to the first row of the new data set with position bottom. This works great and also my asynchronous loading system.
I faced the following issue: When the connection is "too fast", the tableView is reloading the data at a given indexPath while the tableView is still scrolling to the first cell of the new data set, the tableView scrolls back half the height of that cell.
This is what it should look like and what it actually does:
^^^^^^^^^^^^ should ^^^^^^^^^^^^ ^^^^^^^^^^^^^ does ^^^^^^^^^^^^^
And here is some code:
[[self tableView] beginUpdates];
for (NSMutableDictionary *post in object) {
[_dataSource addObject:post];
[[self tableView] insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:[_dataSource indexOfObject:post] inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
[[self tableView] endUpdates];
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[_dataSource indexOfObject:[object firstObject]] inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
-tableView:cellForRowAtIndexPath: starts a JWURLConnection if the object in the data source array is a string, and replaces it with an instance of UIImage in the completion block. Then it reloads the given cell:
id image = [post objectForKey:#"thumbnail_image"];
if ([image isKindOfClass:[NSString class]]) {
JWURLConnection *connection = [JWURLConnection connectionWithGETRequestToURL:[NSURL URLWithString:image] delegate:nil startImmediately:NO];
[connection setFinished:^(NSData *data, NSStringEncoding encoding) {
[post setObject:[UIImage imageWithData:data] forKey:#"thumbnail_image"];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
[cell startLoading];
[connection start];
}
else if ([image isKindOfClass:[UIImage class]]) {
[cell stopLoading];
[cell setImage:image];
}
else {
[cell setImage:nil];
}
Can I prevent the tableView from performing the -reloadRowsAtIndexPaths:withRowAnimation: calls until the tableView scrolling is done? Or can you imagine a good way to prevent this behavior?
Based on the ideas of Malte and savner (please upvote his answer as well) I could implement a solution. His answer didn't do the trick, but it was the right direction.
I had to implement -scrollViewDidEndScrollingAnimation:. I created a bool property called _autoScrolling and an NSMutableArray property for the index paths that got reloaded while scrolling. In the URLConnections finish block I did this:
if (_autoScrolling) {
if (!_indexPathsToReload) {
_indexPathsToReload = [NSMutableArray array];
}
[_indexPathsToReload addObject:indexPath];
}
else {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
And then this:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self performSelector:#selector(performRelodingAfterAutoScroll) withObject:nil afterDelay:0.0];
}
- (void)performRelodingAfterAutoScroll {
_autoScrolling = NO;
if (_indexPathsToReload) {
[[self tableView] reloadRowsAtIndexPaths:_indexPathsToReload withRowAnimation:UITableViewRowAnimationFade];
}
_indexPathsToReload = nil;
}
It took me quite a long time to find the trick with -performSelector:withObject:afterDelay: and I still don't know why I need it.
I thought the method might got called too early. So I implemented a delay of a second and tried how far I can take it down. It still works with 0.0 but not if I call the method directly or use -performSelector:withObject:.
I really hope someone can explain that.
EDIT
After revisiting this a few years later I can explain what's going on here:
Calling -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:] guarantees the call to be performed in the next runloop iteration.
So an even better or IMHO more beautiful solution would be:
[[NSOperationQueue currentQueue] addOperationWithBlock:^{
[self performRelodingAfterAutoScroll];
}];
I wrote a more detailed explanation in this answer.
Sorry i don't have enough reputation to add a comment, hence the answer to your last question in a separate answer.
-performSelector:withObject:afterDelay: with a delay of 0.0 seconds does not execute the given selector immediately but instead performs it after the current Runloop Cycle finishes and after the given delay.
Where as -performSelector:withObject: is added to and executed in the current Runloop Cycle. Which is the same as directly calling the method.
Therefore using -performSelector:withObject:afterDelay: the UI will get updated in the current Runloop Cycle i.e in this case the scrolling animation can finish, before your selector is performed(and reloads the UI once more).
Source: Apple Dev Docs and this Thread Answer
You can use the UIScrollViewDelegate protocols (which you get for free using UITableViewDelegate) and utilize the -scrollViewDidScroll or -scrollViewWillBeginDragging: methods to detect scrolling has started or stopped. Work with those callbacks to control when you want to load/stop loading cell data.

Flash scroll indicators of UICollectionView after changing data source

I'm swapping out the data being displayed in my collection view by changing the datasource. This is being done as part of a tab-like interface. When the new data loads, I would like to flash the scroll indicators to tell the user that there's more data outside of the viewport.
Immediately
Doing so immediately doesn't work because the collection view hasn't loaded the data yet:
collectionView.dataSource = dataSource2;
[collectionView flashScrollIndicators]; // dataSource2 isn't loaded yet
dispatch_async
Dispatching the flashScrollIndicators call later doesn't work either:
collectionView.dataSource = dataSource2;
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView flashScrollIndicators]; // dataSource2 still isn't loaded
});
performSelector:withObject:afterDelay:
Executing the flashScrollIndicators after a timed delay does work (I saw it somewhere else on SO), but leads to a bit of lag with the scroll indicators being shown. I could decrease the delay, but it seems like it'll just leads to a race condition:
collectionView.dataSource = dataSource2;
[collectionView performSelector:#selector(flashScrollIndicators) withObject:nil afterDelay:0.5];
Is there a callback that I can hook on to to flash the scroll indicators as soon as the collection view has picked up on the new data and resized the content view?
Subclassing UICollectionView and overriding layoutSubviews can be a solution. You can call [self flashScrollIndicators] on the collection. Problem is that layoutSubviews gets called in multiple scenarios.
Initially when collection is created and datasource is assigned.
On scrolling, cells which go beyond the viewport get re-used & re-layout.
Explicitly change frame/reload the collection.
Workaround to this can be, keeping a BOOL property which will be made YES only when reloading datasource, otherwise will remain NO. Thus flashing of scroll bars will happen explicitly only when reloading collection.
In terms of source code,
MyCollection.h
#import <UIKit/UIKit.h>
#interface MyCollection : UICollectionView
#property (nonatomic,assign) BOOL reloadFlag;
#end
MyCollection.m
#import "MyCollection.h"
#implementation MyCollection
- (void) layoutSubviews {
[super layoutSubviews];
if(_reloadFlag) {
[self flashScrollIndicators];
_reloadFlag=NO;
}
}
Usage should be
self.collection.reloadFlag = YES;
self.collection.dataSource = self;
Put your call to flashScrollIndicators inside UICollectionViewLayout's method -finalizeCollectionViewUpdates.
From Apple's documentation:
"... This method is called within the animation block used to perform all of the insertion, deletion, and move animations so you can create additional animations using this method as needed. Otherwise, you can use it to perform any last minute tasks associated with managing your layout object’s state information."
Hope this helps!
Edit:
Ok, I got it. Since you mentioned the finalizeCollectionViewUpdates method was not being called I decided to try it myself. And you're right. The problem is (sorry I didn't notice this earlier) that method is only called after you update the Collection View (insert, delete, move a cell, for example). So in this case it doesn't work for you. So, I have a new solution; it involves using UICollectionView's method indexPathsForVisibleItems inside UICollectionViewDataSource's method collectionView:cellForItemAtIndexPath:
Every time you hand a new UICollectionViewCell to your collection view, check if it is the last of the visible cells by using [[self.collectionView indexPathsForVisibleItems] lastObject]. You will also need a BOOL ivar to decide if you should flash the indicators. Every time you change your dataSource set the flag to YES.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
NSIndexPath *iP = [[self.collectionView indexPathsForVisibleItems] lastObject];
if (iP.section == indexPath.section && iP.row == indexPath.row && self.flashScrollIndicators) {
self.flashScrollIndicators = NO;
[self.collectionView flashScrollIndicators];
}
return cell;
}
I tried this approach and it's working for me.
Hope it helps!

iOS: Updating tableViewCell infinite loop

This is the problem: I update my tableview after saving and fetching an thumbnail to Core Data and then I tell the cell to update itself - so it can show a thumbnail when the image has been loaded into Core Data. I Use two different threads as Core Data is not threadsafe and all GUI elements of course needs to happen in the main thread.
But this whole method just keep looping forever, and what is causing it is when I reload the thread:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Why? and how do I fix this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Photo"];
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = photo.title;
cell.detailTextLabel.text = photo.subtitle;
NSLog(#"Context %#", self.photographer.managedObjectContext);
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
return cell;
}
The infinite loop is caused by calling the cellForRowAtIndexPath: again after the fetch is loaded and the reload on a certain cell is called.
A reload will force the cellForRowAtIndexPath: to be called again... and in your case again and again to infinity.
The solution is simple... Do not reload your cell in the cellForRowAtIndexPath but rather in a callback-method of the fetchrequest. Then reload it there rather then the creation of the cell.
Rather do not load the image inside the cellForRowAtIndexpath: at all.
Whenever your table is instantiated make a method that loops over your datasource and get the respective cell for each item. Then load an image for each cell you deem needed. And reload the cell whenever the fetching of the item is done (for instance a callback method).
If you do want the image to be loaded inside the creation of the cell as you have done now (Although I don't think that is the proper way to do it). You can surround the whole performBlock: with an if-statement checking if the image has already been set or not.
As already been said by others, there should be no need to call reloadRowsAtIndexPaths when the image has been loaded. Assigning a new image to cell.imageView.image is sufficient.
But there is another problem with your code. I assume that self.photographer.managedObjectContext is a managed object context of the "private concurrency type", so that performBlock executes on a background thread. (Otherwise there would be no need to use dispatch_async(dispatch_get_main_queue(), ...).)
So performBlock: executes the code asynchronously, on a background thread. (Which is good because the UI is not blocked.) But when the image has been fetched after some time, the cell might have been reused for a different row. (This happens if you scroll the table view so that the row becomes invisible while the image is fetched.)
Therefore, you have to check if the cell is still at the same position in the tableview:
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
if ([[tableView indexPathForCell:cell] isEqualTo:indexPath]) {
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
}
});
}];
I'd replace the following code:
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
With:
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[blockCell setNeedsLayout];
This solves the issue of reloading recursively, as well as the possibility that cell has gotten reused in the interim. I'd also check for whether or not you've loaded the photo data already and not update if so.
Since you are using an NSFetchedResultsController though, you may be better off going with Totomus Maximus' answer, if the delegate callbacks are already informing you when the photo data updates. This way just updates the image view and wouldn't update any other information that the Photo update method could change.

UITableView: using moveRowAtIndexPath:toIndexPath: and reloadRowsAtIndexPaths:withRowAnimation: together appears broken

I want to use iOS 5's nifty row-movement calls to animate a tableview to match some model state changes, instead of the older-style delete-and-insert.
Changes may include both reordering and in-place updates, and I want to animate both, so some rows will need reloadRowsAtIndexPaths.
But! UITableView appears to be just plain wrong in its handling of row reloads in the presence of moves, if the updated cell shifts position because of the moves. Using the older delete+insert calls, in a way that should be equivalent, works fine.
Here's some code; I apologize for the verbosity but it does compile and run. The meat is in the doMoves: method. Exposition below.
#define THISWORKS
#implementation ScrambledList // extends UITableViewController
{
NSMutableArray *model;
}
- (void)viewDidLoad
{
[super viewDidLoad];
model = [NSMutableArray arrayWithObjects:
#"zero",
#"one",
#"two",
#"three",
#"four",
nil];
[self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:
#ifdef THISWORKS
#"\U0001F603"
#else
#"\U0001F4A9"
#endif
style:UIBarButtonItemStylePlain
target:self
action:#selector(doMoves:)]];
}
-(IBAction)doMoves:(id)sender
{
int fromrow = 4, torow = 0, changedrow = 2; // 2 = its "before" position, just like the docs say.
// some model changes happen...
[model replaceObjectAtIndex:changedrow
withObject:[[model objectAtIndex:changedrow] stringByAppendingString:#"\u2032"]];
id tmp = [model objectAtIndex:fromrow];
[model removeObjectAtIndex:fromrow];
[model insertObject:tmp atIndex:torow];
// then we tell the table view what they were
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:changedrow inSection:0]]
withRowAnimation:UITableViewRowAnimationRight]; // again, index for the "before" state; the tableview should figure out it really wants row 3 when the time comes
#ifdef THISWORKS
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:fromrow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:torow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
#else // but this doesn't
[self.tableView moveRowAtIndexPath:[NSIndexPath indexPathForRow:fromrow inSection:0]
toIndexPath:[NSIndexPath indexPathForRow:torow inSection:0]];
#endif
[self.tableView endUpdates];
}
#pragma mark - Table view data source boilerplate, not very interesting
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return model.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#""];
if (cell == nil)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#""];
[cell.textLabel setText:[[model objectAtIndex:indexPath.row] description]];
[cell.detailTextLabel setText:[NSString stringWithFormat:#"this cell was provided for row %d", indexPath.row]];
return cell;
}
What the code does: sets up a tiny model (small mutable array); when a button is pushed, it makes a small change to the middle element of the list, and moves the last element to be the first. Then it updates the table view to reflect these changes: reloads the middle row, removes the last row and inserts a new row zero.
This works. In fact, adding logging to cellForRowAtIndexPath shows that although I ask for row 2 to be reloaded, the tableview correctly asks for row 3 because of the insert once it's time to actually do the update. Huzzah!
Now comment out the top #ifdef to use the moveRowAtIndexPath call instead.
Now the tableview removes row 2, asks for a fresh row 2 (wrong!), and inserts it in the final row-2 position (also wrong!). Net result is that row 1 moved down two slots instead of one, and scrolling it offscreen to force a reload shows how it's gone out of sync with the model. I could understand if moveRowAtIndexPath changed the tableview's private model in a different order, requiring the use of the "new" instead of "old" index paths in reloads or model fetches, but that's not what's going on. Note that in the second "after" pic, the third and fourth rows are in the opposite order, which should't happen no matter which cell I'm reloading.
My vocabulary has grown colorful cursing Apple. Should I be cursing myself instead? Are row moves just plain incompatible with row reloads in the same updates block (as well as, I suspect, inserts and deletes)? Can anyone enlighten me before I go file the bug report?
I just spent some time playing with your code, and I agree; looks like it just doesn't work.
This whole area is a bit under-documented, but they don't actually say that you can mix moveRowAtIndexPath:toIndexPath: with reload methods. It does say in the that it can be mixed with row-insertion and row-deletion methods. Those seems to work if I modify your code to exercise those instead. So, you might be asking for an enhancement, not filing a bug. Either way, I'd definitely send it to radar.

Resources