web-dev-qa-db-ja.com

UITableViewがデータの要求を完了したときに通知を受け取りますか?

UITableViewがデータソースからのデータの要求を終了したことを確認する方法はありますか?

関連付けられたビューコントローラー(viewDidLoad)のviewWillAppear/viewDidAppear/UITableViewControllerメソッドはどれも役に立たないため、すべてが早すぎるため起動します。それらのどれも(完全に理解できるほど)データソースへのクエリが当面(たとえば、ビューがスクロールされるまで)終了することを保証しません。

私が見つけた回避策の1つは、reloadDataが返されたときに、テーブルビューisのクエリの完了が保証されているため、viewDidAppearreloadDataを呼び出すことです。とりあえず必要なだけのデータソース。

ただし、最初にロードされたときにデータソースに同じ情報を2回(自動的に1回、reloadData呼び出しのために1回)求められるため、これはかなり厄介に思えます。

私がこれをしたい理由は、UITableViewのスクロール位置を保持したいからです-もっとも近い行だけでなく、ピクセルレベルまで下ろしたいからです。

スクロール位置を復元するとき(scrollRectToVisible:animated:)、Table Viewに十分なデータが既に含まれているか、scrollRectToVisible:animated:メソッド呼び出しは何も行いません(viewDidLoadviewWillAppear、またはviewDidAppearのいずれかで独自に呼び出しを行った場合に発生します)。

108
kennethmac2000

この回答は、回答が書かれてからUITableViewの実装に変更が加えられたため、もう機能していないようです。このコメントを参照してください: ITableViewがデータの要求を完了したときに通知を受け取りますか?

私はこの問題を数日間遊んでいますが、UITableViewreloadDataをサブクラス化するのが最善の方法だと思います。

- (void)reloadData {

    NSLog(@"BEGIN reloadData");

    [super reloadData];

    NSLog(@"END reloadData");

}

reloadDataは、テーブルがデータのリロードを完了するまで終了しません。したがって、2番目のNSLogが起動されると、Table Viewは実際にデータの要求を完了します。

UITableViewの前後にデリゲートにメソッドを送信するために、reloadDataをサブクラス化しました。それは魅力のように機能します。

60
Eric MORAND

私のアプリには同じシナリオがありましたが、ここに記載されている他の回答はiOS7以降では機能しないため、あなたに答えを投稿すると思いました

最後に、これは私のために働いた唯一のものです。

[yourTableview reloadData];

dispatch_async(dispatch_get_main_queue(),^{
        NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
        //Basically maintain your logic to get the indexpath
        [yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
 });

Swift Update:

yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
    let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
    //Basically maintain your logic to get the indexpath
    yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)

})

これがどのように機能するか。

基本的に、リロードするとメインスレッドがビジーになるため、非同期スレッドをディスパッチすると、メインスレッドが終了するまでブロックが待機します。テーブルビューが完全にロードされると、メインスレッドが終了し、メソッドブロックをディスパッチします

iOS7およびiOS8でテスト済み、それは素晴らしい動作します;)

iOS9のアップデート:これはiOS9でも正常に機能します。 POCとしてgithubでサンプルプロジェクトを作成しました。 https://github.com/ipraba/TableReloadingNotifier

ここにテストのスクリーンショットを添付します。

テスト済みの環境:Xcode7のiOS9 iPhone6シミュレーター

enter image description here

51
iPrabu

編集:この答えは実際には解決策ではありません。リロードは非常に高速に行われる可能性があるため、最初は動作しているように見えますが、実際には、ブロックしません。より良い解決策を検索する必要があります。

@Eric MORANDの答えを拡張するには、完了ブロックを入れましょう。ブロックが好きではない人はいますか?

@interface DUTableView : UITableView

   - (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;

@end

そして...

#import "DUTableView.h"

@implementation DUTableView

- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
    [super reloadData];
    if(completionBlock) {
        completionBlock();
    }
}

@end

使用法:

[self.tableView reloadDataWithCompletion:^{
                                            //do your stuff here
                                        }];
25
bandejapaisa

reloadDataは、表示されているセルのデータを要求するだけです。テーブルの特定の部分が読み込まれたときに通知されるようにするには、tableView: willDisplayCell: 方法。

- (void) reloadDisplayData
{
    isLoading =  YES;
    NSLog(@"Reload display with last index %d", lastIndex);
    [_tableView reloadData];
    if(lastIndex <= 0){
    isLoading = YES;
    //Notify completed
}

- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(indexPath.row >= lastIndex){
    isLoading = NO;
    //Notify completed
}
11
Black Coder

それが私の解決策です。 100%動作し、多くのプロジェクトで使用されています。シンプルなUITableViewサブクラスです。

_@protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
@optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
@end

@interface MyTableView : UITableView {
    struct {
        unsigned int delegateWillReloadData:1;
        unsigned int delegateDidReloadData:1;
        unsigned int reloading:1;
    } _flags;
}
@end

@implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
    return (id<MyTableViewDelegate>)[super delegate];
}

- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
    [super setDelegate:delegate];
    _flags.delegateWillReloadData = [delegate respondsToSelector:@selector(tableViewWillReloadData:)];
    _flags.delegateDidReloadData = [delegate    respondsToSelector:@selector(tableViewDidReloadData:)];
}

- (void)reloadData {
    [super reloadData];
    if (_flags.reloading == NO) {
        _flags.reloading = YES;
        if (_flags.delegateWillReloadData) {
            [(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
        }
        [self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
    }
}

- (void)finishReload {
    _flags.reloading = NO;
    if (_flags.delegateDidReloadData) {
        [(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
    }
}

@end
_

Josh Brownのソリューション に似ていますが、1つ例外があります。 performSelectorメソッドで遅延は必要ありません。 reloadDataがどれだけ長くても。 _tableViewDidLoadData:_は、tableViewdataSourcecellForRowAtIndexPathの確認を完了すると常に起動します。

UITableViewをサブクラス化したくない場合でも、[performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f]を呼び出すだけで、テーブルの再読み込みが完了した直後にセレクターが呼び出されます。ただし、reloadDataの呼び出しごとにセレクターが1回だけ呼び出されるようにする必要があります。

_[self.tableView reloadData];
[self performSelector:@selector(finishReload) withObject:nil afterDelay:0.0f];
_

楽しい。 :)

10

これは、わずかに異なる質問に対する答えです。UITableViewcellForRowAtIndexPath()の呼び出しを終了した時期を知る必要がありました。 layoutSubviews()をサブクラス化し(@Eric MORANDに感謝)、デリゲートコールバックを追加しました。

SDTableView.h:

@protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
@required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
@end

@interface SDTableView : UITableView

@property(nonatomic,assign) id <SDTableViewDelegate> delegate;

@end;

SDTableView.m:

#import "SDTableView.h"

@implementation SDTableView

@dynamic delegate;

- (void) reloadData {
    [self.delegate willReloadData];

    [super reloadData];

    [self.delegate didReloadData];
}

- (void) layoutSubviews {
    [self.delegate willLayoutSubviews];

    [super layoutSubviews];

    [self.delegate didLayoutSubviews];
}

@end

使用法:

MyTableViewController.h:

#import "SDTableView.h"
@interface MyTableViewController : UITableViewController <SDTableViewDelegate>
@property (nonatomic) BOOL reloadInProgress;

MyTableViewController.m:

#import "MyTableViewController.h"
@implementation MyTableViewController
@synthesize reloadInProgress;

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    if ( ! reloadInProgress) {
        NSLog(@"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
        reloadInProgress = TRUE;
    }

    return 1;
}

- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}

- (void)didLayoutSubviews {
    if (reloadInProgress) {
        NSLog(@"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
        reloadInProgress = FALSE;
    }
}

注:これはUITableViewを指すデリゲートプロパティを既に持っているMyTableViewControllerのサブクラスであるため、必要はありません。別のものを追加します。 「@dynamicデリゲート」は、このプロパティを使用するようコンパイラーに指示します。 (これを説明するリンクがあります: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/

UITableViewMyTableViewControllerプロパティは、新しいSDTableViewクラスを使用するように変更する必要があります。これは、Interface Builder Identity Inspectorで行われます。 UITableView内のUITableViewControllerを選択し、その「カスタムクラス」をSDTableViewに設定します。

8
Symmetric

contentSizeTableViewの変更通知を受け取るのに似たものを見つけました。 contentSizeもデータの読み込みに応じて変化するため、ここでも同様に機能するはずです。

これを試して:

viewDidLoad書き込みで、

[self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];

そして、このメソッドをviewControllerに追加します。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"contentSize"]) {
        DLog(@"change = %@", change.description)

        NSValue *new = [change valueForKey:@"new"];
        NSValue *old = [change valueForKey:@"old"];

        if (new && old) {
            if (![old isEqualToValue:new]) {
                // do your stuff
            }
        }


    }
}

変更のチェックでわずかな変更が必要になる場合があります。これは私にとってはうまくいきました。

乾杯! :)

6
Abdullah Umer

ハックですが、考えられる解決策は次のとおりです。

[self.tableView reloadData];
[self performSelector:@selector(scrollTableView) withObject:nil afterDelay:0.3];

-scrollTableViewメソッドは、-scrollRectToVisible:animated:でテーブルビューをスクロールします。そして、もちろん、上記のコードの遅延を0.3からあなたに合っているように設定することもできます。うん、それはとんでもないハックですが、iPhone 5と4Sで動作します...

2
Josh Brown

セルの内容を更新したいようですが、セルの挿入や削除に伴う突然のジャンプはありません。

それを行う上でいくつかの記事があります。 これは1つです

スクロールビューのピクセル単位の設定には、scrollRectToVisible:animated:ではなくsetContentOffset:animated:を使用することをお勧めします。

1
Walt Sellers

似たようなものがあったと思います。インスタンス変数としてBOOLを追加して、オフセットが復元されたかどうかを確認し、-viewWillAppear:。復元されていない場合は、その方法で復元し、BOOLを設定して、オフセットを復元したことを示します。

それは一種のハックであり、おそらくもっと良くできるかもしれませんが、これは今のところうまくいきます。

1
Joost

次のロジックを試すことができます。

-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];

    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyIdentifier"];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
    }

    if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
        NSLog(@"Created Last Cell. IndexPath = %@", indexPath);
        //[self.activityIndicator hide];
        //Do the task for TableView Loading Finished
    }
    prevIndexPath = indexPath;

    return cell;
}



