UIRefreshControl の罠


UIRefreshControl は罠がいくつかあるので、使い方を含めて対策をまとめてみました。

## UIRefreshControl の使い方

`UITableViewController` のセットアップ時に `UIRefreshControl` を生成し、ハンドラを設定した上で `self.refreshControl` に入れてあげます。

```objc:MyTableViewController.m
- (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 が消失します。
お試し用のダミーコードはこんな感じです。

```objc:MyTableViewController.m
- (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 を挿入することができます。

```objc
self.collectionView.backgroundView = refreshControl;
```

## 罠 1: endRefresing してもきれいに引っ込まないことがある

Pull to Refresh 後、以下の状態で `endRefreshing` を呼んでも、`contentOffset` が元に戻らないという問題があります。

* TableView に表示する項目数が、スクロールが発生するほど存在する。
* `endRefreshing` される前に、Refresh Control が一部隠れるくらいスクロールした状態にしておく。

![ss1.png](https://qiita-image-store.s3.amazonaws.com/0/25727/fda6d5eb-e49c-365d-c093-ee985852bd16.png)

この問題を解消するには、`endRefreshing` する際に明示的に `contentOffset` を調整します。
ただこの `contentOffset` の値は `(0, 0)` とは限らないので、

* `viewDidAppear` などのタイミングでデフォルトの `contentOffset` を取得しておく。
* ステータスバーとナビゲーションバーの高さから算出しておく。

のような方法で求める必要があります。
あとはこの値を設定してあげるだけなのですが、`contentOffset` プロパティを書き換えるのではなく `setContentOffset:animated:` メソッドを使ってアニメーションさせるのもポイントです。

```objc
// 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` を使って最終更新日時を表示する場合を考えます。
まず、処理が完了した時点で設定することには全く問題ありません。

```objc
[self.tableView setContentOffset:self.defaultContentOffset animated:YES];
[self.tableView reloadData];

// 最終更新日時を設定
NSAttributedString* lastUpdatedDateTime = ...;
self.refreshControl.attributedTitle = lastUpdatedDateTime;

[self.refreshControl endRefreshing];
```

次にこの画面が初めて表示されたときでも、前回の最終更新日時を表示するようにしてみましょう。

```MyTableViewController.m
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    NSAttributedString* lastUpdatedDateTime = ...;
    self.refreshControl.attributedTitle = lastUpdatedDateTime;
}
```

しかしこの状態で Pull to refresh を行おうとすると...こんな残念なことに！
バウンスもなんだかおかしなことになっています。

![ss2.png](https://qiita-image-store.s3.amazonaws.com/0/25727/e86e4e84-898f-d73a-fe0d-ba2cc7a87b3e.png)

これをなんとかするには、「Refresh Control を ViewController の view 配下に置く前に、`attributedTitle` に空でないテキストを入れておく」必要があります。
(これを前もって行うことで、`attributedTitle` を含めたレイアウトの調整が行われると考えられる)

```MyTableViewController.m
- (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;
}
```

対症療法的な解決でいまいちすっきりしませんが、もし同じ問題でお困りの方の一助になれば幸いです。