Cancel NSTimers associated with UITableViewCells when they go offscreen - ios

I implement a UITableView of UIImageView cells, each of which periodically refreshes itself every 5 seconds via NSTimer. Each image is loaded from a server in the background thread, and from that background thread I also update the UI, displaying the new image, by calling performSelectorOnMainThread. So far so good.
The problem I noticed is the number of threads is increasing over time and UI becomes non-responsive. Therefore, I want to invalidate NSTimer if a cell goes off screen. Which delegation methods in UITableView should I use to do this efficiently?
The reason why I associate an NSTimer with each cell because I don't want image transition to occur at the same time for all cells.
Is there any other methods to do this by the way? For example, is it possible to use just a single NSTimer?
(I can't use SDWebImage because my requirement is to display a set of images in loop loaded from a server)
// In MyViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
NSTimer* timer=[NSTimer scheduledTimerWithTimeInterval:ANIMATION_SCHEDULED_AT_TIME_INTERVAL
target:self
selector:#selector(updateImageInBackground:)
userInfo:cell.imageView
repeats:YES];
...
}
- (void) updateImageInBackground:(NSTimer*)aTimer
{
[self performSelectorInBackground:#selector(updateImage:)
withObject:[aTimer userInfo]];
}
- (void) updateImage:(AnimatedImageView*)animatedImageView
{
#autoreleasepool {
[animatedImageView refresh];
}
}
// In AnimatedImageView.m
-(void)refresh
{
if(self.currentIndex>=self.urls.count)
self.currentIndex=0;
ASIHTTPRequest *request=[[ASIHTTPRequest alloc] initWithURL:[self.urls objectAtIndex:self.currentIndex]];
[request startSynchronous];
UIImage *image = [UIImage imageWithData:[request responseData]];
// How do I cancel this operation if I know that a user performs a scrolling action, therefore departing from this cell.
[self performSelectorOnMainThread:#selector(performTransition:)
withObject:image
waitUntilDone:YES];
}
-(void)performTransition:(UIImage*)anImage
{
[UIView transitionWithView:self duration:1.0 options:(UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction) animations:^{
self.image=anImage;
currentIndex++;
} completion:^(BOOL finished) {
}];
}

willMoveToSuperview: and/or didMoveToSuperview: do not work on ios 6.0
from ios 6.0 you have the following method of UITableViewDelegate
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Use this method to detect when a cell is removed from a table view, as
opposed to monitoring the view itself to see when it appears or
disappears.

If you properly manage memory and dequeue reusable cells, you can subclass UITableViewCell and override its - prepareForReuse method in order to stop the timer.
Furthermore, as #lnfaziger points out, if you want to stop the timer immediately when the cell is removed from the table view, you can also override its willMoveToSuperview: and/or didMoveToSuperview: method and check if its superview parameter is nil - if it is, the cell is being removed, so you can stop the timer.

Related

Why UITableView reloadData can not in animate block?

