SimpleBoxes

SBPullToRefreshHeaderView - 「引っ張って更新」を再実装してみる

HMDT さんから開発者向け電子書籍がリリースされています ([本] HMDT JOURNAL 創刊 | Cocoaの日々情報局 経由)。

早速、Vol.001 と Vol.002 を購入してみました。木下さんらしい読みやすい文体で、かなり丁寧に書かれています。

……特定の連載記事だけをバラで購入できたら嬉しいなぁ……なんて。ともあれ、今後の展開が楽しみです。

EGOTableViewPullRefresh

さて、この HMDT JOURNAL Vol.001 で紹介されている EGOTableViewPullRefresh は、いわゆる「引っ張って更新」を実現するクラスで、Facebook 謹製のライブラリでも採用されていたりする有名なライブラリです。

組み込みも簡単で、コードも分かりやすく記述されているライブラリなのですが、導入にあたってコントローラ側で UIScrollViewDelegate のメッセージをフォワードする必要がある点がちょっと不満。

例えば、UIButton ではボタンのハイライトのためにタッチイベントをコントローラ側からフォワードしなくても、ボタン側で勝手に処理してくれるように、EGOTableViewPullRefresh もスクロールイベントを勝手に処理してくれたらなぁと思うわけです。

SBPullToRefreshHeaderView - yet another "pull-to-refresh"

……そこで自分で「引っ張って更新」するクラス SBPullToRefreshHeaderView を自作してみました。

[図] SBPullToRefreshHeaderView の実装例

ARC 対応で __weak 修飾子を使っているので、iOS5 以降でお使いいただけます。

他に EGOTableViewPullRefresh から微妙に変わっている部分に「離して更新」の時にリロード用画像を使っています。OS X 向け Twitter アプリケーションのような感じ。

サンプルデモのコードにある通り、コントローラ側では SBPullToRefreshHeaderView の初期化メソッドにターゲットとなるスクロールビューを渡すと、あとは SBPullToRefreshHeaderView 側でよしなにしてくれます……はずです。

mRefreshHeaderView = [[SBPullToRefreshHeaderView alloc] initOnScrollView:self.tableView
                                                            withDelegate:self];

デリゲート SBPullToRefreshHeaderViewDelegate のメソッドはふたつ。

@protocol SBPullToRefreshHeaderViewDelegate <NSObject>

/// 「引っ張り更新」の発動 (ユーザが更新するためにスクロールビューを離した) を通知します。
/// @param headerView - 関連する SBPullToRefreshHeaderView のインスタンス
- (void)didTriggerRefresh:(SBPullToRefreshHeaderView *)headerView;

/// ターゲットが「ローディング」状態がどうかを確認します。
/// @param headerView - 関連する SBPullToRefreshHeaderView のインスタンス
/// @return ローディング状態にある場合、YES をそうでない場合 NO を返します
- (BOOL)isRefreshStillProcessing:(SBPullToRefreshHeaderView *)headerView;

@end

コントローラ側では基本的にはこのふたつのデリゲートメソッドを実装して、ローディングが終了した後に「resetView:」メソッドを送信します。

SBPullToRefreshHeaderView の実装

SBPullToRefreshHeaderView ではスクロールイベントを処理するために KVO (Key-Value Observing) を使っています。

スクロールが発生すると、UIScrollViewcontentOffset が変化することに着目して、そこでスクロール時に処理する内容を記述しています。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
  if (object == mScrollView &&
      [keyPath isEqual:@"contentOffset"])
  {
    [self scrollViewDidScroll:mScrollView];
    if (mIsDragging != mScrollView.isDragging)
    {
      if (!mScrollView.isDragging)
      {
        [self stopDragging];
      }
      mIsDragging = mScrollView.isDragging;
    }
  }
}

ユーザーがターゲットのスクロールビューから指を離したかどうかの判定もここで行っています。スクロールの変化で判定しているので、実際のタッチリリースとは異なるタイミングになりますが、動作させてみた感じ特に違和はない感じ。

オブザーブを開始・終了するタイミングが少しハック的な実装になっています。

……というのもオブザーブを開始した時点で参照カウンタがインクリメントされてしまうため、どこか適当なタイミングでオブザーブを終了してあげないといつまで経ってもオブジェクトが解放されなくなってしまいます。

SBPullToRefreshHeaderView の実装では、ターゲットとなっているスクロールビューの子ビューとして追加されたときにオブザーブを開始、親ビューがターゲットから外れた時点でオブザーブを終了するようにします。

- (void)didMoveToSuperview
{
  if (self.superview == mScrollView)
  { // The view has been added into the target scroll view so it starts 
    // observing contentOffset changes.
    [mScrollView addObserver:self 
                  forKeyPath:@"contentOffset" 
                     options:NSKeyValueObservingOptionNew 
                     context:nil];
  }
  else
  { // The view has been removed from the target scroll view so it stops 
    // observing contentOffset changes.
    [mScrollView removeObserver:self
                     forKeyPath:@"contentOffset"];
  }
}

UIView の 「didMoveToSuperview」メソッドで親ビューの変化をキャッチできるので、そこでオブザーブの開始・終了を制御しています。

GitHub にサンプルデモと一緒に置いてみましたので、もしよろしければお使いください (MIT-License)。

スポンサーリンク

<< Xcode 4.3 リリース :: Objective-C / ARC で unsafe_unretained 利用時のインスタンス破棄タイミングについて >>