当UITableView完成数据请求时收到通知?

时间:2009-09-27 13:29:22

标签: ios iphone uitableview

有没有办法找出UITableView何时完成从数据源请求数据?

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

我找到的一种解决方法是在reloadData中调用viewDidAppear,因为当reloadData返回时,表格视图 保证已完成查询暂时需要的数据源。

然而,这看起来相当令人讨厌,因为我认为它首次加载时会导致数据源被要求提供相同的信息两次(一次是自动的,一次是因为reloadData调用)。 / p>

我想要这样做的原因是我想要保留UITableView的滚动位置 - 但是直到像素级别,而不仅仅是最近的行。

当恢复滚动位置时(使用scrollRectToVisible:animated:),我需要表视图中已有足够的数据,否则scrollRectToVisible:animated:方法调用什么都不做(如果放置则会发生这种情况在viewDidLoadviewWillAppearviewDidAppear中的任意一项中自行调用。

18 个答案:

答案 0 :(得分:60)

这个答案似乎不再起作用,因为自编写答案以来对UITableView实现进行了一些更改。请参阅此评论:Get notified when UITableView has finished asking for data?

我一直在玩这个问题几天,并认为继承UITableView的{​​{1}}是最好的方法:

reloadData
在表重新加载其数据之前,

- (void)reloadData { NSLog(@"BEGIN reloadData"); [super reloadData]; NSLog(@"END reloadData"); } 没有结束。因此,当第二个reloadData被触发时,表视图实际上已经完成了对数据的询问。

我已将NSLog子类化为UITableView之前和之后向委托发送方法。它就像一个魅力。

答案 1 :(得分:50)

我在我的应用程序中确实有相同的场景,并且我会将答案发给你们,因为这里提到的其他答案对我来说对iOS7及更高版本不起作用

最后这是我唯一能解决的问题。

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

快速更新:

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)

})

这是如何运作的。

基本上当你重新加载时,主线程变得很忙,所以当我们执行调度异步线程时,该块将等到主线程完成。因此,一旦完成了tableview的加载,主线程将完成,因此它将调度我们的方法块

在iOS7和iOS8中测试,效果很棒;)

iOS9更新:这也很好用iOS9也行。我在github中创建了一个示例项目作为POC。 https://github.com/ipraba/TableReloadingNotifier

我在这里附上我的测试截图。

经测试的环境:来自Xcode7的iOS9 iPhone6模拟器

enter image description here

答案 2 :(得分:25)

编辑:这个答案实际上不是解决方案。它似乎首先起作用,因为重新加载可以非常快地发生,但实际上完成块不一定在数据完全重新加载后被调用 - 因为reloadData不会阻塞。您应该寻找更好的解决方案。

为了扩展@Eric MORAND的答案,让我们放一个完成块。谁不爱一块?

@interface DUTableView : UITableView

   - (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;

@end

和...

#import "DUTableView.h"

@implementation DUTableView

- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
    [super reloadData];
    if(completionBlock) {
        completionBlock();
    }
}

@end

用法:

[self.tableView reloadDataWithCompletion:^{
                                            //do your stuff here
                                        }];

答案 3 :(得分:11)

reloadData只询问可见单元格的数据。说,要在指定表的部分加载时收到通知,请挂钩tableView: willDisplayCell:方法。

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

答案 4 :(得分:10)

这是我的解决方案。 100%工作并在许多项目中使用。它是一个简单的UITableView子类。

@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

它与Josh Brown's solution类似,但有一个例外。 performSelector方法不需要延迟。 无论reloadData多长时间。 tableViewDidLoadData:完成询问tableView dataSource时始终会触发cellForRowAtIndexPath

即使您不想继承UITableView,也可以简单地调用[performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f],并在表重新加载后立即调用您的选择器。但是你应该确保每次调用reloadData时只调用一次选择器:

[self.tableView reloadData];
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];

享受。 :)

答案 5 :(得分:8)

这是一个稍微不同的问题的答案:我需要知道UITableView何时也完成了调用cellForRowAtIndexPath()。我将layoutSubviews()子类化(感谢@Eric MORAND)并添加了一个委托回调:

