UIScrollViewで2本指or3本指スワイプジェスチャーを使う

2012.11.13

またUIScrollViewではまったのでメモ。今回は描画領域より大きなコンテンツ領域を持つUIScrollViewで、2本指or3本指スワイプジェスチャーを使用するという一見簡単そうなのですが、ちょっと悩まされてしまいました。具体的には以下のような問題が発生してしまいました。

  1. スワイプジェスチャーが検出されない
  2. 3本指でスワイプすると、「Ignoring call to [UIPanGestureRecognizer setTranslation:inView:] since gesture recognizer is not active.」がでる

上記のような状況になった方は参考までに読んで頂ければ幸いです。では、とりあえず動かして確認してみましょう。

本サンプルの作成・実行環境は以下の通りです。

サンプルプロジェクトの作成

  • Xcode 4.5.2
  • iOS SDK 6.0

まずは、XcodeよりSingle View Applicationを選択し、以下の内容でプロジェクトを作成しましょう。尚、今回はサンプルですので、保存の際に「Create local git repository for this project」はチェックを外しておきましょう。

項目 設定値
Product Name UIScrollViewSample
Organization Name 自分の名前(サンプルなのでテキトー)
Company Identifier 会社名(サンプルなのでテキトー)
Class Prefix なし
Devices iPhone
Use Storyboards チェックする(ストーリーボードを使用)
Use Automatic Reference Counting チェックする(ARC有効)
Include Unit Tests チェックしない(unit testのターゲットを含まない)

ViewController.hを開き、UIScrollViewのデリゲートメソッドを呼び出せるように、UIScrollViewDelegateプロトコルを実装します。

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UIScrollViewDelegate>

@end

UIScrollViewにスワイプジェスチャーを登録してビューに追加します。1本指でスワイプジェスチャーを検出してしまうとUIScrollViewをパン(ドラッグ)できなくなってしまうので、まずは2本指のスワイプジェスチャーにします。ここで生成するUIScrollViewのインスタンスはViewControllerのプロパティとして保持します。また、サンプルを分かりやすくするため、背景色にスクロールビューテクスチャを設定した2,000x2,000pxの矩形をUIScrollView内に配置します。それではViewController.mを開き、以下のように追加・変更してください。

ViewController.m

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) UIScrollView *scrollView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // UIself.scrollViewのインスタンスを生成
    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    
    // UIself.scrollViewのDelegateにself(ViewController)を設定
    self.scrollView.delegate = self;
    
    // コンテンツサイズをスクロールビューの表示領域より大きい値に設定する(値は適当)
    self.scrollView.contentSize = CGSizeMake(2000.0f, 2000.0f);
    
    // ズームスケールの最大・最小値を設定
    self.scrollView.maximumZoomScale = 2.0f;
    self.scrollView.minimumZoomScale = 0.5f;
    self.scrollView.maximumZoomScale = 2.0f;
    self.scrollView.minimumZoomScale = 0.5f;
    
    // 背景色にスクロールビューテクスチャを設定した矩形を配置
    UIView *aView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 2000.0f, 2000.0f)];
    aView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
    [self.scrollView addSubview:aView];
    
    // UISwipeGestureRecognizerインスタンスを生成
    // スワイプが実行されたときに- swiped:メソッドを実行する
    UISwipeGestureRecognizer *swipeGestureRecognizer = [[UISwipeGestureRecognizer alloc]
                                                        initWithTarget:self
                                                        action:@selector(swiped:)];
    
    // 2本指でスワイプされたときに動作するようにする
    swipeGestureRecognizer.numberOfTouchesRequired = 2;
    
    // 上方向のスワイプを検知する
    swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
    
    // スクロールビューにスワイプジェスチャーを登録
    [self.scrollView addGestureRecognizer:swipeGestureRecognizer];
    
    // ビューにself.scrollViewを追加
    [self.view addSubview:self.scrollView];
}

// 拡大・縮小の対象となるビューを返す
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    // - viewDidLoadメソッドで生成したaViewが返る
    return [[self.scrollView subviews] objectAtIndex:0];
}

