web-dev-qa-db-ja.com

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

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

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

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

134
emenegro

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

PDATE:さて、@ htafoyaのコメントは正しい。このコードでソースからのすべてのデータの読み込みの終了を検出する場合、検出されませんが、それは元の質問ではありません。このコードは、表示されるはずのすべてのセルがいつ表示されるかを検出するためのものです。ここで使用されるwillDisplayCell:はUIをよりスムーズにするために使用します(通常、単一セルはwillDisplay:呼び出し後に高速に表示されます)。 tableView:didEndDisplayingCell:を使用して試すこともできます。

217
folex

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

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

Swift 3および4バージョン:

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

私にとってはうまくいくと思われる別のオプションがあります。 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's answer が適切なルートです。

10
Kyle Clegg

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

この魔法を試してください:

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

テーブルの読み込みの動作は、テーブルが行数を認識し、デフォルトで行を選択するまで、選択行を呼び出せないことを意味します。ビューコントローラーではないテーブルビューデリゲートがあったため、テーブルセル選択をビューに単純に表示したり、デリゲートメソッドを読み込んだりすることはできませんでした。

9
malhal

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変数が必要でした。これを含めないと、テーブルをスクロールするたびにコードが再度呼び出されます。

8
Travis M.

私が知っている最良のアプローチは、エリックの答えです: ITableViewがデータの要求を終えたら通知を受け取りますか?

更新:動作させるには、これらの呼び出しを-tableView:cellForRowAtIndexPath:に入れる必要があります

[tableView beginUpdates];
[tableView endUpdates];
6
René Retz

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. 基本クラス(rootVC BaseVcなど)で、

    A.「DidFinishReloading」コールバックを送信するプロトコルを作成します。

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

    B.テーブルビューをリロードする汎用メソッドを作成します。

    -(void)reloadTableView:(UITableView *)tableView withOwner:(UIViewController *)aViewController;
    
  2. 基本クラスのメソッド実装では、reloadDataに続いて、デリゲートメソッドを遅延して呼び出します。

    -(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=

1
Suhas Aithal

Swiftでは、次のようなことができます。 tableViewの最後に到達するたびに、次の条件が満たされます。

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

かなり偶然、私はこのソリューションにぶつかりました:

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

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

0
Kowboj

複数のセクションがある場合、最後のセクションの最後の行を取得する方法は次のとおりです(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
Daniel McLean

私はこれが答えられていることを知っています、私はちょうど勧告を追加しています。

次のドキュメントに従って

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

Dispatch_asyncでタイミングの問題を修正するのは悪い考えです。 FLAGなどを追加することでこれを処理することをお勧めします。

0
arango_86

@folexの答えは正しいです。

ただし、tableViewに複数のセクションが一度に表示される場合は、failになります。

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

 }
}
0
umakanta

テーブルビューがそのコンテンツの読み込みをいつ完了するかを知るには、まず、ビューが画面にどのように表示されるかについての基本的な理解が必要です。

アプリのライフサイクルには、4つの重要な瞬間があります。

  1. アプリはイベント(タッチ、タイマー、ディスパッチされたブロックなど)を受け取ります
  2. アプリはイベントを処理します(制約の変更、アニメーションの開始、背景の変更など)
  3. アプリは新しいビュー階層を計算します
  4. アプリはビュー階層をレンダリングして表示します

2回と3回は完全に分離されています。 なぜ?パフォーマンス上の理由から、変更が行われるたびに瞬間3のすべての計算を実行する必要はありません。

だから、私はあなたがこのようなケースに直面していると思う:

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

ここで何が問題なのですか?

他のビューと同様に、Table Viewはコンテンツを遅延的にリロードします。実際、reloadDataを複数回呼び出しても、パフォーマンスの問題は発生しません。テーブルビューは、デリゲートの実装に基づいてコンテンツサイズのみを再計算し、セルをロードする瞬間3を待機します。この時間は、レイアウトパスと呼ばれます。

さて、レイアウトパスに入る方法は?

レイアウトパス中に、アプリはビュー階層のすべてのフレームを計算します。関与するには、layoutSubviewsの専用メソッドupdateLayoutConstraintsUIViewなど、およびView Controllerサブクラスの同等のメソッドをオーバーライドできます。

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

さて、しかし今、Tablesビューがコンテンツのリロードを完了したことを知る方法は?

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

最も簡単な方法は、テーブルビューのレイアウトに入ることです:

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

このメソッドは、Table Viewのライフサイクルで何度も呼び出されることに注意してください。テーブルビューのスクロールとデキュー動作のため、セルは頻繁に変更、削除、追加されます。しかし、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()はパフォーマンスの問題などを引き起こす可能性がありますが、これらのオプションを知っていれば、ニーズに合った代替案を考えられるはずです。

0
GaétanZ