Как определить конец загрузки UITableView

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

В любом случае на SDK знать, когда загрузка uitableview закончилась? Я не вижу ничего ни в делегате, ни в протоколах источника данных.

Я не могу использовать количество источников данных из-за загрузки только видимых ячеек.

Ответ 1

Улучшите ответ @RichX: lastRow может быть как [tableView numberOfRowsInSection: 0] - 1, так и ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row. Таким образом, код будет:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

UPDATE: Ну, комментарий @htafoya прав. Если вы хотите, чтобы этот код обнаружил конец загрузки всех данных из источника, это не так, но это не оригинальный вопрос. Этот код предназначен для обнаружения, когда отображаются все ячейки, которые должны быть видимыми. willDisplayCell:, используемый здесь для более плавного пользовательского интерфейса (одиночная ячейка обычно отображается быстро после вызова willDisplay:). Вы также можете попробовать его с помощью tableView:didEndDisplayingCell:.

Ответ 2

Я всегда использую это очень простое решение:

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}

Ответ 3

Версия Swift 3 & 4:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}

Ответ 4

Вот еще один вариант, который, похоже, работает для меня. В методе делегата viewForFooter проверьте, есть ли он в последнем разделе и добавьте там свой код. Этот подход пришел на ум после понимания того, что willDisplayCell не учитывает нижние колонтитулы, если они у вас есть.

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section 
{
  // Perform some final layout updates
  if (section == ([tableView numberOfSections] - 1)) {
    [self tableViewWillFinishLoading:tableView];
  }

  // Return nil, or whatever view you were going to return for the footer
  return nil;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
  // Return 0, or the height for your footer view
  return 0.0;
}

- (void)tableViewWillFinishLoading:(UITableView *)tableView
{
  NSLog(@"finished loading");
}

Я считаю, что этот подход работает лучше всего, если вы хотите найти конечную загрузку для всего UITableView, а не просто видимых ячеек. В зависимости от ваших потребностей вам могут понадобятся только видимые ячейки, и в этом случае folex answer - хороший маршрут.

Ответ 5

Решение Swift 2:

// willDisplay function
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let lastRowIndex = tableView.numberOfRowsInSection(0)
    if indexPath.row == lastRowIndex - 1 {
        fetchNewDataFromServer()
    }
}

// data fetcher function
func fetchNewDataFromServer() {
    if(!loading && !allDataFetched) {
        // call beginUpdates before multiple rows insert operation
        tableView.beginUpdates()
        // for loop
        // insertRowsAtIndexPaths
        tableView.endUpdates()
    }
}

Ответ 6

Попробуйте эту магию:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // cancel the perform request if there is another section
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tableViewDidLoadRows:) object:tableView];

    // create a perform request to call the didLoadRows method on the next event loop.
    [self performSelector:@selector(tableViewDidLoadRows:) withObject:tableView afterDelay:0];

    return self.objects.count;
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    // make the cell selected after all rows loaded
    if(self.selectedObject){
        NSInteger index = [self.objects indexOfObject:self.selectedObject];
        [tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle];
    }
}

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

Ответ 7

Для выбранной версии ответа в Swift 3:

var isLoadingTableView = true

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if tableData.count > 0 && isLoadingTableView {
        if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows, let lastIndexPath = indexPathsForVisibleRows.last, lastIndexPath.row == indexPath.row {
            isLoadingTableView = false
            //do something after table is done loading
        }
    }
}

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

Ответ 9

Вот как вы это делаете в Swift 3:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    if indexPath.row == 0 {
        // perform your logic here, for the first row in the table
    }

    // ....
}

Ответ 10

Вот что я буду делать.

  • В вашем базовом классе (может быть rootVC BaseVc и т.д.),

    а. Напишите протокол для отправки обратного вызова "DidFinishReloading".

    @protocol ReloadComplition <NSObject>
    @required
    - (void)didEndReloading:(UITableView *)tableView;
    @end
    

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

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  • В реализации метода базового класса вызовите reloadData, за которым следует функция delegateMethod с задержкой.

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController{
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            [tableView reloadData];
            if(aViewController && [aViewController respondsToSelector:@selector(didEndReloading:)]){
                [aViewController performSelector:@selector(didEndReloading:) withObject:tableView afterDelay:0];
            }
        }];
    }
    
  • Подтвердите протокол завершения перезагрузки во всех контроллерах представления, где вам нужен обратный вызов.

    -(void)didEndReloading:(UITableView *)tableView{
        //do your stuff.
    }
    

