LoginSignup
26
26

More than 5 years have passed since last update.

[Objective-C] タップによるズーム機能の実装メモ

Posted at

[PDF] iOS Scroll Viewプログラミングガイドを読んで、タップによるズーム機能が紹介されていたので、実際に作ってみたもののメモです。

実際のサンプルがgithubに公開されているので、そちらを見てみると分かりやすいと思います。
ここでは、ガイドラインを元に自分でコードを書いてみて気づいた点などを書いていきます。

タップによるズーム

ピンチジェスチャに関しては基本的な実装をフレームワーク側で行っていますが、タップによるズーム(ダブルタップでズーム、など)を実装するにはアプリケーション側での実装が必要です。

該当ビューのサブクラス

タップによるズームをさせたいビューをサブクラス化し、タッチに対しての制御を実装します。
また、いくつかのプロトコルも定義します。

SMPView.h
@protocol TapDetectingViewDelegate;

@interface SMPView : UIView

@property (assign, nonatmic) id<TapDetectingViewDelegate> delegate;

@end

@protocol TapDetectingViewDelegate <NSObject>

@optional
- (void)tapDetectingView:(SMPView *)view goSingleTapAtPoint:(CGPoint)tapPoint;
- (void)tapDetectingView:(SMPView *)view goDoubleTapAtPoint:(CGPoint)tapPoint;
- (void)tapDetectingView:(SMPView *)view goTwoFingerTapAtPoint:(CGPoint)tapPoint;

@end

プロトコルのメソッドは、このビューがタッチを識別し、 シングルタップダブルタップ2本指タップ を判断したあとにデリゲートに対してプロトコロで定義されたメソッドを呼び出します。

SMPView.m
@interface SMPView ()

@property (assign, nonatomic) BOOL multipleTouches;
@property (assign, nonatomic) BOOL twoFingerTapIsPossible;
@property (assign, nonatomic) CGPoint tapLocation;

- (void)handleSingleTap;
- (void)handleDoubleTap;
- (void)handleTwoFingerTap;

@end

multipleTouchestwoFingerTapIsPossibleプロパティは、それぞれマルチタッチかどうか、2本指タップかどうかを表すBOOL値です。
タッチ開始時と終了時で適切にハンドリングし、デリゲートメソッドを呼び出すのに利用します。

@implementation SMPView

- (id)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        // マルチタッチを有効化
        self.multipleTouchEnabled = YES;

        self.twoFingerTapIsPossible = NO;
        self.multipleTouches        = NO;
    }

    return self;
}
// ----------------------------------------------
// タッチ制御
// ----------------------------------------------

