UITableViewの読み込みの終了を検出する方法


141

ロードが終了したときにテーブルのオフセットを変更したいのですが、そのオフセットはテーブルにロードされたセルの数に依存します。

とにかくSDKで、uitableviewの読み込みがいつ終了したかを知ることはありますか?デリゲートにもデータソースプロトコルにも何も表示されません。

表示されているセルのみを読み込んでいるため、データソースの数を使用できません。


データソース数と「indexPathsForVisibleRows」の組み合わせを試してください
Swapnil Luktuke

1
フラグの詳細情報に基づいて再度開いた:「重複していない。データの要求の終了についてではなく、表示されているセルの読み込みについて尋ねる。承認された回答の更新を参照」
Kev

この解決策は私にとってはうまくいきます。あなたはそれをチェック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:ここでは、UIをよりスムーズにするために使用します(通常、単一セルはwillDisplay:呼び出し後に高速に表示されます)。で試すこともできtableView:didEndDisplayingCell:ます。


表示されているすべてのセルがいつ読み込まれるかを知るには、はるかに良い方法です。
エリック

14
ただし、これは、ユーザーがスクロールしてさらに多くのセルを表示するたびに呼び出されます。
htafoya 2013年

この問題は、フッタービューを考慮していないことです。ほとんどの場合、問題になることはないかもしれませんが、問題がある場合は、同じアイデアをviewForFooterInSectionメソッドに適用できます。
Kyle Clegg 2013年

1
@KyleClegg patric.schenkeが理由の1つをコメントへのコメントで述べました。また、セルの読み込みの最後にフッターが表示されない場合はどうなりますか?
フォレックス、2014年

27
tableView:didEndDisplayingCell:は、ビューからのセルの削除時に実際に呼び出されます。ビューでのレンダリングが完了して機能しない場合は呼び出されません。良いメソッド名ではありません。お奨めは、ドキュメントを読んでください。
Andrew Raphael

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 2018

こんにちは、ムハンマドナヤブ、「if indexPath == lastVisibleIndexPath」を「if indexPath.row == yourDataSource.count」に置き換えることができます
Daniele Ceglia

1
@DanieleCegliaこれを指摘してくれてありがとう。if indexPath == lastVisibleIndexPath && indexPath.row == yourDataSource.count-1これは私にとってはうまくいくでしょう。乾杯!!!
Yogesh Patel

28

私は常にこの非常にシンプルなソリューションを使用しています:

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

最後の行を検出する方法は?それが私にとっての問題です.. ifラストローをif条件に入れる方法を説明できますか?
Sameera Chathuranga

6
最後の行は[tableView numberOfRowsInSection: 0] - 1です。0必要な値に置き換える必要があります。しかし、それは問題ではありません。問題は、UITableViewが表示のみをロードすることです。しかし((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row、問題を解決します。
フォレックス2012

1
絶対的な補完を探しているなら、使用するのが最善ではないでしょうか-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath?
morcutt 2013

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は、表示されているセルだけでなく、全体の最終読み込みを検索する場合に最適です。必要に応じて、表示されているセルのみが必要な場合があります。その場合、フォレックスの回答が適切なルートです。


自動レイアウトを使用して、iOS 7でレイアウトniltableView:viewForFooterInSection:めちゃくちゃにしてから戻る
ma11hew28 2013年

面白い。高さと幅が0のフレームに設定するとどうなりますか?return CGRectMake(0,0,0,0);
カイルクレッグ2013年

私はそれを試し、設定さえしましたself.tableView.sectionFooterHeight = 0。どちらの方法でも、高さが約10のセクションフッタービューが挿入されているようです。返すビューに高さ0の制約を追加することで、これを修正できると思います。とにかく、最後のセルでUITableViewを開始する方法を実際に理解したかったが、最初にこれを確認したので、私は元気です
ma11hew28 2013年

1
@MattDiPasqualeは、viewForFooterInSectionを実装する場合、heightForFooterInSectionも実装する必要があります。フッターがnilのセクションでは0を返す必要があります。これも公式ドキュメントに含まれています。
patric.schenke 2013

heightForFooterInSectionを0に設定した場合、viewForFooterInSectionは呼び出されません
Oded 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;
}

考えられるより良い設計は、表示されているセルをセットに追加することです。その後、テーブルがロードされているかどうかを確認する必要がある場合は、代わりにこのセットの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)
        }
    }
}