Before, I don't know why, when I reloadData my tableVIew, it takes a very long time.
I have test Log to prove, shows the slow is not because the network:
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-began
2016-12-29 14:50:20.958 Eee[1572:25220] lml-info-vc-test-net-end
2016-12-29 14:50:20.972 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-bigin
2016-12-29 14:50:34.870 Eee[1572:25220] lml-info-vc-test-net-animations-reloadData-end
As we see, the net-began and net-end takes very little time.
But the reloadData-bigin and reloadData-end takes a long time, so I searched the SO, what did the reloadData do? I want to know deeply, not simple answer, I searched always is simple answer, not analyse in depth.
My reloadData code:
//[self.pre_tableView reloadData];
[UIView animateWithDuration:0 animations:^{
NSLog(#"lml-info-vc-test-net-animations-reloadData-bigin");
[self.pre_tableView reloadData];
NSLog(#"lml-info-vc-test-net-animations-reloadData-end");
} completion:^(BOOL finished) {
//Do something after that...
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
}];
I use animation block, and in the completionHadler to end the refreshing.
I also searched the apple docs for reloadData
In the Discussion:
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view’s delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates.
Attention
With the attentive sentence:
It should not be called in the methods that insert or delete rows, especially within an animation block
Well! It means I should not in my UIView's animateWithDuration method to reloadData, so I replace my code to below:
[self.pre_tableView reloadData];
[_pre_tableView.mj_footer endRefreshing];
[_pre_tableView.mj_header endRefreshing];
Now it is not slow any more. Very happy, I find the reason.
But, I just want to know why, why can not put reloadData method in animation block ?
And it reloadData did not fail, even takes a long time, in the animate block happens what? then it takes so many time here?
Edit -1
My additional code is below:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.pre_dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:info_TableIdentifier];
if (cell == nil) {
cell = [[LMLAgricultureTechCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:info_TableIdentifier];
}
//NSLog(#"lml-info-vc-test-cellSetModel-began");
((LMLAgricultureTechCell *)cell).model = self.pre_dataSource[indexPath.row];
//NSLog(#"lml-info-vc-test-cellSetModel-end");
((LMLAgricultureTechCell *)cell).indexPath = indexPath;
((LMLAgricultureTechCell *)cell).delegate = self;
((LMLAgricultureTechCell *)cell).photo_view.delegate = self;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// >>>>>>>>>>>>>>>>>>>>> * cell自适应 * >>>>>>>>>>>>>>>>>>>>>>>>
id model;
model = self.pre_dataSource[indexPath.row];
return [self.pre_tableView cellHeightForIndexPath:indexPath model:model keyPath:#"model" cellClass:[LMLAgricultureTechCell class] contentViewWidth:[self cellContentViewWith]];
}
UIView.animateWithDuration... methods can only animate values that are animatable. Although it sometimes feels like magic - it's not really...
reloadData is an async method that can not be handled by the animate block the be animated.
If you want to animate the transition you can either use insert/delete/moveRows or use the transitionWithView method of UIView. This method would render the view off-screen completely as it will look after all the changes you put in it's block, than animate transit between the current state of the view and the newly rendered view. The animation itself depends on the options you deliver, and you probably want to use UIViewAnimationOptionsTransitionCrossDissolve.
[UIView transitionWithView:self.pre_tableView
duration:0.3
options:UIViewAnimationOptionsTransitionCrossDissolve
animations: ^{
[self.pre_tableView reloadData];
} completion: ^(BOOL finished) {
...
}];

Objective-C: Best practice to call method every second

THE SCENARIO I need a method to fire off every second. I also need to be able to stop the firing of the method at any time. At the moment I am using an NSTimer:
THE CODE
self.controlTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(updatePlayer) userInfo:nil repeats:YES];
THE ISSUE I am certain I can achieve this functionality using an NSTimer and call invalidate when I want it to stop, however I am concerned about the performance overhead of placing an NSTimer in a UITableViewCell.
THE QUESTION Does anyone know of a more light-weight alternative to calling a method every second?
I have used NSTimer instances inside of UITableViewCell and UICollectionViewCell custom subclasses to do what you are doing, but I created a protocol PLMMonitor to provide -startMonitoring and -stopMonitoring contracts on my cells to start/stop (see: invalidate) any timing mechanisms.
The Protocol
(Obviously the protocol name prefix can be easily changed)
#protocol PLMMonitor <NSObject>
#required
- (void)startMonitoring;
- (void)stopMonitoring;
#end
Using Cell Visibility to Control the Timers
I could then utilize -[UITableViewDataSource tableView:cellForRowAtIndexPath:] or -[UICollectionViewDelegate collectionView:willDisplayCell:forItemAtIndexPath:] to call -startMonitoring on the cell if it conforms to the protocol (allows for mixed cells in the UITableView/UICollectionView):
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)cell startMonitoring];
}
}
Then I utilized the -[UITableViewDelegate tableView:didEndDisplayingCell:forRowAtIndexPath:] or -[UICollectionViewDelegate collectionView:didEndDisplayingCell:forItemAtIndexPath:] to call -stopMonitoring on the cell if it conformed to the protocol (again allowing for mixed cells in the UITableView/UICollectionView):
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)cell stopMonitoring];
}
}
Using View Controller Visibility to Control the Timers
You should also add code to -viewWillAppear and -viewWillDisappear to -startMonitoring and -stopMonitoring on the visible cells that conform to the protocol to ensure the timers get started/stopped appropriately when they are no longer visible:
- (void)viewWillAppear
{
for (UICollectionViewCell *aCell in [self.collectionView visibleCells])
{
if ([aCell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)aCell startMonitoring];
}
}
}
- (void)viewWillDisappear
{
for (UICollectionViewCell *aCell in [self.collectionView visibleCells])
{
if ([aCell conformsToProtocol:#protocol(PLMMonitor)])
{
[(UICollectionViewCell<PLMMonitor> *)aCell stopMonitoring];
}
}
}
Performance Implications / Energy Usage of NSTimers
One way you can reduce the impact NSTimer instances have on battery life, etc is the make use of their tolerance property which allows iOS to do some power savings magic with them while sacrificing a strict firing interval.
Alternative Timer/Trigger Mechanisms
You can utilize Grand Central Dispatch's (GCD) dispatch_after()
mechanism, but you will lose the ability to cancel the invocation.
Another option is to utilize -[NSObject
performSelector:withObject:afterDelay:] methods and the
accompanying +[NSObject
cancelPreviousPerformRequestsWithTarget:selector:object:]
method to schedule a selector to be invoked and cancel an invocation
respectively.
NSTimer is pretty lightweight. You just need to make sure you properly handle the Cell's timer when the cell is reused.

Exclusive RACCommand execution on tableView:didSelectRowAtIndexPath

I'm trying to express the following scenario in ReactiveCocoa and MVVM.
There's a table view which shows a list of Bluetooth devices nearby
On row selection we start a process of connecting to the selected device and display an activity indicator as an accessoryView of the selected cell.
Now we have alternative endings:
When connected successfully we dismiss the table view controller and pass device handle to the parent view controller or rather parent view model.
When during connecting process user taps another table view cell then we cancel the previous process and start a new one with the selected device.
On error show a message.
I have a problem with ending no 2. I came up with RACCommand in my view model that triggers the process of connection. Then in didSelectRowAtIndexPath I execute that command.
ViewModel:
- (RACCommand *)selectionCommand {
if (!_selectionCommand) {
_selectionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [self selectionSignal];
}];
}
return _selectionCommand;
}
- (RACSignal *)selectionSignal {
// not implemented for real
return [[[RACSignal return:#"ASDF"] delay:2.0] logAll];
}
ViewController:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[activityIndicatorView startAnimating];
cell.accessoryView = activityIndicatorView;
[[self.viewModel.selectionCommand execute:indexPath] subscribeCompleted:^{
[activityIndicatorView stopAnimating];
cell.accessoryView = nil;
}];
}
This shows and hides the activity view during the connection process but only when I wait for it to finish without tapping on other cells.
I ask for a guidance on how such behaviour could be completed. (It also feels like this isn't the right place to subscribe to the signal, right? Should it go to viewDidLoad?)
Apparently I asked the wrong question. It should say "How to cancel a RACCommand". The answer is: takeUntil: can do that (source: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326).
So if I modify my command creation method to look like below everything starts to work like I expected. Now it cancels itself when it is used again. Notice that allowsConcurrentExecution must be set to YES to enable this behaviour, otherwise the signal will emit errors saying that RACCommand is currently not enabled.
- (RACCommand *)selectionCommand {
if (!_selectionCommand) {
#weakify(self);
_selectionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
#strongify(self);
return [[self selectionSignal] takeUntil:self->_selectionCommand.executionSignals];
}];
_selectionCommand.allowsConcurrentExecution = YES;
}
return _selectionCommand;
}
I do this by attaching a block operation to a custom UITableViewCell sub class. I make my tableViewCells part of this subClass and then when I'm laying out my tableviewcells in the view controller, I call to exposed block header in the UITabbleViewCell subclass where it's exposed in this subclasses header file and attach a touch even to the block operation. The custom UITableViewCell needs a tapgesture recognizer and this will do the trick, well it will do the trick as long as in your UITableViewCell custom sub class you also expose the various elements of each blooth tooth tableview cell, that is, create customized setters and getters. This is the easiest way to do it and it takes about 15 lines of code and ZERO third party libraries.
header file:
#property (nonatomic, copy) void (^detailsBlock)();
implementation file:
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(cellTapped:)];
[_tapGesture setDelegate:self];
[_tapGesture setCancelsTouchesInView:FALSE];
[self addGestureRecognizer:_tapGesture];
- (void)cellTapped:(UITapGestureRecognizer*)sender
{
if ([self detailsBlock]) {
[self detailsBlock]();
}
}
making the block work for a tableview in the viewcontroller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"something" forIndexPath:indexPath];
[cell setDetailsBlock:^{
[self termsButtonPressed];
}];
return cell;
}
-(void)termsButtonPressed
{
//do work
}