<强> 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

<强>用法:

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

备注: 由于这是UITableView的子类,它已经有一个指向MyTableViewController的委托属性,因此无需添加另一个属性。 “@dynamic delegate”告诉编译器使用此属性。 (这是一个描述这个的链接:http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/

必须更改UITableView中的MyTableViewController属性才能使用新的SDTableView类。这是在Interface Builder Identity Inspector中完成的。选择UITableView内的UITableViewController,将其“自定义类”设置为SDTableView

答案 6 :(得分:5)

我找到了类似的内容,以便在contentSize TableView中收到有关更改的通知。我认为这也应该适用于此,因为contentSize也会随着加载数据而改变。

试试这个:

viewDidLoad写,

[self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];

并将此方法添加到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
            }
        }


    }
}

您可能需要稍微修改检查更改。这虽然对我有用。

干杯! :)

答案 7 :(得分:2)

这是一个可能的解决方案,虽然它是一个黑客:

[self.tableView reloadData];
[self performSelector:@selector(scrollTableView) withObject:nil afterDelay:0.3];

您的-scrollTableView方法使用-scrollRectToVisible:animated:滚动表格视图。当然,您可以将上面代码中的延迟从0.3配置为适合您的任何内容。是的,这是荒谬的hacky,但它适用于我的iPhone 5和4S ......

答案 8 :(得分:1)

我相信我有类似的东西。我添加了一个BOOL作为实例变量,它告诉我偏移是否已经恢复并在-viewWillAppear:中检查。当它还没有恢复时,我在该方法中恢复它并设置BOOL以指示我确实恢复了偏移。

这是一种破解,它可能会做得更好,但这对我来说很有用。

答案 9 :(得分:1)

听起来你想要更新细胞内容,但没有伴随细胞插入和删除的突然跳跃。

有几篇关于这样做的文章。 This is one.

我建议使用setContentOffset:animated:而不是scrollRectToVisible:animated:用于滚动视图的像素完美设置。

答案 10 :(得分:1)

您可以尝试以下逻辑:

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

在调用reloadData之前,请将prevIndexPath设置为nil。像:

prevIndexPath = nil;
[mainTableView reloadData];

我用NSLogs测试过,这个逻辑似乎没问题。您可以根据需要自定义/改进。

答案 11 :(得分:0)

您可以在加载所有数据时调整tableview的大小或在此方法中设置内容大小:

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

答案 12 :(得分:0)

我在Swift中找到的最佳解决方案

extension UITableView {
    func reloadData(completion: ()->()) {
        self.reloadData()
        dispatch_async(dispatch_get_main_queue()) {
            completion()
        }
    }
}

答案 13 :(得分:0)

从iOS 6开始,UITableview委托方法调用:

-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
一旦表重新加载成功,

将执行。您可以根据需要在此方法中进行自定义。

答案 14 :(得分:0)

我只是在tableHeaderView高度(表示表中有行内容)时,只有当table的contentSize更大时,才会运行重复计划的计时器并使其无效。 C#中的代码(monotouch),但我希望这个想法很明确:

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

答案 15 :(得分:0)

最后我让我的代码使用了这个 -

[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];

很少有事情需要照顾 -

  1. 在“- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  2. 中调用它
  3. 确保将“scrollToRowAtIndexPath”消息发送到UITableView的相关实例,在这种情况下肯定是MyTableview。
  4. 在我的情况下,UIView是包含UITableView
  5. 实例的视图
  6. 此外,还会针对每个单元负载调用此方法。因此,在“cellForRowAtIndexPath”中设置一个逻辑,以避免多次调用“scrollToRowAtIndexPath”。

答案 16 :(得分:0)

在表格视图显示内容之前,是否UITableView layoutSubviews被调用了吗?我注意到一旦表视图加载完它的数据就会调用它,也许你应该调查那个方向。

答案 17 :(得分:-1)

为什么不延伸?

@interface UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
@end

@implementation UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
    [self reloadData];
    if(completionBlock) {
        completionBlock();
    }
}
@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];
    }
}];

未经过大量数据测试