// スワイプジェスチャーを検知したときに呼ばれるメソッド
- (void)swiped:(UISwipeGestureRecognizer *)sender
{
    // スワイプされたらログ出力
    NSLog(@"swiped!!");
}

@end

この状態でまず実行してみましょう。・・・いかがでしょうか?スワイプジェスチャー発生時に呼び出されるはずの- swiped:メソッドが実行されないはずです。

問題1.スワイプジェスチャーが検出されない!!

これはUIScrollViewに予め登録されている他のジェスチャーにイベントを奪われてしまうからです。UIScrollViewには予め、表示領域をパン(ドラッグ)するためのUIPanGestureRecognizerと、つまんで拡大・縮小(ピンチイン・ピンチアウト)するためのUIPinchGestureRecognizerが登録されています。上の例では、UIPanGestureRecognizerにイベントを奪われて、いくらスワイプしてもパン(ドラッグ)ジェスチャーとして処理されてしまうため、スワイプを検知できなくなっているわけです。ちなみに、UIScrollViewに定義されているUIPanGestureRecognizerとUIPinchGestureRecognizerのインスタンスはそれぞれ、panGestureRecognizerpinchGestureRecognizerでアクセスできます。

解決方法:ジェスチャーの優先順位を変更する「- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer」メソッド

パンジェスチャーより先にスワイプジェスチャーを検出するには、処理するジェスチャーの優先順位を変えてあげればいいのです。その優先順位を変えてあげるのが、- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizerメソッドです。このメソッドはジェスチャー系クラスの親クラスであるUIGestureRecgnizerクラスで定義され、実行すると引数otherGestureRecognizerで指定したジェスチャーが失敗するまで自身のジェスチャーが実行されなくなります。早速このメソッドを使用して、ジェスチャーの優先順位を変えてみましょう。

ViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    ・・・
    
    // 上方向のスワイプを検知する
    swipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionUp;

    // スワイプが失敗するまでパン(ドラッグ)を実行しない!
    [self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:swipeGestureRecognizer];

    // スクロールビューにスワイプジェスチャーを登録
    [self.scrollView addGestureRecognizer:swipeGestureRecognizer];    

    ・・・
}

実行してみましょう。・・・いかがでしょうか?スワイプするとXcodeのコンソールにswiped!!と表示されたと思います。

問題2.3本指でスワイプすると、「Ignoring call to ・・・」がでる

では今度は、3本指でスワイプするように設定してみましょう。

ViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    ・・・
    
    // 3本指でスワイプされたときに動作するようにする
    swipeGestureRecognizer.numberOfTouchesRequired = 3; // 2から3に変更

    ・・・
}

実行してみましょう。・・・すると、Xcodeのコンソールにたまに「Ignoring call to [UIPanGestureRecognizer setTranslation:inView:] since gesture recognizer is not active.」が表示されてしまいます。このとき、アプリが落ちることはないのですがちょっと気持ち悪いです。

解決方法:パンジェスチャーの最大検出数をスワイプジェスチャーの指の本数に合わせる

この警告を表示させないようにするには、パンジェスチャーの最大検出数maximumNumberOfTouchesをスワイプジェスチャーで指定した指の本数を指定してあげます。このmaximumNumberOfTouchesですが、デフォルト値が2に設定されているため、2本指でのスワイプジェスチャーのときはこのメッセージが表示されていなかったようです。

ViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    ・・・
    
    // 3本指でスワイプされたときに動作するようにする
    swipeGestureRecognizer.numberOfTouchesRequired = 3; // 2から3に変更

    // パンジェスチャーの最大検出数をスワイプジェスチャーの指の数(3)にあわせる
    self.scrollView.panGestureRecognizer.maximumNumberOfTouches = 3;

    ・・・
}

いかがでしょうか。警告が表示されなくなったかと思います。

まとめ

まだまだ謎の多いUIScrollViewですがとても便利なコンポーネントです。またはまったりしたときは共有していきたいと思います。ではまた!!