Получать уведомление, когда UITableView закончил запрашивать данные?

Есть ли способ узнать, когда UITableView закончил запрашивать данные из своего источника данных?

Ни один из методов viewDidLoad/viewWillAppear/viewDidAppear связанного контроллера представления (UITableViewController) здесь не используется, поскольку все они запускаются слишком рано. Ни один из них (вполне понятно) не гарантирует, что запросы к источнику данных закончатся (например, пока прокрутка будет прокручена).

Обходной путь, который я нашел, заключается в вызове reloadData в viewDidAppear, поскольку, когда reloadData возвращает, представление таблицы, как гарантируется, завершает запрос источника данных столько, сколько ему нужно.

Однако это кажется довольно неприятным, так как я полагаю, что он дважды запрашивает одну и ту же информацию о источнике данных (один раз автоматически и один раз из-за вызова reloadData) при первом загрузке.

Причина, по которой я хочу сделать это, заключается в том, что я хочу сохранить положение прокрутки UITableView, но вплоть до уровня пикселя, а не только до ближайшей строки.

При восстановлении позиции прокрутки (используя scrollRectToVisible:animated:) мне нужно, чтобы в представлении таблицы уже было достаточно данных, иначе вызов метода scrollRectToVisible:animated: ничего не делает (что происходит, если вы поместите вызов на свой принадлежит в любом из viewDidLoad, viewWillAppear или viewDidAppear).

Ответ 1

Этот ответ, похоже, больше не работает, из-за некоторых изменений, внесенных в реализацию UITableView с момента написания ответа. См. Этот комментарий: Получать уведомление, когда UITableView закончил запрашивать данные?

Я играю с этой проблемой в течение нескольких дней и думаю, что подкласс UITableView reloadData - лучший подход:

- (void)reloadData {

    NSLog(@"BEGIN reloadData");

    [super reloadData];

    NSLog(@"END reloadData");

}

reloadData не заканчивается до того, как таблица завершит перезагрузку своих данных. Итак, когда второй NSLog запущен, представление таблицы фактически завершает запрос данных.

Я подклассифицировал UITableView, чтобы отправить методы делегату до и после reloadData. Он работает как шарм.

Ответ 2

У меня был такой же сценарий в моем приложении, и я подумал, что вы ответите мне, ребята, потому что другие ответы, упомянутые здесь, не работают для меня для 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)

})

Итак, как это работает.

В основном, когда вы выполняете перезагрузку, основной поток становится занятым, поэтому в то время, когда мы выполняем асинхронный поток отправки, блок будет ждать завершения основного потока. Поэтому, как только таблица будет полностью загружена, основной поток будет завершен, и поэтому он отправит наш блок методов

Протестировано в iOS7 и iOS8, и он работает потрясающе;)

Обновление для iOS9:. Это просто отлично работает с iOS9. Я создал образец проекта в github как POC. https://github.com/ipraba/TableReloadingNotifier

Я прилагаю скриншот моего теста здесь.

Протестированная среда: iOS9 iPhone6 ​​симулятор от Xcode7

введите описание изображения здесь

Ответ 3

EDIT: этот ответ на самом деле не является решением. Вероятно, он работает сначала, потому что перезагрузка может произойти довольно быстро, но на самом деле блок завершения не обязательно вызывается после того, как данные полностью завершили перезагрузку, потому что 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
                                        }];

Ответ 4

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
}

Ответ 5

Это мое решение. 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: всегда срабатывает, когда 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];

Enjoy.:)

Ответ 6

Это ответ на несколько иной вопрос: мне нужно было знать, когда 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, у которого уже есть свойство delegate, указывающее на MyTableViewController, нет необходимости добавлять еще один. "@dynamic delegate" сообщает компилятору использовать это свойство. (Здесь ссылка, описывающая это: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/)

Свойство UITableView в MyTableViewController должно быть изменено для использования нового класса SDTableView. Это делается в Индексе идентификации идентификатора интерфейса. Выберите UITableView внутри UITableViewController и установите его "Пользовательский класс" на SDTableView.

Ответ 7

Я нашел нечто подобное, чтобы получить уведомление об изменении в 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
            }
        }


    }
}

Возможно, вам понадобятся небольшие изменения в проверке изменений. Это работало для меня, хотя.

Ура!:)

Ответ 8

Вот возможное решение, хотя это взломать:

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

Где ваш метод -scrollTableView прокручивает представление таблицы с помощью -scrollRectToVisible:animated:. И, конечно же, вы можете настроить задержку в коде выше от 0,3 до того, что, похоже, сработает для вас. Да, это смешно взломано, но это работает для меня на iPhone 5 и 4S...

Ответ 9

У меня было что-то похожее, я верю. Я добавил переменную экземпляра BOOL, которая сообщает мне, восстановлено ли смещение и проверьте, что в -viewWillAppear:. Когда он не был восстановлен, я восстанавливаю его в этом методе и устанавливаю BOOL, чтобы указать, что я восстановил смещение.

Это своего рода взлом, и, вероятно, это может быть сделано лучше, но это работает для меня в данный момент.

Ответ 10

Похоже, вы хотите обновить содержимое ячейки, но без внезапных переходов, которые могут сопровождать вставки и удаления ячеек.

Есть несколько статей об этом. Это один.

Я предлагаю использовать setContentOffset: анимированный: вместо scrollRectToVisible: анимированный: для идеальных для пикселя настроек прокрутки.

Ответ 11

Вы можете попробовать следующую логику:

-(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, и эта логика выглядит нормально. Вы можете настроить/улучшить по мере необходимости.

Ответ 12

Разве не UITableView layoutSubviews вызывается непосредственно перед отображением его в виде таблицы? Я заметил, что он вызывается после того, как представление таблицы завершило загрузку своих данных, возможно, вам следует исследовать в этом направлении.

Ответ 13

Наконец, я сделал свой код с этим -

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

было немного вещей, о которых нужно было позаботиться -

  • назовите его "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
  • просто убедитесь, что сообщение "scrollToRowAtIndexPath" отправлено в соответствующий экземпляр UITableView, который в данном случае определенно является MyTableview.
  • В моем случае UIView - это представление, которое содержит экземпляр UITableView
  • Кроме того, это будет вызвано для каждой загрузки ячеек. Поэтому, выведите логику внутри "cellForRowAtIndexPath", чтобы избежать вызова "scrollToRowAtIndexPath" более одного раза.

Ответ 14

Вы можете изменить размер табличного представления или задать его размер содержимого в этом методе, когда все загруженные данные:

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

Ответ 15

Я просто запускаю повторный запланированный таймер и аннулирую его, только когда table contentSize больше при высоте tableHeaderView (означает, что в таблице есть содержимое строк). Код в С# (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;
                    }
                }));
        }
    }

Ответ 16

Начиная с iOS 6, метод делегата UITableview, называемый:

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

будет выполняться после успешной перезагрузки вашей таблицы. Вы можете выполнить настройку, как требуется в этом методе.

Ответ 17

Лучшее решение, которое я нашел в Swift

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

Ответ 18

Почему просто не растягивается?

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

Не проверено с большим количеством данных