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

141

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

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

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

emenegro
источник
попробуйте комбинацию количества источников данных и indexPathsForVisibleRows
Swapnil Luktuke
1
Открыт повторно на основе дополнительной информации во флаге: «Это не дубликат. Он спрашивает о загрузке видимых ячеек, а не о завершении запроса данных. См. Обновление принятого ответа»
Кев
Это решение мне подходит. Вы проверяете это stackoverflow.com/questions/1483581/…
Халед Аннаджар,

Ответы:

224

Улучшить ответ @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];
    }
}

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

folex
источник
Намного лучше знать, когда загружаются все видимые ячейки.
Эрик
14
Однако это будет вызываться всякий раз, когда пользователь прокручивает для просмотра дополнительных ячеек.
htafoya 07
Проблема в том, что он не учитывает просмотр нижнего колонтитула. Возможно, для большинства это не проблема, но если это так, вы можете применить ту же идею, но в viewForFooterInSectionметоде.
Кайл Клегг,
1
@KyleClegg patric.schenke упомянул одну из причин в комментарии к вашему ответу. Кроме того, что, если нижний колонтитул не отображается в конце загрузки ячейки?
folex 01
28
tableView: didEndDisplayingCell: фактически вызывается при удалении ячейки из представления, а не при завершении ее рендеринга в представлении, поэтому это не сработает. Плохое название метода. Надо прочитать документы.
Эндрю Рафаэль
36

Swift 3, 4 и 5 версии:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if let lastVisibleIndexPath = tableView.indexPathsForVisibleRows?.last {
        if indexPath == lastVisibleIndexPath {
            // do here...
        }
    }
}
Даниэле Сеглия
источник
1
этот метод вызывается при первой загрузке данных. В моем случае видимых ячеек всего 4. Я загрузил 8 строк, но эта функция все равно вызывалась. Я хочу загружать больше данных, когда пользователь прокручивает до последней строки
Мухаммад Наяб
Привет, Мухаммад Наяб, может быть, вы могли бы заменить «if indexPath == lastVisibleIndexPath» на «if indexPath.row == yourDataSource.count»
Даниэле Сеглия
1
@DanieleCeglia Спасибо, что отметили это. если indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count - 1, это сработает для меня. Ура !!!
Йогеш Патель,
28

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

-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if([indexPath row] == lastRow){
        //end of loading
        //for example [activityIndicator stopAnimating];
    }
}
RichX
источник
как определить последнюю строку? вот в чем проблема для меня ... не могли бы вы объяснить, как вы попадаете на последнюю строчку в это условие if?
Sameera Chathuranga
6
Последняя строка - это [tableView numberOfRowsInSection: 0] - 1. Вы должны заменить 0на необходимое значение. Но проблема не в этом. Проблема в том, что UITableView загружается только видимым. Однако ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).rowрешает проблему.
folex
1
Если мы ищем абсолютное завершение, не лучше ли использовать - (void) tableView: (UITableView *) tableView didEndDisplayingCell: (UITableViewCell *) ячейку forRowAtIndexPath: (NSIndexPath *) indexPath?
morcutt
10

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

Кайл Клегг
источник
Возвращаюсь nilиз tableView:viewForFooterInSection:испорченного макета в iOS 7 с помощью Auto Layout.
ma11hew28
Интересно. Что, если вы установите его на фрейм с высотой и шириной 0? return CGRectMake(0,0,0,0);
Кайл Клегг,
Я пробовал это и даже ставил self.tableView.sectionFooterHeight = 0. В любом случае кажется, что вставляется представление нижнего колонтитула раздела высотой около 10. Бьюсь об заклад, я мог бы исправить это, добавив ограничение высоты 0 к возвращаемому представлению. В любом случае, у меня все хорошо, потому что я действительно хотел выяснить, как запустить UITableView в последней ячейке, но увидел это первым.
ma11hew28
1
@MattDiPasquale, если вы реализуете viewForFooterInSection, вам также необходимо реализовать heightForFooterInSection. Он должен возвращать 0 для разделов с нулевым нижним колонтитулом. Это также есть в официальных документах.
patric.schenke
viewForFooterInSection не вызывается, если вы установите для heightForFooterInSection значение 0
Одед Харт,
10

Решение 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()
    }
}
Фатихильдижан
источник
9

Использование частного API:

