#要件
UITableView
やUICollectionView
の再ロードにおいて、
再ロード処理が終了した後に、処理を行いたい。
#結論(2018/02/18修正)
UIView
のアニメーション処理を利用する解法4で、スッキリ解決しそうです。
##UITableViewController
,UICollectionViewController
の場合
@implementation TableViewController {
NSBlockOperation *completionLayoutSubViews; // 終了処理ブロック
}
// レイアウト後処理
-(void)viewDidLayoutSubviews
{
// 処理ブロックがあれば、実行
if (completionLayoutSubViews) {
[completionLayoutSubViews start];
// 実行後はクリア
completionLayoutSubViews = nil;
}
}
-(void)test
{
[self.tableView reloadData];
// 終了処理ブロックに、再ロード終了後処理を格納
completionLayoutSubViews = [NSBlockOperation blockOperationWithBlock:^{
/* やりたい処理 */
}];
}
viewDidLayoutSubviews
でreloadData
の描画イベントをキャッチ出来る為、可能な方法です。
UITableView
,UICollectionView
では、この方法は使えません。
##UITableView
やUICollectionView
の場合
こちらでは、今のところ良い手が思いつきません。
コメントのfuzzballさんの手法を参考にしてみてください。
以下、細かな解説です。
#解説
再ロード処理のreloadData
には、
困ったことにcomplation:
(終了後処理)などの指定がなく、
UITableView
,UICollectionView
にも、終了時イベントなどはありません。
当然、'reloadData'で発生する描画処理は呼出即実行ではないので、
以下の記載でも、「やりたい処理」は先に処理されてしまいます。
-(void)test
{
[self.tableView reloadData];
/* やりたい処理 */
}
##解法1
UITableViewController
やUICollectionViewController
ならば、
viewDidLayoutSubviews
に記載するのも1つの手です。
-(void)viewDidLayoutSubviews
{
if (/*reloadData直後とわかるフラグ*/) {
/* やりたい処理 */
}
}
ただしviewDidLayoutSubviews
は、全ての再描画処理で呼ばれるので、再ロード処理以外でも呼び出されます。(スクロールなどでも呼ばれる)
「やりたい処理」が再ロード処理後に一度だけ実行されるよう、なんらかのフラグが必須です。
フラグ管理が必要なのは面倒な話です。
そもそも、UITableView
やUICollectionView
だと使えない手段です。
##解法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を少し改良、フラグ管理の手間を削減する案を思いつきました。
@implementation TableViewController {
NSBlockOperation *completionLayoutSubViews; // 終了処理ブロック
}
// レイアウト後処理
-(void)viewDidLayoutSubviews
{
// 処理ブロックがあれば、実行
if (completionLayoutSubViews) {
[completionLayoutSubViews start];
// 実行後はクリア
completionLayoutSubViews = nil;
}
}
-(void)test
{
[self.tableView reloadData];
// 終了処理ブロックに、再ロード終了後処理を格納
completionLayoutSubViews = [NSBlockOperation blockOperationWithBlock:^{
/* やりたい処理 */
}];
}
フラグ管理の手間も無く、確実にレイアウト処理後に実行されます。スマートでなかなか良い手です。
ただし、UITableViewController
やUICollectionViewController
の場合のみというのは変わりません。
##解法4(2018/02/18追記)
wzhangさんに提案いただきました。
animateWithDuration
のCompletion
を利用する方法で、最有力案だと思います。
-(void)test
{
[UIView
animateWithDuration: 0.0
animations:^{
// リロード
[self.tableView reloadData];
} completion:^(BOOL finished) {
if (finished) { // 一応finished確認はしておく
/* やりたい処理 */
}
}];
}
-func test()
{
UIView.animate(
withDuration: 0.0,
animations:{
// リロード
self.tableView.reloadData()
}, completion:{ finished in
if (finished) { // 一応finished確認はしておく
/* やりたい処理 */
}
});
}
reloadDataのラッパーにしてしまうのも良いですね。
解法3に比べ、インスタンス変数やviewDidLayoutSubviews
の記載が不要で、reloadData
の呼び元メソッドで完結し、
なんといっても、UITableViewController
やUICollectionViewController
のみの制約無しが大きいです。
#最後に
解法4でFAではないでしょうか
animateWithDuration
が何をアニメーションと判断しているのかの点は気になりますが、
UIViewのドキュメントを見る限り、少なくともUIViewのFrameはアニメーションに含まれるので問題はないかと思います。
https://developer.apple.com/documentation/uikit/uiview