-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{

    BOOL bRetVal = NO;
    NSArray *visibleIndices = [tableView indexPathsForVisibleRows];

    if (!visibleIndices || ![visibleIndices count])
        bRetVal = YES;

    NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
    NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];

    if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
        //Ascending - scrolling up
        if ([indexPath isEqual:lastVisibleIP]) {
            bRetVal = YES;
            //NSLog(@"Last Loading Cell :: %@", indexPath);
        }
    } else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
        //Descending - scrolling down
        if ([indexPath isEqual:firstVisibleIP]) {
            bRetVal = YES;
            //NSLog(@"Last Loading Cell :: %@", indexPath);
        }
    }
    return bRetVal;
}

ReloadDataを呼び出す前に、prevIndexPathをnilに設定します。好む:

prevIndexPath = nil;
[mainTableView reloadData];

NSLogsでテストしましたが、このロジックは問題ないようです。必要に応じてカスタマイズ/改善できます。

1
Joe M

最後に、これでコードを機能させました-

_[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
_

世話をする必要があるものはほとんどありませんでした-

  1. - (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath」内で呼び出します
  2. 「scrollToRowAtIndexPath」メッセージがUITableViewの関連インスタンス(この場合は間違いなくMyTableview)に送信されることを確認してください。
  3. 私の場合、UIViewはUITableViewのインスタンスを含むビューです
  4. また、これはセルのロードごとに呼び出されます。したがって、「cellForRowAtIndexPath」内にロジックを配置して、「scrollToRowAtIndexPath」を複数回呼び出さないようにします。
0
smile.al.d.way

繰り返しスケジュールされたタイマーを実行し、tableHeaderViewの高さ(テーブルに行コンテンツがあることを意味する)のときにテーブルのcontentSizeが大きい場合にのみ無効にします。 C#(モノタッチ)のコードですが、アイデアが明確であることを願っています。

    public override void ReloadTableData()
    {
        base.ReloadTableData();

        // don't do anything if there is no data
        if (ItemsSource != null && ItemsSource.Length > 0)
        {
            _timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue, 
                new NSAction(() => 
                {
                    // make sure that table has header view and content size is big enought
                    if (TableView.TableHeaderView != null &&
                        TableView.ContentSize.Height > 
                            TableView.TableHeaderView.Frame.Height)
                    {
                        TableView.SetContentOffset(
                            new PointF(0, TableView.TableHeaderView.Frame.Height), false);
                        _timer.Invalidate();
                        _timer = null;
                    }
                }));
        }
    }
0

テーブルビューがコンテンツを表示する直前にUITableViewlayoutSubviewsが呼び出されていませんか?テーブルビューがデータの読み込みを完了すると呼び出されることに気付きました。おそらくその方向で調査する必要があります。

0
Eric MORAND

すべてのデータがロードされたら、このメソッドでTableViewのサイズを変更するか、コンテンツのサイズを設定できます。

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    tableView.frame =CGRectMake(tableView.frame.Origin.x, tableView.frame.Origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
0
Banker Mittal

Swiftで見つけた最高のソリューション

extension UITableView {
    func reloadData(completion: ()->()) {
        self.reloadData()
        dispatch_async(dispatch_get_main_queue()) {
            completion()
        }
    }
}
0
YannSteph

IOS 6以降、UITableviewデリゲートメソッドは以下を呼び出しました。

-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section

テーブルが正常にリロードされると実行されます。この方法では、必要に応じてカスタマイズを行うことができます。

0