How to ensure remotely-fetched images in UITableView cells always filled?

I have gotten this question several times in job interviews: You have a UITableView and the user is scrolling very fast. How exactly do you make sure at all of the cells' images which are being loaded from a server are visible wherever the user scrolls to.
I can think of several techniques. For instance what I have done is use an operation queue, loaded with requests to fetch images, and when the user starts scrolling, empty the queue and fill it with requests for images wherever the user is going towards.
Another solution is to fill the images with super-low-resolution thumbnails e.g. a 4-point gradient so that some image, albeit a bad one, is there long before the real image arrives.
I would say that the key here is "wherever the user scrolls to". Because of that, you shouldn't start downloading images until you know which cells are going to be visible.
So what I would do is watch for the UIScrollViewDelegate call:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
At that point, get the cells visible using:
- (NSArray *)indexPathsForVisibleRows
And then spin off my downloads for images. Putting it all together:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *theIndexPaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *theIndexPath in theIndexPaths) {
SomeObject *myObject = self.listOfMyObjects[theIndexPath.row];
[myObject downloadImage];
}
}
This is very basic. Obviously need to update the cell, etc.
I would say "empty the queue and fill it with requests for images wherever the user is going towards" is correct using indexPathsForVisibleRows. But the images will never appear immediately unless they are very small.
I think what they mean is to show the downloaded image immediately after it has been downloaded from the background thread, which would be: dispatch_get_main_queue()
In .h file:
#interface ViewController : UIViewController <UIScrollViewDelegate>
In .m file:
- (void)downloadVisibleRowImages
{
NSArray *visibleRows = [self.tableView indexPathsForVisibleRows];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
for (NSIndexPath *visibleRow in visibleRows) {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath: visibleRow];
NSString *imageUrl = [_responseImages objectAtIndex:visibleRow.row];
UIImage *image = nil; // Download image here
dispatch_async(dispatch_get_main_queue(), ^{ // use main thread to display image immediately after download
if (image) [cell.imageView setImage:image];
});
}
});
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self downloadVisibleRowImages];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self downloadVisibleRowImages];
}

