Edited at

reloadData処理後に処理を行いたい


要件

UITableViewUICollectionViewの再ロードにおいて、

再ロード処理が終了した後に、処理を行いたい。


結論(2018/02/18修正)

UIViewのアニメーション処理を利用する解法4で、スッキリ解決しそうです。


UITableViewController,UICollectionViewControllerの場合


NSBlockOperationの利用

@implementation TableViewController {

NSBlockOperation *completionLayoutSubViews; // 終了処理ブロック
}

// レイアウト後処理
-(void)viewDidLayoutSubviews
{
// 処理ブロックがあれば、実行
if (completionLayoutSubViews) {
[completionLayoutSubViews start];
// 実行後はクリア
completionLayoutSubViews = nil;
}
}

-(void)test
{
[self.tableView reloadData];

// 終了処理ブロックに、再ロード終了後処理を格納
completionLayoutSubViews = [NSBlockOperation blockOperationWithBlock:^{
/* やりたい処理 */
}];
}


viewDidLayoutSubviewsreloadDataの描画イベントをキャッチ出来る為、可能な方法です。

UITableView,UICollectionViewでは、この方法は使えません。


UITableViewUICollectionViewの場合

こちらでは、今のところ良い手が思いつきません。

コメントのfuzzballさんの手法を参考にしてみてください。

以下、細かな解説です。


解説

再ロード処理のreloadDataには、

困ったことにcomplation:(終了後処理)などの指定がなく、

UITableView,UICollectionViewにも、終了時イベントなどはありません。

当然、'reloadData'で発生する描画処理は呼出即実行ではないので、

以下の記載でも、「やりたい処理」は先に処理されてしまいます。


直後に記載

-(void)test

{
[self.tableView reloadData];

/* やりたい処理 */
}



解法1

UITableViewControllerUICollectionViewControllerならば、

viewDidLayoutSubviewsに記載するのも1つの手です。


viewDidLayoutSubviewsに記載

-(void)viewDidLayoutSubviews

{
if (/*reloadData直後とわかるフラグ*/) {
/* やりたい処理 */
}
}

ただしviewDidLayoutSubviewsは、全ての再描画処理で呼ばれるので、再ロード処理以外でも呼び出されます。(スクロールなどでも呼ばれる)

「やりたい処理」が再ロード処理後一度だけ実行されるよう、なんらかのフラグが必須です。

フラグ管理が必要なのは面倒な話です。

そもそも、UITableViewUICollectionViewだと使えない手段です。


解法2

iOSには下記の鉄則があります。


描画処理は、全てメインスレッドで処理しなければならない。


ふと思いました。

reloadDataって描画処理ですよね?

それならreloadDataって、再ロード処理をメインスレッドに予約しているのでは??

それならば、


直後にメインキューで記載

-(void)test

{
[self.tableView reloadData];

[[NSOperationQueue mainQueue] addOperationWithBlock:^{
/* やりたい処理 */
}];
}


reloadDataの後に、

「やりたい処理」の方も、メインスレッドに予約すれば、

必ず、再ロード処理→「やりたい処理」の順序で、実行されるのでは?

という考え方です。

(2017/04/04追記)

ダメでした。

サブスレッドでセル内容を設定するcollectionView:cellForItemAtIndexPath:が動いているタイミングで、メインスレッドで「やりたい処理」が動きます。

reloadDataで予約されるサブスレッド処理内で、描画処理がトリガーされるようですね。

ただし、セル数、サイズなどレイアウトに関する情報は揃っているタイミングなので、

「やりたい処理」によっては(スクロール位置の変更など)、解法2でも問題ありません。


解法3(2017/04/04追記)

解法1を少し改良、フラグ管理の手間を削減する案を思いつきました。


NSBlockOperationの利用

@implementation TableViewController {

NSBlockOperation *completionLayoutSubViews; // 終了処理ブロック
}

// レイアウト後処理
-(void)viewDidLayoutSubviews
{
// 処理ブロックがあれば、実行
if (completionLayoutSubViews) {
[completionLayoutSubViews start];
// 実行後はクリア
completionLayoutSubViews = nil;
}
}

-(void)test
{
[self.tableView reloadData];

// 終了処理ブロックに、再ロード終了後処理を格納
completionLayoutSubViews = [NSBlockOperation blockOperationWithBlock:^{
/* やりたい処理 */
}];
}


フラグ管理の手間も無く、確実にレイアウト処理後に実行されます。スマートでなかなか良い手です。

ただし、UITableViewControllerUICollectionViewControllerの場合のみというのは変わりません。


解法4(2018/02/18追記)

wzhangさんに提案いただきました。

animateWithDurationCompletionを利用する方法で、最有力案だと思います。


animateWithDurationの利用(Objective-C)

-(void)test

{
[UIView
animateWithDuration: 0.0
animations:^{
// リロード
[self.tableView reloadData];

} completion:^(BOOL finished) {
if (finished) { // 一応finished確認はしておく
/* やりたい処理 */
}
}];
}



animateの利用(swift)

-func test()

{

UIView.animate(
withDuration: 0.0,
animations:{
// リロード
self.tableView.reloadData()
}, completion:{ finished in
if (finished) { // 一応finished確認はしておく
/* やりたい処理 */
}
});
}


reloadDataのラッパーにしてしまうのも良いですね。

解法3に比べ、インスタンス変数やviewDidLayoutSubviewsの記載が不要で、reloadDataの呼び元メソッドで完結し、

なんといっても、UITableViewControllerUICollectionViewControllerのみの制約無しが大きいです。


最後に

解法4でFAではないでしょうか

animateWithDurationが何をアニメーションと判断しているのかの点は気になりますが、

UIViewのドキュメントを見る限り、少なくともUIViewのFrameはアニメーションに含まれるので問題はないかと思います。

https://developer.apple.com/documentation/uikit/uiview