他のものよりはるかに良い解決策。tableViewのリロードは必要ありません。これは非常にシンプルで、viewDidLoadRows:最後のセルが読み込まれるたびに呼び出されるわけではありません。
Matt Hudson

他のソリューションは複数のセクションを考慮に入れていないため、これはこれまでのところ最高のソリューションです
justicepenny

ぎこちないコメントで申し訳ありませんが、この「魔法」はどの程度正確ですか?デリゲートから別のメソッドを呼び出す。わかりません。
Lukas Petr 2017年

@LukasPetrはキャンセルとafterDelayを見てください。これにより、最後のセクションを除くすべてのセクションでメソッド呼び出しをキャンセルできます。これは、テーブルの読み込みが完全に完了したときです。
Malhal 2017年

@malhalおお、大丈夫。一見思ったより少し賢いです。
Lukas Petr 2017年

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

私が知っている最善のアプローチは、Ericの回答です。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待機します。今回はレイアウトパスと呼ばれます。

さて、どのようにレイアウトパスに入るのですか?

レイアウトパス中に、アプリはビュー階層のすべてのフレームを計算します。関与取得するには、専用のメソッドをオーバーライドすることができlayoutSubviewsupdateLayoutConstraintsなどでA UIViewとビューコントローラのサブクラスで同等の方法。

これがまさにテーブルビューの機能です。これはオーバーライドlayoutSubviewsし、デリゲートの実装に基づいてセルを追加または削除します。cellForRow新しいセルを追加してレイアウトする直前、直後に呼び出しますwillDisplayreloadData階層にテーブルビューを呼び出すか追加した場合、テーブルビューは、この重要な瞬間にフレームを埋めるために必要な数のセルを追加します。

よし、でも今は、テーブルビューがコンテンツの再読み込みをいつ終了したかを知る方法は?

これで、この質問を言い換えることができます。テーブルビューがサブビューのレイアウトを終了したときを知る方法は?

最も簡単な方法は、テーブルビューのレイアウトを表示することです。

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

このメソッドは、テーブルビューのライフサイクルで何度も呼び出されることに注意してください。テーブルビューのスクロールとデキューの動作により、セルは頻繁に変更、削除、追加されます。しかし、それはsuper.layoutSubviews()、セルがロードされた直後に機能します。このソリューションはwillDisplay、最後のインデックスパスのイベントを待機することと同じです。このイベントは、layoutSubviews追加された各セルのテーブルビューの実行中に呼び出されます。

もう1つの方法は、アプリがレイアウトパスを終了したときに呼び出されるようにすることです。

ドキュメントで説明されているように、次のオプションを使用できますUIView.animate(withDuration:completion)

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

このソリューションは機能しますが、レイアウトが完了してからブロックが呼び出されるまでの間に画面が1回更新されます。これは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. コールバックが必要なすべてのView Controllerのリロード完了プロトコルを確認します。

    -(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の最後に到達するたびにtrueになります

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


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

たとえばviewDidLoadでcontentSizeを取得する前に、footerViewを設定する必要があります。ところで footeViewを設定すると、「未使用」のセパレータを削除できます


-1

テーブルに表示されるアイテムの総数または現在表示されているアイテムの総数をお探しですか?どちらの方法でも、すべてのデータソースメソッドが呼び出された後、「viewDidLoad」メソッドが実行されると思います。ただし、これはデータの最初のロードでのみ機能します(単一のalloc ViewControllerを使用している場合)。


-1

Andrewのコードをコピーして、テーブルに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];
}
}

注:私はAndrewのコードの1つのセクションを使用しているだけなので、覚えておいてください。


SO @ jpage4500へようこそ。私はあなたの回答を編集して、低い担当者ポイントと質問のバンプに関するものを削除しました。別のユーザーの回答を拡張することは、それ自体で完全に有効な回答です。そのため、その項目を省略することで、混乱を減らすことができます。アンドリューにクレジットを与えてよかったので、それはそのままでした。
skrrgwasme 2014

-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

はい、そうです。UITableViewのbeginUpdatesは、UICollectionViewのperformBatchUpdates:completionと同じです。stackoverflow.com/a/25128583/988169
pkc456 2015年

質問に適切で有効な答えを与えてください。
G.Abhisek 2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.