Ссылка: https://discussions.apple.com/thread/2598339?start=0&tstart=0

Ответ 11

вот как я это делаю в Swift 3

let threshold: CGFloat = 76.0 // threshold from bottom of tableView

internal func scrollViewDidScroll(_ scrollView: UIScrollView) {

    let contentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;

    if  (!isLoadingMore) &&  (maximumOffset - contentOffset <= threshold) {
        self.loadVideosList()
    }
}

Ответ 12

Ответ

@folex правильный.

Но он будет выходить из строя, если в таблицеView отображается несколько разделов.

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
   if([indexPath isEqual:((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject])]){
    //end of loading

 }
}

Ответ 13

В Swift вы можете сделать что-то подобное. Следующее условие будет истинным каждый раз, когда вы достигнете конца tableView

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row+1 == postArray.count {
            println("came to last row")
        }
}

Ответ 14

Я знаю, что это ответ, я просто добавляю рекомендацию.

В соответствии со следующей документацией

https://www.objc.io/issues/2-concurrency/thread-safe-class-design/

Исправление проблем синхронизации с dispatch_async - плохая идея. Я предлагаю, чтобы мы справились с этим, добавив FLAG или что-то в этом роде.

Ответ 15

Если у вас несколько разделов, вот как получить последнюю строку в последнем разделе (Swift 3):

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let visibleRows = tableView.indexPathsForVisibleRows, let lastRow = visibleRows.last?.row, let lastSection = visibleRows.map({$0.section}).last {
        if indexPath.row == lastRow && indexPath.section == lastSection {
            // Finished loading visible rows

        }
    }
}

Ответ 16

Совершенно случайно я наткнулся на это решение:

tableView.tableFooterView = UIView()
tableViewHeight.constant = tableView.contentSize.height

Вам нужно установить footerView перед получением contentSize, например, в viewDidLoad. Btw. установка footeView позволяет удалять "неиспользуемые" разделители

Ответ 17

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

В жизненном цикле приложения есть 4 ключевых момента:

  1. Приложение получает событие (касание, таймер, отправка блока и т.д.)
  2. Приложение обрабатывает событие (оно изменяет ограничение, запускает анимацию, меняет фон и т.д.)
  3. Приложение вычисляет новую иерархию представлений
  4. Приложение отображает иерархию представления и отображает ее

2 и 3 раза полностью разделены. Зачем? По соображениям производительности мы не хотим выполнять все вычисления момента 3 каждый раз, когда выполняется модификация.

Итак, я думаю, что вы сталкиваетесь с таким случаем:

tableView.reloadData()
tableView.visibleCells.count // wrong count oO

Что здесь не так?

Как и любое представление, табличное представление перезагружает свой контент лениво. На самом деле, если вы вызываете reloadData несколько раз, это не создает проблем с производительностью. Табличное представление только повторно вычисляет свой размер контента на основе его реализации делегата и ждет момента 3, чтобы загрузить его ячейки. Это время называется проходом макета.

Хорошо, как попасть в макет прохода?

На этапе макета приложение вычисляет все фреймы иерархии представления. Чтобы принять участие, вы можете переопределить выделенные методы layoutSubviews, updateLayoutConstraints т.д. В UIView и эквивалентные методы в подклассе контроллера представления.

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

Хорошо, но теперь, как узнать, когда табличное представление завершило перезагрузку своего содержимого?

Теперь мы можем перефразировать этот вопрос: как узнать, когда табличное представление завершило размещение своих подпредставлений?

Самый простой способ - попасть в макет табличного представления:

class MyTableView: UITableView {
    func layoutSubviews() {
        super.layoutSubviews()
        // the displayed cells are loaded
    }
}

Обратите внимание, что этот метод вызывается много раз в жизненном цикле табличного представления. Из-за прокрутки и поведения очереди в табличном представлении ячейки часто изменяются, удаляются и добавляются. Но это работает, сразу после super.layoutSubviews(), ячейки загружаются. Это решение эквивалентно ожиданию события willDisplay последнего пути индекса. Это событие вызывается во время выполнения layoutSubviews табличного представления для каждой добавленной ячейки.