@objc func tableViewDidFinishReload(_ tableView: UITableView) {
    print(#function)
    cellsAreLoaded = true
}

Использование публичного API:

- (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.myDataSource numberOfRowsInSection:section];
}

// called after the rows in the last section is loaded
-(void)tableViewDidLoadRows:(UITableView*)tableView{
    self.cellsAreLoaded = YES;
}

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

var visibleCells = Set<UITableViewCell>()

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.insert(cell)
}

override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    visibleCells.remove(cell)
}

// example property you want to show on a cell that you only want to update the cell after the table is loaded. cellForRow also calls configure too for the initial state.
var count = 5 {
    didSet {
        for cell in visibleCells {
            configureCell(cell)
        }
    }
}
Malhal
источник
Намного лучшее решение, чем другие. Это не требует перезагрузки tableView. Это очень просто, и viewDidLoadRows: не вызывается каждый раз, когда загружается последняя ячейка.
Мэтт Хадсон
на данный момент это лучшее решение, поскольку другие решения не принимают во внимание несколько разделов
судьяпенни
Прошу прощения за язвительный комментарий, но как именно это «волшебство»? Вызов другого метода из делегата. Я не понимаю.
Лукас Петр
@LukasPetr взгляните на отмену и задержку после. Они позволяют отменить вызов метода для каждого раздела, кроме последнего, когда загрузка таблицы полностью завершена.
malhal
@malhal о, хорошо. Это немного умнее, чем я думал на первый взгляд.
Лукас Петр
8

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

Трэвис М.
источник
6

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

Обновление: чтобы заставить его работать, мне нужно поместить эти звонки в -tableView:cellForRowAtIndexPath:

[tableView beginUpdates];
[tableView endUpdates];
Рене Рец
источник
Как бы то ни было, я уже несколько месяцев использую этот очень простой метод во всех своих проектах, и он отлично работает.
Эрик МОРАНД
3

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

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

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

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

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

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

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

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

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

Во время этапа макета приложение вычисляет все кадры иерархии представления. Чтобы принять участие, вы можете переопределить выделенные методы layoutSubviewsи updateLayoutConstraintsт. Д. В a 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()может создать проблемы с производительностью и т. Д. Но зная эти варианты, вы должны уметь думать об одной альтернативе, которая будет соответствовать вашим потребностям.

GaétanZ
источник
1

Вот как это делается в 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
    }

    // ....
}
Ондрей Квасновский
источник
1

вот как я это делаю в 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()
    }
}
AbdulMomen عبدالمؤمن
источник
1

Вот что бы я сделал.

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

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

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

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

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  2. В реализации метода базового класса вызовите 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];
            }
        }];
    }
    
  3. Подтвердите протокол завершения перезагрузки во всех контроллерах представления, где вам нужен обратный вызов.

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

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

Сухас Айтал
источник
0

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

Но это не удастся, если в tableView одновременно отображается более одного раздела .

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

 }
}
умаканта
источник
0

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

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row+1 == postArray.count {
            println("came to last row")
        }
}
Codetard
источник
0

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

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

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

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

arango_86
источник
0

Если у вас несколько разделов, вот как получить последнюю строку в последнем разделе (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

        }
    }
}
Дэниел Маклин
источник
0

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

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

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

Ковбой
источник
-1

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

DerekH
источник
-1

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

- (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 раздел из кода Эндрю, так что имейте это в виду ..

jpage4500
источник
Добро пожаловать в SO @ jpage4500. Я отредактировал ваш ответ, чтобы убрать все, что связано с низкой репутацией и повышенным количеством вопросов. Расширение ответа другого пользователя само по себе является совершенно правильным ответом, так что вы можете уменьшить беспорядок, оставив этот материал. Хорошо, что ты отдал должное Эндрю, так что это осталось.
skrrgwasme
-3

В 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;
}
Эндрю Рафаэль
источник
-3

Цель C

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

Swift

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

                            }, completion: { (Bool finished) -> Void in
                                /// table-view finished reload
                            })
pkc456
источник
1
Этот метод доступен только на UICollectionViewнасколько я понимаю.
Awesome-o
Да ты прав. Параметр beginUpdates UITableView совпадает с параметром performBatchUpdates: Завершение UICollectionView. stackoverflow.com/a/25128583/988169
pkc456
Так что дайте правильный и обоснованный ответ на вопрос.
G.Abhisek