Як виявити кінець завантаження UITableView


141

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

Чи все-таки на SDK слід знати, коли закінчиться завантаження uitableview? Я нічого не бачу ні в делегатах, ні в протоколах джерел даних.

Я не можу використовувати кількість джерел даних через завантаження лише видимих ​​комірок.


спробуйте комбінацію підрахунку джерел даних та 'indexPathsForVisibleRows'
Swapnil Luktuke

1
Повторно відкрито на основі додаткової інформації у прапорі: "Це не дублікат. Він запитує про завантаження видимих ​​комірок, а не про завершення запиту даних. Дивіться оновлення до прийнятої відповіді"
Кев

Це рішення добре працює для мене. Ви перевіряєте його stackoverflow.com/questions/1483581 / ...
Халед Annajar

Відповіді:


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:.


Набагато краще знати, коли всі клітини завантажують видимі.
Ерік

14
Однак це буде викликано, коли користувач прокручує, щоб переглянути більше комірок.
хтафоя

Проблема в тому, що він не враховує подання нижнього колонтитула. Це може бути проблемою для більшості, але якщо це так, ви можете застосувати ту саму ідею, але в viewForFooterInSectionметоді.
Кайл Клегг

1
@KyleClegg patric.schenke вказав одну з причин у коментарі до вашої відповіді. Крім того, що робити, якщо колонтитул не видно в кінці завантаження комірки?
folex

27
tableView: didEndDisplayingCell: насправді викликається під час видалення комірки з виду, а не тоді, коли її візуалізація у представленні завершена, тому вона не працюватиме. Не гарне ім'я методу. Треба прочитати документи.
Ендрю Рафаель

34

Версія 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 рядків, але все-таки цю функцію отримували. Те, що я хочу, - це завантажити більше даних, коли користувач прокручується вниз до останнього ряду
Muhammad Nayab

Привіт, Мухаммед Наяб, можливо, ви можете замінити "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];
    }
}

як виявити останній рядок? ось проблема для мене .. Чи можете ви пояснити, як ви отримуєте це останнє в тому, якщо умова.
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, а не просто видимих ​​комірок. Залежно від ваших потреб вам можуть потрібні лише видимі клітини, і в цьому випадку відповідь Фолекса - це хороший шлях.


Повертаючись nilіз tableView:viewForFooterInSection:заплутаного мого макета в iOS 7 за допомогою автоматичного макета.
ma11hew28

Цікаво. Що робити, якщо встановити його на рамку висотою та шириною 0? return CGRectMake(0,0,0,0);
Кайл Клегг

Я спробував це і навіть встановив self.tableView.sectionFooterHeight = 0. У будь-якому випадку, здається, вставити подання нижнього колонтитулу з висотою близько 10. Б'юсь об заклад, що я міг це виправити, додавши обмеження на 0-висоту до поданого повернення. У будь-якому випадку, я добре, бо насправді хотів зрозуміти, як запустити UITableView на останній комірці, але побачив це першим.
ma11hew28

1
@MattDiPasquale, якщо ви реалізуєте viewForFooterInSection, вам також доведеться реалізувати висотуForFooterInSection. Він повинен повернути 0 для секцій з нульовим колонтитулом. Це також є в офіційних документах.
patric.schenke

viewForFooterInSection НЕ додзвонилися , якщо ви встановите heightForFooterInSection до 0
Одед Harth

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

Можлива краща конструкція - додати видимі комірки до набору, тоді, коли вам потрібно перевірити, чи завантажена таблиця, ви можете замість цього зробити цикл навколо цього набору, наприклад

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

Набагато краще рішення, ніж інші. Для цього не потрібно перезавантажити tableView. Це дуже просто, і viewDidLoadRows: не викликається кожен раз, коли завантажується остання комірка.
Метт Хадсон

це найкраще рішення поки що, оскільки інші рішення не враховують декілька розділів
Justicepenny

Вибачте за примхливий коментар, але як саме ця "магія"? Виклик делегата до іншого методу. Я не розумію.
Лукас Петро

@LukasPetr подивіться на скасування та afterDelay. Вони дозволяють скасувати виклик методу для кожного розділу, окрім останнього, який завершується завантаженням таблиці.
злочин

@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т.д. в 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()може створити проблеми з продуктивністю тощо. Але знаючи ці варіанти, ви повинні мати можливість продумати одну альтернативу, яка відповідає вашим потребам.


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

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")
        }
}

0

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

Відповідно до наступної документації

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

Виправлення проблем із тимчасовим використанням dispatch_async - погана ідея. Я пропоную нам вирішити це, додавши FLAG або щось подібне.


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. Btw. встановлення footeView дозволяє видаляти "невикористані" роздільники


-1

Шукаєте загальну кількість елементів, які відображатимуться в таблиці, або загальну кількість предметів, які в даний час видно? У будь-якому випадку .. Я вважаю, що метод 'viewDidLoad' виконується після виклику всіх методів джерела даних. Однак це буде працювати лише при першому завантаженні даних (якщо ви використовуєте один алокатор ViewController).


-1

Я копіюю коду Ендрю та розширюю його для обліку випадку, коли у вас просто 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 розділ з коду Ендрю, тому майте це на увазі ..


Ласкаво просимо до 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
                              }];

Швидкий

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

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

1
Цей метод доступний лише UICollectionViewнаскільки я розумію.
Awesome-o

Так, ти маєш рацію. StartUpdates UITableView дорівнює UICollectionView'sBatchUpdates: завершення. stackoverflow.com/a/25128583/988169
pkc456

Тож дайте належну та достовірну відповідь на питання.
Г.Абхісек
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.