UITableView完成查询数据后会收到通知吗?


108

有什么方法可以找出何时UITableView从其数据源中查询数据?

的无viewDidLoad/ viewWillAppear/ viewDidAppear关联的视图控制器(方法UITableViewController)是使用在这里,因为他们都火得太早。它们(完全可以理解)都不能保证对数据源的查询暂时已经完成(例如,直到视图滚动为止)。

一个解决办法,我发现是调用reloadDataviewDidAppear,因为当reloadData返回时,表视图保证完成查询数据源,它更需要的时间存在。

但是,这似乎有点令人讨厌,因为我认为这会导致在reloadData首次加载数据源时两次(自动一次,由于调用而一次)要求输入相同的信息。

我要执行此操作的原因是我想保留-的滚动位置,UITableView但要一直到像素级别,而不仅仅是到最近的行。

当恢复滚动位置(使用scrollRectToVisible:animated:),我需要的表视图已经有足够的数据在里面,否则scrollRectToVisible:animated:方法调用什么也不做(这是如果你把它自己的呼叫任何会发生什么viewDidLoadviewWillAppearviewDidAppear)。


看来您正在寻找与此stackoverflow.com/a/11672379/2082172类似的东西。
Timur Kuchkarov

以我的经验,UITableView可以缓存对reloadData的调用,就像它缓存对插入/删除行和其他内容的调用一样。UITableView准备就绪时将调用数据源委托,具体取决于cpu的使用情况和其他因素。
Walt Sellers

Answers:


61

由于自编写答案以来对UITableView实现进行了一些更改,因此该答案似乎不再有效。看到这个评论:UITableView完成请求数据时得到通知?

我一直在玩这个问题困扰了几天,觉得子类UITableViewreloadData是最好的方法:

- (void)reloadData {

    NSLog(@"BEGIN reloadData");

    [super reloadData];

    NSLog(@"END reloadData");

}

reloadData在表完成重新加载其数据之前不会结束。因此,当第二个NSLog被触发时,表视图实际上已经完成了对数据的查询。

我已经将其子类化UITableView,以便在之前和之后将方法发送给委托reloadData。它像一种魅力。


2
得说,这似乎是最好的解决方案。只需实施它,就像您说的那样,它就像一种魅力。谢谢!
理论

2
@EricMORAND您说“ reloadData不会在表完成重新加载其数据之前结束。” 您能阐明您的意思吗?我发现,reloadData立即返回,我看到“END reloadData” 之前的细胞实际上是重新加载(IE浏览器之前UITableViewDataSource被调用的方法)。我的实验证明了您所说的恰恰相反。我一定会误解你想说的话。
罗布

3
埃里克(Eric)的答案是否与未实现(读取未覆盖)reloaddata,调用reloaddata(本质上是[super reloaddata],然后在调用它之后,完成后想要的东西)不同?
Nirav Bhatt

5
从前,reloadData在结束之前确实完成了加载过程。但是苹果在某个时候改变了它。现在,UITableView类可以缓存带有所有行插入和删除调用的reloadData调用。如果查看UITableView的@interface声明,则会在_insertItems和_deleteItems的正下方找到NSMutableArray成员_reloadItems。(由于此更改,我不得不重做继承的代码。)
Walt Sellers

3
调用后,将完成代码发布到主队列中的某个块中对[super reloadData]我有用:dispatch_async(dispatch_get_main_queue(), ^{NSLog(@"reload complete");});。它基本上跳过了表视图中发布的块reloadData
Timothy Moose 2013年

53

我的应用中确实有相同的情况,并认为会将我的答案发布给大家,因为此处提到的其他答案不适用于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模拟器

在此处输入图片说明


8
我发现的最好的answear!
Nikolay Shubenkov 2014年

@Gon我确实在iOS9中进行了测试,但效果很好。能否请您参阅github.com/ipraba/TableReloadingNotifier
iPrabu 2015年

它在我的模拟器上可以正常工作,但在实际设备上似乎无法正常工作。还有谁有相同的问题吗?
sosale151

