UIRefreshControl は罠がいくつかあるので、使い方を含めて対策をまとめてみました。
UIRefreshControl の使い方
UITableViewController
のセットアップ時に UIRefreshControl
を生成し、ハンドラを設定した上で self.refreshControl
に入れてあげます。
- (void)viewDidLoad {
[super viewDidLoad];
// Refresh Control のインスタンス化
UIRefreshControl* refreshControl = [UIRefreshControl new];
// ユーザが Pull to refresh したときのハンドラを設定
[refreshControl addTarget:self
action:@selector(refreshControlStateChanged:)
forControlEvents:UIControlEventValueChanged];
// TableViewController に追加
self.refreshControl = refreshControl;
}
ハンドラはユーザが Pull to refresh を行ったときに呼び出されます。
一般的にはこのとき TableView の更新処理を行い、これが完了したら [self.refreshControl endRefreshing];
を呼び出せば、表示中の Refresh Control が消失します。
お試し用のダミーコードはこんな感じです。
- (void)refreshControlStateChanged:(id)sender
// 3 秒待ってからハンドリングを行う、URL リクエストとレスポンスに似せたダミーコード
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// モデルの更新などのレスポンス処理...
// UI 更新
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
});
});
}
実は Collection View でも使える
UICollectionViewController
には refreshControl
プロパティがありませんが、下記のようにすれば Refresh Control を挿入することができます。
self.collectionView.backgroundView = refreshControl;
罠 1: endRefresing してもきれいに引っ込まないことがある
Pull to Refresh 後、以下の状態で endRefreshing
を呼んでも、contentOffset
が元に戻らないという問題があります。
- TableView に表示する項目数が、スクロールが発生するほど存在する。
-
endRefreshing
される前に、Refresh Control が一部隠れるくらいスクロールした状態にしておく。
この問題を解消するには、endRefreshing
する際に明示的に contentOffset
を調整します。
ただこの contentOffset
の値は (0, 0)
とは限らないので、
-
viewDidAppear
などのタイミングでデフォルトのcontentOffset
を取得しておく。 - ステータスバーとナビゲーションバーの高さから算出しておく。
のような方法で求める必要があります。
あとはこの値を設定してあげるだけなのですが、contentOffset
プロパティを書き換えるのではなく setContentOffset:animated:
メソッドを使ってアニメーションさせるのもポイントです。
// defaultContentOffset は上記の方法などであらかじめ求めておく
[self.tableView setContentOffset:self.defaultContentOffset animated:YES];
[self.tableView reloadData];
[self.refreshControl endRefreshing];
でもユーザが下のほうにスクロールしていたら、これだと無理矢理一番上へ戻されてしまわない?と思うかもしれませんが、なぜか戻されない...謎です。
(iOS 7.1 〜 8.1 では問題ないことを確認しています)
罠 2: attributedTitle を設定するタイミングによって表示がおかしくなる
Refresh Control には attributedTitle
というプロパティがあり、インジケーターの下に任意のテキストを表示することができます。
しかしこのプロパティにテキストを設定すると、タイミングによって Table View の表示がおかしくなることがあります。
attributeTitle
を使って最終更新日時を表示する場合を考えます。
まず、処理が完了した時点で設定することには全く問題ありません。
[self.tableView setContentOffset:self.defaultContentOffset animated:YES];
[self.tableView reloadData];
// 最終更新日時を設定
NSAttributedString* lastUpdatedDateTime = ...;
self.refreshControl.attributedTitle = lastUpdatedDateTime;
[self.refreshControl endRefreshing];
次にこの画面が初めて表示されたときでも、前回の最終更新日時を表示するようにしてみましょう。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSAttributedString* lastUpdatedDateTime = ...;
self.refreshControl.attributedTitle = lastUpdatedDateTime;
}
しかしこの状態で Pull to refresh を行おうとすると...こんな残念なことに!
バウンスもなんだかおかしなことになっています。
これをなんとかするには、「Refresh Control を ViewController の view 配下に置く前に、attributedTitle
に空でないテキストを入れておく」必要があります。
(これを前もって行うことで、attributedTitle
を含めたレイアウトの調整が行われると考えられる)
- (void)viewDidLoad {
[super viewDidLoad];
UIRefreshControl* refreshControl = [UIRefreshControl new];
[refreshControl addTarget:self action:@selector(refreshControlEventChanged:) forControlEvents:UIControlEventValueChanged];
// attributedTitle にテキストを入れておく
// self.refreshControl にセットする前に行うこと!
refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@" " attributes:nil];
self.refreshControl = refreshControl;
}
対症療法的な解決でいまいちすっきりしませんが、もし同じ問題でお困りの方の一助になれば幸いです。