当 UITableView 已经完成数据请求时获得通知?

有没有什么方法可以知道 UITableView什么时候已经完成了从它的数据源请求数据的过程?

相关视图控制器(UITableViewController)的 viewDidLoad/viewWillAppear/viewDidAppear方法在这里都没有用,因为它们都过早地触发了。它们中没有一个(完全可以理解)保证对数据源的查询暂时已经完成(例如,直到视图被滚动)。

我发现的一个变通方法是在 viewDidAppear中调用 reloadData,因为当 reloadData返回时,表视图 保证已经完成了目前所需的数据源查询。

然而,这似乎相当令人讨厌,因为我假设它导致数据源在首次加载时被请求两次相同的信息(一次是自动的,一次是因为 reloadData调用)。

我想这样做的原因是,我想保留的 UITableView的滚动位置-但权利下降到像素水平,而不只是最近的行。

在恢复滚动位置时(使用 scrollRectToVisible:animated:) ,我需要表视图中已经有足够的数据,否则 scrollRectToVisible:animated:方法调用什么也不做(如果在 viewDidLoadviewWillAppearviewDidAppear中单独调用,就会发生这种情况)。

62955 次浏览

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.

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.

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.

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 -

  1. call it within "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
  2. just ensure that "scrollToRowAtIndexPath" message is sent to relevant instance of UITableView, which is definitely MyTableview in this case.
  3. In my case UIView is the view which contains instance of UITableView
  4. Also, this will be called for every cell load. Therefore, put up a logic inside "cellForRowAtIndexPath" to avoid calling "scrollToRowAtIndexPath" more than once.

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
}

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

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.

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 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! :)

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

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 ABC0 takes. ABC1 always fires when ABC2 finishes asking ABC3 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. :)

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.

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

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

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

enter image description here

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()
}
}
}