1
这个解决方案终于对我有用!它在iOS 9上运行良好

1
我已经在iPhone 6s的Real Device上进行了测试。它也很好。
2015年

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

2
我的积木不够用。当您遇到障碍时,谁需要代表!
bandejapaisa 2012年

我喜欢这样,但是系统调用reloadData()的时间如何?例如第一次显示表时呢?
2012年

12
这不是解决方案完成块在cellForRowAtIndexPath之前开始执行
zvjerka24 2013年

1
这行不通;reloadData不是阻塞方法。调用reloadData之后立即调用此代码,即使尚未重新加载单元也是如此。此外,查看此代码,您会发现您也可以将代码放在reloadData之后
colinta

1
这对我有用,但有一点改动。与其直接调用完成块,不如在主队列上发布的块中调用它dispatch_async(dispatch_get_main_queue(), ^{completionBlock();});。这基本上跳过了表视图中发布的块reloadData
蒂莫西·穆斯

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
}

1
我不确定这项工作是否可行。最后一个索引是表数据的末尾(例如100条记录),但是表将仅显示屏幕上可见的内容(例如8条记录)。
特拉维斯M.14年

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

除了一个例外,它类似于乔什·布朗的解决方案。performSelector方法无需延迟。不管reloadData需要多长时间。问完tableViewDidLoadData:总是开枪。tableViewdataSource cellForRowAtIndexPath

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

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

请享用。:)


1
使用performSelector调用非常出色。简单实用,thanx
朱莉娅

太棒了 我已经为此争论了好几天。谢谢!
戴维·卡里科

@MarkKryzhanouski,您是否在iOS 9上进行了测试?
维克多2015年

@MarkKryzhanouski,感谢您的及时反馈!iOS上的7和8的伟大工程
维克多

解决方案performSelector或在主线程上dispatch_asynch执行在iOS 9上不起作用。
曼努埃尔·2015年

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无需再添加一个委托属性。“ @动态委托”告诉编译器使用此属性。(以下是描述此链接:http : //farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/

必须更改中的UITableView属性MyTableViewController以使用新SDTableView类。这是在Interface Builder身份检查器中完成的。选择的UITableView内部,UITableViewController并将其“自定义类”设置为SDTableView


您在哪里将MyTableViewController设置为SDTableView的委托?当SDTableView的超类已经具有该名称的属性(UITableView.delegate)时,怎么可能在SDTableView上设置名称“ delegate”的属性?当属性为“ UITablewView”类型且对象(SDTableView的实例)为“ SDTableView”类型时,如何将自定义SDTableView分配给MyTableViewController.tableView属性?我正在解决相同的问题,所以我希望有解决这些问题的方法:)
Earl Gray

在对称代码(SDTableView)中,是否应该在didReloadData中而不是didLayoutSubviews中将reloadInProgress设置为FALSE?因为reloadData是在layoutSubviews之后调用的,所以不应认为加载是在reloadData完成之前完成的。
tzuchien.chiu 2012年

这不适用于iOS 8,必须合并iPrabu的答案
Stunner

6

我已经发现了类似以获取更改通知的东西contentSizeTableView。我认为这也应该在这里工作,因为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
            }
        }


    }
}

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

干杯! :)


1
非常聪明。非常适合我。感谢分享!
DZenBot

2
优雅!我唯一要添加的是您需要删除自己和一个观察者(也许在-dealloc方法中)。或在中将自己添加为观察者,-viewWillAppear然后在-viewWillDisapear方法中将自己删除。
Loozie 2015年

2

这是一个可能的解决方案,尽管它是一个hack:

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

您的-scrollTableView方法使用滚动表格视图的位置-scrollRectToVisible:animated:。而且,当然,您可以将上述代码中的延迟从0.3配置为适合您的延迟。是的,这很荒谬,但是它可以在我的iPhone 5和4S上使用。


2
看上去“ hacky”,但这实际上是滚动的标准方法:将其推迟到下一个运行周期。我会将延迟更改为0,因为那样效果很好,并且延迟更短。
phatmann

