是否有某种方法可以查明 UITableView
何时完成从其数据源请求数据?
关联的视图控制器 (UITableViewController
) 的 viewDidLoad
/viewWillAppear
/viewDidAppear
方法在这里都没有用 因为它们都触发得太早了。它们中的任何一个(完全可以理解)都不能保证对数据源的查询暂时已经完成(例如,直到视图被滚动)。
我发现的一种解决方法是在 viewDidAppear
中调用 reloadData
,因为当 reloadData
返回时,表视图 保证已完成对数据源的查询暂时。
但是,这似乎很讨厌,因为我认为它会导致数据源在首次加载时被要求提供相同的信息两次(一次是自动的,一次是因为 reloadData
调用)。
我想这样做的原因是我想保留 UITableView
的滚动位置 - 但一直到像素级别,而不仅仅是最近的行。
恢复滚动位置时(使用 scrollRectToVisible:animated:
),我需要表格视图中已经有足够的数据,否则 scrollRectToVisible:animated:
方法调用什么也不做(如果您将调用单独放在任何viewDidLoad
、viewWillAppear
或 viewDidAppear
)。
由于自编写答案以来对 UITableView 实现进行了一些更改,此答案似乎不再有效。查看此评论:Get notified when UITableView has finished asking for data?
我已经玩了几天这个问题,并认为子类化 UITableView
的 reloadData
是最好的方法:
- (void)reloadData {
NSLog(@"BEGIN reloadData");
[super reloadData];
NSLog(@"END reloadData");
}
reloadData
不会在表完成重新加载其数据之前结束。因此,当第二个 NSLog
被触发时,表格视图实际上已经完成了数据请求。
我对 UITableView
进行了子类化,以便在 reloadData
之前和之后向委托发送方法。它就像一个魅力。
我的应用程序中确实有相同的情况,并认为会将我的答案发布给你们,因为此处提到的其他答案不适用于 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
编辑:这个答案实际上不是一个解决方案。一开始它可能看起来可以工作,因为重新加载可以很快发生,但实际上完成块不一定在数据完全完成重新加载后被调用 - 因为 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
}];
dispatch_async(dispatch_get_main_queue(), ^{completionBlock();});
。这基本上跳过了 reloadData
中表格视图发布的块。
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% 工作并用于许多项目。这是一个简单的 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
或使用 dispatch_asynch
在主线程上执行不适用于 iOS 9。
这是对稍微不同的问题的回答:我需要知道 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
。
我发现了类似于在 TableView
的 contentSize
中获取更改通知的内容。我认为这也应该在这里工作,因为 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
}
}
}
}
您可能需要在检查更改时稍作修改。不过,这对我有用。
干杯! :)
-viewWillAppear
中的观察者并在 -viewWillDisapear
方法中删除自己。
这是一个可能的解决方案,虽然它是一个黑客:
[self.tableView reloadData];
[self performSelector:@selector(scrollTableView) withObject:nil afterDelay:0.3];
您的 -scrollTableView
方法使用 -scrollRectToVisible:animated:
滚动表格视图。而且,当然,您可以将上面代码中的延迟从 0.3 配置为似乎对您有用的任何值。是的,这很可笑,但它适用于我的 iPhone 5 和 4S...
我相信我有类似的东西。我添加了一个 BOOL 作为实例变量,它告诉我偏移是否已恢复并在 -viewWillAppear:
中检查。当它没有被恢复时,我用那个方法恢复它并设置 BOOL 来表示我确实恢复了偏移量。
这是一种 hack,它可能可以做得更好,但目前这对我有用。
-viewDidLoad
中的偏移量(当然它应该发生的地方),但这只有在我设置偏移量动画时才有效。通过将偏移量设置移动到 -viewWillAppear:
它可以工作,但我必须维护一个标志以仅设置一次。我想表视图在添加到视图后会重新加载其数据,所以这已经在 -loadView
中。您确定您的数据可用于视图加载吗?或者它是在一个单独的线程中加载的还是什么?
听起来您想更新单元格内容,但没有伴随单元格插入和删除的突然跳跃。
有几篇关于这样做的文章。 This is one.
我建议使用 setContentOffset:animated: 而不是 scrollRectToVisible:animated: 来实现滚动视图的像素完美设置。
你可以试试下面的逻辑:
-(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 进行了测试,这个逻辑似乎还可以。您可以根据需要定制/改进。
最后我已经让我的代码与这个一起工作 -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
有几件事情需要照顾 -
在“- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath”中调用它,只需确保将“scrollToRowAtIndexPath”消息发送到 UITableView 的相关实例,在这种情况下肯定是 MyTableview。在我的情况下, UIView 是包含 UITableView 实例的视图,这将在每个单元格加载时调用。因此,在“cellForRowAtIndexPath”中放置一个逻辑以避免多次调用“scrollToRowAtIndexPath”。
加载所有数据后,您可以在此方法中调整 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);
}
我只是运行重复的预定计时器,并且仅当 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;
}
}));
}
}
UITableView
layoutSubviews
不是在表格视图显示其内容之前调用吗?我注意到一旦表格视图完成加载其数据,它就会被调用,也许你应该朝那个方向进行调查。
从 iOS 6 开始,UITableview
委托方法调用:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
将在您的表重新加载成功后执行。您可以在此方法中根据需要进行自定义。
我在 Swift 中找到的最佳解决方案
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
为什么不只是扩展?
@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];
}
}];
未经大量数据测试
不定期副业成功案例分享
reloadData
立即返回,并且我看到“END reloadData”在单元格实际重新加载之前(即在调用UITableViewDataSource
方法之前)。我的实验证明与你所说的完全相反。我一定误解了你想说什么。[super reloadData]
后将完成代码发布到主队列的块中对我有用:dispatch_async(dispatch_get_main_queue(), ^{NSLog(@"reload complete");});
。它基本上跳过了reloadData
中表格视图发布的块。