Get notified when UITableView has finished asking for data?

Is there some way to find out when a UITableView has finished asking for data from its data source?
None of the viewDidLoad/viewWillAppear/viewDidAppear methods of the associated view controller (UITableViewController) are of use here, as they all fire too early. None of them (entirely understandably) guarantee that queries to the data source have finished for the time being (eg, until the view is scrolled).
One workaround I have found is to call reloadData in viewDidAppear, since, when reloadData returns, the table view is guaranteed to have finished querying the data source as much as it needs to for the time being.
However, this seems rather nasty, as I assume it is causing the data source to be asked for the same information twice (once automatically, and once because of the reloadData call) when it is first loaded.
The reason I want to do this at all is that I want to preserve the scroll position of the UITableView - but right down to the pixel level, not just to the nearest row.
When restoring the scroll position (using scrollRectToVisible:animated:), I need the table view to already have sufficient data in it, or else the scrollRectToVisible:animated: method call does nothing (which is what happens if you place the call on its own in any of viewDidLoad, viewWillAppear or viewDidAppear).
This answer doesn't seem to be working anymore, due to some changes made to UITableView implementation since the answer was written. See this comment : Get notified when UITableView has finished asking for data?
I've been playing with this problem for a couple of days and think that subclassing UITableView's reloadData is the best approach :
- (void)reloadData {
NSLog(#"BEGIN reloadData");
[super reloadData];
NSLog(#"END reloadData");
}
reloadData doesn't end before the table has finish reload its data. So, when the second NSLog is fired, the table view has actually finish asking for data.
I've subclassed UITableView to send methods to the delegate before and after reloadData. It works like a charm.
I did have a same scenario in my app and thought would post my answer to you guys as other answers mentioned here does not work for me for iOS7 and later
Finally this is the only thing that worked out for me.
[yourTableview reloadData];
dispatch_async(dispatch_get_main_queue(),^{
NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
//Basically maintain your logic to get the indexpath
[yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
Swift Update:
yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
//Basically maintain your logic to get the indexpath
yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
})
So how this works.
Basically when you do a reload the main thread becomes busy so at that time when we do a dispatch async thread, the block will wait till the main thread gets finished. So once the tableview has been loaded completely the main thread will gets finish and so it will dispatch our method block
Tested in iOS7 and iOS8 and it works awesome;)
Update for iOS9: This just works fine is iOS9 also. I have created a sample project in github as a POC.
https://github.com/ipraba/TableReloadingNotifier
I am attaching the screenshot of my test here.
Tested Environment: iOS9 iPhone6 simulator from Xcode7
EDIT: This answer is actually not a solution. It probably appears to work at first because reloading can happen pretty fast, but in fact the completion block doesn't necessarily get called after the data has fully finished reloading - because reloadData doesn't block. You should probably search for a better solution.
To expand on #Eric MORAND's answer, lets put a completion block in. Who doesn't love a block?
#interface DUTableView : UITableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
and...
#import "DUTableView.h"
#implementation DUTableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[super reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
Usage:
[self.tableView reloadDataWithCompletion:^{
//do your stuff here
}];
reloadData just asking for data for the visible cells. Says, to be notified when specify portion of your table is loaded, please hook the tableView: willDisplayCell: method.
- (void) reloadDisplayData
{
isLoading = YES;
NSLog(#"Reload display with last index %d", lastIndex);
[_tableView reloadData];
if(lastIndex <= 0){
isLoading = YES;
//Notify completed
}
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row >= lastIndex){
isLoading = NO;
//Notify completed
}
That is my solution. 100% works and used in many projects. It's a simple UITableView subclass.
#protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
#optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
#end
#interface MyTableView : UITableView {
struct {
unsigned int delegateWillReloadData:1;
unsigned int delegateDidReloadData:1;
unsigned int reloading:1;
} _flags;
}
#end
#implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
return (id<MyTableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
[super setDelegate:delegate];
_flags.delegateWillReloadData = [delegate respondsToSelector:#selector(tableViewWillReloadData:)];
_flags.delegateDidReloadData = [delegate respondsToSelector:#selector(tableViewDidReloadData:)];
}
- (void)reloadData {
[super reloadData];
if (_flags.reloading == NO) {
_flags.reloading = YES;
if (_flags.delegateWillReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
}
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
}
}
- (void)finishReload {
_flags.reloading = NO;
if (_flags.delegateDidReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
}
}
#end
It's similar to Josh Brown's solution with one exception. No delay is needed in performSelector method. No matter how long reloadData takes. tableViewDidLoadData: always fires when tableView finishes asking dataSource cellForRowAtIndexPath.
Even if you do not want to subclass UITableView you can simply call [performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f] and your selector will be called right after the table finishes reloading. But you should ensure that selector is called only once per call to reloadData:
[self.tableView reloadData];
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
Enjoy. :)
This is an answer to a slightly different question: I needed to know when UITableView had also finished calling cellForRowAtIndexPath(). I subclassed layoutSubviews() (thanks #Eric MORAND) and added a delegate callback:
SDTableView.h:
#protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
#required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
#end
#interface SDTableView : UITableView
#property(nonatomic,assign) id <SDTableViewDelegate> delegate;
#end;
SDTableView.m:
#import "SDTableView.h"
#implementation SDTableView
#dynamic delegate;
- (void) reloadData {
[self.delegate willReloadData];
[super reloadData];
[self.delegate didReloadData];
}
- (void) layoutSubviews {
[self.delegate willLayoutSubviews];
[super layoutSubviews];
[self.delegate didLayoutSubviews];
}
#end
Usage:
MyTableViewController.h:
#import "SDTableView.h"
#interface MyTableViewController : UITableViewController <SDTableViewDelegate>
#property (nonatomic) BOOL reloadInProgress;
MyTableViewController.m:
#import "MyTableViewController.h"
#implementation MyTableViewController
#synthesize reloadInProgress;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ( ! reloadInProgress) {
NSLog(#"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
reloadInProgress = TRUE;
}
return 1;
}
- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}
- (void)didLayoutSubviews {
if (reloadInProgress) {
NSLog(#"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
reloadInProgress = FALSE;
}
}
NOTES:
Since this is a subclass of UITableView which already has a delegate property pointing to MyTableViewController there's no need to add another one. The "#dynamic delegate" tells the compiler to use this property. (Here's a link describing this: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/)
The UITableView property in MyTableViewController must be changed to use the new SDTableView class. This is done in the Interface Builder Identity Inspector. Select the UITableView inside of the UITableViewController and set its "Custom Class" to SDTableView.
I had found something similar to get notification for change in contentSize of TableView. I think that should work here as well since contentSize also changes with loading data.
Try this:
In viewDidLoad write,
[self.tableView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];
and add this method to your viewController:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"contentSize"]) {
DLog(#"change = %#", change.description)
NSValue *new = [change valueForKey:#"new"];
NSValue *old = [change valueForKey:#"old"];
if (new && old) {
if (![old isEqualToValue:new]) {
// do your stuff
}
}
}
}
You might need slight modifications in the check for change. This had worked for me though.
Cheers! :)
Here's a possible solution, though it's a hack:
[self.tableView reloadData];
[self performSelector:#selector(scrollTableView) withObject:nil afterDelay:0.3];
Where your -scrollTableView method scrolls the table view with -scrollRectToVisible:animated:. And, of course, you could configure the delay in the code above from 0.3 to whatever seems to work for you. Yeah, it's ridiculously hacky, but it works for me on my iPhone 5 and 4S...
I had something similar I believe. I added a BOOL as instance variable which tells me if the offset has been restored and check that in -viewWillAppear:. When it has not been restored, I restore it in that method and set the BOOL to indicate that I did recover the offset.
It's kind of a hack and it probably can be done better, but this works for me at the moment.
It sounds like you want to update cell content, but without the sudden jumps that can accompany cell insertions and deletions.
There are several articles on doing that. This is one.
I suggest using setContentOffset:animated: instead of scrollRectToVisible:animated: for pixel-perfect settings of a scroll view.
You can try the following logic:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
NSLog(#"Created Last Cell. IndexPath = %#", indexPath);
//[self.activityIndicator hide];
//Do the task for TableView Loading Finished
}
prevIndexPath = indexPath;
return cell;
}
-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{
BOOL bRetVal = NO;
NSArray *visibleIndices = [tableView indexPathsForVisibleRows];
if (!visibleIndices || ![visibleIndices count])
bRetVal = YES;
NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];
if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
//Ascending - scrolling up
if ([indexPath isEqual:lastVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
} else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
//Descending - scrolling down
if ([indexPath isEqual:firstVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
}
return bRetVal;
}
And before you call reloadData, set prevIndexPath to nil. Like:
prevIndexPath = nil;
[mainTableView reloadData];
I tested with NSLogs, and this logic seems ok. You may customise/improve as needed.
finally i have made my code work with this -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
there were few things which needed to be taken care of -
call it within "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
just ensure that "scrollToRowAtIndexPath" message is sent to relevant instance of UITableView, which is definitely MyTableview in this case.
In my case UIView is the view which contains instance of UITableView
Also, this will be called for every cell load. Therefore, put up a logic inside "cellForRowAtIndexPath" to avoid calling "scrollToRowAtIndexPath" more than once.
You can resize your tableview or set it content size in this method when all data loaded:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.frame =CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
I just run repeating scheduled timer and invalidate it only when table's contentSize is bigger when tableHeaderView height (means there is rows content in the table). The code in C# (monotouch), but I hope the idea is clear:
public override void ReloadTableData()
{
base.ReloadTableData();
// don't do anything if there is no data
if (ItemsSource != null && ItemsSource.Length > 0)
{
_timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue,
new NSAction(() =>
{
// make sure that table has header view and content size is big enought
if (TableView.TableHeaderView != null &&
TableView.ContentSize.Height >
TableView.TableHeaderView.Frame.Height)
{
TableView.SetContentOffset(
new PointF(0, TableView.TableHeaderView.Frame.Height), false);
_timer.Invalidate();
_timer = null;
}
}));
}
}
Isn't UITableView layoutSubviews called just before the table view displays it content? I've noticed that it is called once the table view has finished load its data, maybe you should investigate in that direction.
Since iOS 6 onwards, the UITableview delegate method called:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
will execute once your table reloads successfully. You can do customisation as required in this method.
The best solution I've found in Swift
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Why no just extend?
#interface UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
#implementation UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[self reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
scroll to the end:
[self.table reloadDataWithCompletion:^{
NSInteger numberOfRows = [self.table numberOfRowsInSection:0];
if (numberOfRows > 0)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:numberOfRows-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}];
Not tested with a lot of data

Resources