Другой способ - вызов по окончании макета приложения.

Как описано в документации, вы можете использовать опцию UIView.animate(withDuration:completion):

tableView.reloadData()
UIView.animate(withDuration: 0) {
    // layout done
}

Это решение работает, но экран обновляется один раз между временем создания макета и временем вызова блока. Это эквивалентно решению DispatchMain.async но указано.

• В качестве альтернативы, я бы предпочел форсировать макет табличного представления.

Существует специальный метод, позволяющий любому представлению немедленно вычислять свои кадры layoutIfNeeded:

tableView.reloadData()
table.layoutIfNeeded()
// layout done

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


Я думаю, что нет идеального решения. Подклассы классов могут привести к трюкам. Передача макета начинается сверху и заканчивается снизу, поэтому получить уведомление о завершении макета не так просто. И layoutIfNeeded() может создать проблемы с производительностью и т.д. Но, зная эти варианты, вы должны быть в состоянии найти одну альтернативу, которая будет соответствовать вашим потребностям.

Ответ 18

Вы ищете общее количество элементов, которые будут отображаться в таблице или общее количество элементов, видимых в настоящее время? В любом случае. Я считаю, что метод 'viewDidLoad' выполняется после вызова всех методов datasource. Однако это будет работать только при первом загрузке данных (если вы используете одиночный диспетчер ViewController).

Ответ 19

Я копирую код Andrew и расширяю его, чтобы учесть случай, когда у вас всего одна строка в таблице. Он работает до сих пор для меня!

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// detect when all visible cells have been loaded and displayed
// NOTE: iOS7 workaround used - see: http://stackoverflow.com/questions/4163579/how-to-detect-the-end-of-loading-of-uitableview?lq=1
NSArray *visibleRows = [tableView indexPathsForVisibleRows];
NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
BOOL isPreviousCallForPreviousCell = self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
BOOL isFinishedLoadingTableView = isLastCell && ([tableView numberOfRowsInSection:0] == 1 || isPreviousCallForPreviousCell);

self.previousDisplayedIndexPath = indexPath;

if (isFinishedLoadingTableView) {
    [self hideSpinner];
}
}

ПРИМЕЧАНИЕ. Я просто использую 1 раздел из кода Andrew, поэтому имейте это в виду.

Ответ 20

Цель C

[self.tableView reloadData];
[self.tableView performBatchUpdates:^{}
                              completion:^(BOOL finished) {
                                  /// table-view finished reload
                              }];

Свифта

self.tableView?.reloadData()
self.tableView?.performBatchUpdates({ () -> Void in

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })

Ответ 21

В iOS7.0x решение немного отличается. Вот что я придумал.

    - (void)tableView:(UITableView *)tableView 
      willDisplayCell:(UITableViewCell *)cell 
    forRowAtIndexPath:(NSIndexPath *)indexPath
{
    BOOL isFinishedLoadingTableView = [self isFinishedLoadingTableView:tableView  
                                                             indexPath:indexPath];
    if (isFinishedLoadingTableView) {
        NSLog(@"end loading");
    }
}

- (BOOL)isFinishedLoadingTableView:(UITableView *)tableView 
                         indexPath:(NSIndexPath *)indexPath
{
    // The reason we cannot just look for the last row is because 
    // in iOS7.0x the last row is updated before
    // looping through all the visible rows in ascending order 
    // including the last row again. Strange but true.
    NSArray * visibleRows = [tableView indexPathsForVisibleRows];   // did verify sorted ascending via logging
    NSIndexPath *lastVisibleCellIndexPath = [visibleRows lastObject];
    // For tableviews with multiple sections this will be more complicated.
    BOOL isPreviousCallForPreviousCell = 
             self.previousDisplayedIndexPath.row + 1 == lastVisibleCellIndexPath.row;
    BOOL isLastCell = [indexPath isEqual:lastVisibleCellIndexPath];
    BOOL isFinishedLoadingTableView = isLastCell && isPreviousCallForPreviousCell;
    self.previousDisplayedIndexPath = indexPath;
    return isFinishedLoadingTableView;
}