ChatGPT解决这个技术问题 Extra ChatGPT

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

是否有某种方法可以查明 UITableView 何时完成从其数据源请求数据?

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

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

但是,这似乎很讨厌,因为我认为它会导致数据源在首次加载时被要求提供相同的信息两次(一次是自动的,一次是因为 reloadData 调用)。

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

恢复滚动位置时(使用 scrollRectToVisible:animated:),我需要表格视图中已经有足够的数据,否则 scrollRectToVisible:animated: 方法调用什么也不做(如果您将调用单独放在任何viewDidLoadviewWillAppearviewDidAppear)。

您似乎正在寻找类似于此 stackoverflow.com/a/11672379/2082172 的内容。
根据我的经验,UITableView 可以缓存对 reloadData 的调用,就像它缓存对插入/删除行和其他内容的调用一样。 UITableView 将在准备好后调用数据源委托,具体取决于 cpu 使用情况和其他情况。

C
Community

由于自编写答案以来对 UITableView 实现进行了一些更改,此答案似乎不再有效。查看此评论:Get notified when UITableView has finished asking for data?

我已经玩了几天这个问题,并认为子类化 UITableViewreloadData 是最好的方法:

- (void)reloadData {

    NSLog(@"BEGIN reloadData");

    [super reloadData];

    NSLog(@"END reloadData");

}

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

我对 UITableView 进行了子类化,以便在 reloadData 之前和之后向委托发送方法。它就像一个魅力。


不得不说,这似乎是最好的解决方案。刚刚实现它,就像你说的,它就像一个魅力。谢谢!
@EricMORAND您说“在表完成重新加载数据之前,reloadData 不会结束。”你能澄清一下你的意思吗?我发现 reloadData 立即返回,并且我看到“END reloadData”单元格实际重新加载之前(即在调用 UITableViewDataSource 方法之前)。我的实验证明与你所说的完全相反。我一定误解了你想说什么。
埃里克的回答与不实现(读取而不是覆盖)reloaddata,调用reloaddata(本质上将是[super reloaddata],然后在调用它之后,在它完成时做你想要的东西吗?
曾几何时,reloadData 确实在加载过程结束之前完成了加载过程。但苹果在某个时候改变了它。现在 UITableView 类可以缓存所有行插入和删除调用的 reloadData 调用。如果您查看 UITableView 的 @interface 声明,您会在 _insertItems 和 _deleteItems 下找到 NSMutableArray 成员 _reloadItems。 (由于此更改,我不得不重新编写继承的代码。)
在调用 [super reloadData] 后将完成代码发布到主队列的块中对我有用:dispatch_async(dispatch_get_main_queue(), ^{NSLog(@"reload complete");});。它基本上跳过了 reloadData 中表格视图发布的块。
i
ipraba

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

https://i.stack.imgur.com/P7D1t.png


我找到的最好的答案!
@Gon 我确实在 iOS9 中进行了测试,效果很好。能否请您参考一下github.com/ipraba/TableReloadingNotifier
它在我的模拟器上运行良好,但似乎不适用于实际设备。还有谁有相同的问题吗?
这个解决方案终于对我有用!它在 iOS 9 上运行良好
我在 Real Device 上进行了测试,iPhone 6s。它也很好用。
b
bandejapaisa

编辑:这个答案实际上不是一个解决方案。一开始它可能看起来可以工作,因为重新加载可以很快发生,但实际上完成块不一定在数据完全完成重新加载后被调用 - 因为 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
                                        }];

我无法获得足够的积木。当你有一个很好的块时,谁需要一个代表!
我喜欢这样,但是当系统调用 reloadData() 时,例如第一次显示表格时呢?
这不是解决方案 Completion 块在 cellForRowAtIndexPath 之前开始执行
这行不通; reloadData 不是阻塞方法。调用 reloadData 后立即调用此代码,即使尚未重新加载单元格也是如此。另外,看看这段代码,你会发现你还不如把你的代码放在reloadData之后。
这对我有用,只需稍作改动。不要直接调用完成块,而是在主队列上发布的块中调用它:dispatch_async(dispatch_get_main_queue(), ^{completionBlock();});。这基本上跳过了 reloadData 中表格视图发布的块。
N
Natan R.

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
}

我不确定这是否有效。最后一个索引是表格数据的末尾(例如 100 条记录),但表格只会显示屏幕上可见的内容(例如 8 条记录)。
C
Community

这就是我的解决方案。 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];

享受。 :)


使用 performSelector 调用非常棒。简单而有效,谢谢
这真是太棒了。这几天我一直在纠结这个。谢谢!
@MarkKryzhanouski,你在 iOS 9 上测试过吗?
@MarkKryzhanouski,感谢您的及时反馈!在 iOS 7 和 8 上运行良好。
解决方案 performSelector 或使用 dispatch_asynch 在主线程上执行不适用于 iOS 9
D
Dharmesh Dhorajiya

这是对稍微不同的问题的回答:我需要知道 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/

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