// タッチ開始処理
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 保留中のhandleSingleTapメッセージがあればそれを取り消す
    [NSObject cancelPreviousPerformRequestsWithTarget:self
                                             selector:@selector(handleSingleTap)
                                               object:nil];

    // タッチの状態を更新
    if ([[event touchesForView:self] count] > 1) {
        self.multipleTouches = YES;
    }
    if ([[event touchesForView:self] count] > 2) {
        // 指が3本以上触れていたら2本指タップの状態を`NO`に
        self.twoFingerTapIsPossible = NO;
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // touches.countは話された指の数
    // [event touchesForView:self].countは、直前までViewに触れていた指の数
    BOOL allTouchesEnded = ([touches count] == [[event touchesForView:self] count]);

    // まず単純なシングル/ダブルタップを判定する
    // 複数回のタッチが行われていない場合のみ、シングル/ダブルタップの可能性がある
    if (!self.multipleTouches) {
        UITouch *touch = [touch anyObject];
        self.tapLocation = [touch locationInView:self];

        // シングルタップ
        if ([touch tapCount] == 1) {
            [self performSelector:@selector(handleSingleTap)
                      withObject:nil
                      afterDelay:0.35];
        }
        else if ([touch tapCount] == 2) {
            [self handleDoubleTap];
        }
    }

    // 複数回のタッチが行われていた場合は、それが2本指のタップだったかどうか、
    // また、そうである状況が否定されていないか確認する
    else if (self.multipleTouches && self.twoFingerTapIsPossible) {

        // ケース1: 両方のタッチの同時の終了の場合
        if ([touches count] == 2 && allTouchesEnded) {
            int i = 0;
            NSUinteger tapCounts[2];
            CGPoint tapLocation[2];

            for (UITouch *touch in touches) {
                tapCount[i]    = [touch tapCount];
                tapLocation[i] = [touch locationInView:self];
                i++;
            }

            if (tapCounts[0] == 1 && tapCounts[1] == 1) {
                // 両方ともシングルタップであれば、これは2本指のタップ
                self.tapLocation = midpointBetweenPoints(tapLocations[0], tapLocations[1]);
                [self handleTwoFingerTap];
            }
        }

        // ケース2: これが1回のタッチの終了で、もう1つはまだ終了していない場合
        else if ([touches count] == 1 && !allTouchesEnded) {
            UITouch *touch = [touches anyObject];

            if ([touch tapCount] == 1) {
                // タッチが1回のタップであれば、その位置を保存し、
                // 2番目のタッチの位置と平均値を求められるようにする
                self.tapLocation = [touch locationWithView:self];
            }
            else {
                self.twoFingerTapIsPossible = NO;
            }
        }

        // ケース3: これが2つのタッチの2番目の終了である場合
        else if ([touches count] == 1 && allTouchesEnded) {
            UITouch *touch = [touches anyObject];

            if ([touch tapCount] == 1) {
                // 最後のタッチがシングルタップであれば、これは2本指のタップ
                self.tapLocation = midpointBetweenPoints(self.tapLocation, [touch locationInView:self]);
                [self handleTwoFingerTap];
            }
        }
    }

    // すべてのタッチが終了した場合、タッチの監視状態をリセットする
    if (allTouchesEnded) {
        self.twoFingerTapIsPossible = YES;
        self.multipleTouches = NO;
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.twoFingerTapIsPossible = YES;
    self.multipleTouches = NO;
}
// ----------------------------------------------
// タップイベント制御
// ----------------------------------------------

- (void)handleSingleTap
{
    if ([self.delegate respondsToSelector(@selector(tapDetectingView:goSingleTapAtPoint:)]) {
        [self.delegate tapDetectingView:self goSingleTapAtPoint:self.tapLocation];
    }
}

- (void)handleDoubleTap
{
    if ([self.delegate respondsToSelector(@selector(tapDetectingView:goDoubleTapAtLocation:)]) {
        [self.delegate tapDetectingView:self goDoubleTapAtLocation:self.tapLocation];
    }
}

- (void)handleTwoFingerTap
{
    if ([self.delegate respondsToSelector(@selector(tapDetectingView:goTwoFingerTapAtLocation:)]) {
        [self.delegate tapDetectingView:self goTwoFingerTapAtPoint:self.tapLocation];
    }
}

@end
// ----------------------------------------------
// ヘルパー関数
// ----------------------------------------------

CGPoint midpointBetweenPoints(CGPoint a, CGPoint b)
{
    CGFloat x = (a.x + b.x) / 2.0;
    CGFloat y = (a.y + b.y) / 2.0;
    return CGPointMake(x, y);
}

コントローラクラスでデリゲートを実装する

コントローラ側では、ScrollViewとズーム対象のビューを入れ子にして生成します。
その上で、ScrollViewのdelegateプロパティに、自分自身をセットします。(つまり普通のdelegateの使い方です)

// SMPViewController.m

// デリゲートのみ記載

#define ZOOM_STEP 1.5

#pragma mark delegate methods

- (void)tapDetectingView:(SMPView *)view goSingleTapAtPoint:(CGPoint)tapPoint
{
    // シングルタップは処理しない
}

// ダブルタップ時の処理
- (void)tapDetectingView:(SMPView *)view goDoubleTapAtPoint:(CGPoint)tapPoint
{
    float newScale = self.scrollView.zoomScale / ZOOM_STEP;
    CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
    [self.scrollView zoomToRect:zoomRect animated:YES];
}

// 2本指タップ時の処理
- (void)tapDetectingView:(SMPView *)view goTwoFingerTapAtPoint:(CGPoint)tapPoint
{
    float newScale = self.scrollView.zoomScale / ZOOM_STEP;
    CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
    [self.scrollView zoomToRect:zoomRect animated:YES];
}

#pragma mark helper function

- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center
{
    CGRect zoomRect;

    // scaleで割ることで、ズーム対象となる矩形のサイズを決める
    zoomRect.size.height = self.scrollView.frame.size.height / scale;
    zoomRect.size.width  = self.scrollView.frame.size.width  / scale;

    // 矩形の中心をズームの中心にする
    zoomRect.origin.x = center.x - (zoomRect.size.width  / 2.0);
    zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);

    return zoomRect;
}

解説

基本的にどのデリゲートメソッドも、渡された位置を元に処理を行っています。
処理の中心になるのがzoomRectForScale:withCenter:メソッドです。

これは、渡されたスケールと位置から、ズーム対象となる矩形を生成して返します。
zoomRectのイメージは、生成された矩形が元のスケールに戻るように計算される、と(自分は)イメージしています。
(つまり、小さい矩形なら、「それを元に戻す=拡大」、大きい矩形なら、「それを元に戻す=縮小」、というイメージ)

あとは生成された矩形を、ScrollViewのzoomToRect:animated:メソッドに渡してやることでズームが実行されます。

26
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
26