延迟设置为0对我不起作用;0.3是我能获得的最低数据。
乔什·布朗

@phatmann这对我有用。我也可以使用0进行延迟。感谢你们俩。
mcphersonjr 2014年

1

我相信我也有类似的事情。我添加了一个BOOL作为实例变量,该变量告诉我偏移量是否已还原,并在中进行检查-viewWillAppear:。当尚未还原时,我用该方法还原它,并将BOOL设置为指示我确实恢复了偏移量。

这是一种破解,可能可以做得更好,但是此刻对我而言有效。


是的,问题在于viewWillAppear似乎为时过早(至少在我的情况下)。尝试在viewWillAppear中恢复偏移量没有任何作用-除非我先添加调用reloadData的技巧。据我了解,viewWillAppear / viewDidAppear仅指实际出现的表视图-他们不对视图中的表单元格是否已枚举或填充进行任何声明...并且这需要在恢复之前发生一个偏移量,否则您将在一个空视图中恢复一个偏移量(我知道为什么这不起作用!)。
kennethmac2000 2009年

但是也许您的意思是,您还希望通过先在viewWillAppear中调用reloadData来强制加载表单元格吗?
kennethmac2000 2009年

不,我不强制重新加载。当我遇到问题时,我尝试还原偏移-viewDidLoad(当然应该在其中发生),但这仅在将偏移设置为动画时才起作用。通过将偏移量的设置移动到-viewWillAppear:它的工作,但我必须维护一个标志,仅设置一次。我想表视图一旦添加到视图后便会重新加载其数据,因此已经在中-loadView。您确定您的数据在视图加载时可用吗?还是将其加载到单独的线程中?
Joost

好的,也许您可​​以在这里帮助我的理解。这就是我理解建立UITableView时的调用顺序的方式。1)首先触发viewDidLoad。这表明UITableView已加载到内存中。2)viewWillAppear靠近火。这表明将显示UITableView,但不一定所有可见的UITableViewCell对象都将完全实例化/完成渲染。
kennethmac2000

2
上面所有这些都是正确的,那么问题是:当UITableView从其数据源获取足够的信息以保证滚动请求(例如,scrollRectToVisible:animated:call)时,我们有哪些非hacky选项可用于查找)会真正起作用(而不是什么也不做)?
kennethmac2000

1

听起来好像您要更新单元格内容,但要避免单元格插入和删除伴随的突然跳跃。

有几篇有关此操作的文章。 这是一。

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


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进行了测试,这种逻辑看起来还可以。您可以根据需要自定义/改进。


0

终于我使我的代码可以使用了-

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

有几件事需要照顾-

  1. 在“ - (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath” 内称呼它
  2. 只需确保将“ scrollToRowAtIndexPath”消息发送到UITableView的相关实例即可,在这种情况下,该实例肯定是MyTableview。
  3. 在我的情况下,UIView是包含UITableView实例的视图
  4. 同样,将为每个单元负载调用此方法。因此,请在“ cellForRowAtIndexPath”内部放置一个逻辑,以避免多次调用“ scrollToRowAtIndexPath”。

并确保该片段不会被多次调用。如果没有,则锁定滚动。
smile.al.d.way 10-10-6

这可能有效,但是绝对不是很优雅,也不是我想要的解决方案。不幸的是,似乎没有更好的方法来确定-reloadData是否实际上已经完成了数据的获取……
Josh Brown

0

加载所有数据后,可以使用以下方法调整表格视图的大小或设置其内容大小:

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

0

我只是运行重复的计划计时器,并且仅当tableHeaderView高度(表中存在行内容)时表的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;
                    }
                }));
        }
    }

0

是不是UITableView layoutSubviews在表格视图显示内容之前就调用了它?我注意到,一旦表视图完成了其数据的加载,便会调用它,也许您应该朝这个方向进行研究。


0

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

-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section

成功加载表后,将执行。您可以根据需要在此方法中进行自定义。


0

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

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

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

未经大量数据测试

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.