您在哪里将 MyTableViewController 设置为 SDTableView 的委托?当它的超类已经具有该名称的属性(UITableView.delegate)时,为什么可以在 SDTableView 上设置名称为“delegate”的属性?当属性为“UITablewView”类型且对象(SDTableView 的实例)为“SDTableView”类型时,如何将自定义 SDTableView 附加到 MyTableViewController.tableView 属性?我正在解决同样的问题,所以我希望有解决这些问题的方法:)
在 Symmetric (SDTableView) 的代码中,不应该在 didReloadData 而不是 didLayoutSubviews 中将 reloadInProgress 设置为 FALSE 吗?因为 reloadData 在 layoutSubviews 之后调用,并且在 reloadData 完成之前不应考虑加载。
这不适用于 iOS 8,必须合并 iPrabu's answer
A
Abdullah Umer

我发现了类似于在 TableViewcontentSize 中获取更改通知的内容。我认为这也应该在这里工作,因为 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
            }
        }


    }
}

您可能需要在检查更改时稍作修改。不过,这对我有用。

干杯! :)


非常聪明。非常适合我。感谢分享!
优雅的!我唯一要补充的是,您需要删除自己和观察者(可能在 -dealloc 方法中)。或者将自己添加为 -viewWillAppear 中的观察者并在 -viewWillDisapear 方法中删除自己。
J
Josh Brown

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

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

您的 -scrollTableView 方法使用 -scrollRectToVisible:animated: 滚动表格视图。而且,当然,您可以将上面代码中的延迟从 0.3 配置为似乎对您有用的任何值。是的,这很可笑,但它适用于我的 iPhone 5 和 4S...


它可能看起来“hacky”,但这确实是滚动的标准方法:将其推迟到下一个运行周期。我会将延迟更改为 0,因为它同样有效并且延迟更少。
延迟设置为 0 对我不起作用; 0.3 是我可以达到的最低值,并且仍然可以获取数据。
@phatmann 这对我有用。我也可以使用 0 来延迟。感谢你们俩。
J
Joost

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

这是一种 hack,它可能可以做得更好,但目前这对我有用。


是的,问题是 viewWillAppear 似乎为时过早(至少在我的场景中)。试图恢复 viewWillAppear 中的偏移量什么都不做——除非我先添加调用 reloadData 的技巧。据我了解,viewWillAppear/viewDidAppear 仅指实际出现的表格视图本身 - 他们没有就视图中的表格单元格是否已被枚举或填充......这需要在您可以恢复之前发生偏移量,否则您将在空视图上恢复偏移量(我明白为什么这不起作用!)。
但也许您的意思是您还通过在 viewWillAppear 中调用 reloadData 来强制加载表格单元格?
不,我不是在强制重新加载。当我遇到问题时,我尝试恢复 -viewDidLoad 中的偏移量(当然它应该发生的地方),但这只有在我设置偏移量动画时才有效。通过将偏移量设置移动到 -viewWillAppear: 它可以工作,但我必须维护一个标志以仅设置一次。我想表视图在添加到视图后会重新加载其数据,所以这已经在 -loadView 中。您确定您的数据可用于视图加载吗?或者它是在一个单独的线程中加载的还是什么?
好的,也许你可以在这里帮助我理解。这就是我理解 UITableView 构建时调用顺序的方式。 1) viewDidLoad 首先被触发。这表明 UITableView 已加载到内存中。 2) viewWillAppear 在火旁边。这表示 UITableView 将被显示,但不一定所有可见的 UITableViewCell 对象都将被完全实例化/完成渲染。
以上所有内容都是正确的,那么问题是:我们有哪些非hacky选项可以找出UITableView何时从其数据源获得足够的信息以保证滚动请求(即scrollRectToVisible:animated:调用) 会真正起作用(而不仅仅是什么都不做)?
C
Community

听起来您想更新单元格内容,但没有伴随单元格插入和删除的突然跳跃。

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

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


A
Abdullah Umer

你可以试试下面的逻辑:

-(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 进行了测试,这个逻辑似乎还可以。您可以根据需要定制/改进。


s
smile.al.d.way

最后我已经让我的代码与这个一起工作 -

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

有几件事情需要照顾 -

在“- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath”中调用它,只需确保将“scrollToRowAtIndexPath”消息发送到 UITableView 的相关实例,在这种情况下肯定是 MyTableview。在我的情况下, UIView 是包含 UITableView 实例的视图,这将在每个单元格加载时调用。因此,在“cellForRowAtIndexPath”中放置一个逻辑以避免多次调用“scrollToRowAtIndexPath”。


并确保这件作品不会被多次调用。如果不是,它会锁定滚动。
这可能有效,但它绝对不优雅,也不是我正在寻找的解决方案。不幸的是,似乎没有更好的方法来确定 -reloadData 是否实际上已完成获取数据......
i
iDev

加载所有数据后,您可以在此方法中调整 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);
}

E
Eugene Tiutiunnyk

我只是运行重复的预定计时器,并且仅当 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;
                    }
                }));
        }
    }

R
Rahul Mane

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


h
honk

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

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

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


Y
YannSteph

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

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

A
Andrii Tishchenko

为什么不只是扩展?

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

未经大量数据测试