ピンチイン&アウトで画像の拡大縮小

セカエカメラ

大学の講義でチームでiPhoneアプリを作成することになり、ドラえもんの着せ替えカメラをパk(ryにインスパイアされたセカエカメラというものを作っている。

僕はGUIを担当することになり、カメラ起動中の上のレイヤーに洋服の画像をオーバーレイさせたり、その服画像をピンチイン&アウトで拡大縮小するあたりで苦戦したのでまとめ。

タッチ検出

Viewへのタッチを検出するには、UIViewもしくはそのサブクラスでtouchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent:の3つのメソッドをオーバーライドする必要がある。
「UIViewもしくはそのサブクラスで」というのが肝で、ずっとUIViewControllerのサブクラスでオーバーライドしていた。
タッチを検出したい場合、適当なUIViewのサブクラスでtouches系のメソッドをオーバーライドして、それをUIViewControllerに持たせてあげればよい。
そして、タッチを検出するUIViewではself(UIView)のuserInteractionEnabledプロパティをYESにしておかないと、touches系のメソッドをオーバーライドしてもタッチが通知されない。これはオライリーiPhone SDK アプリケーション開発ガイドにも載ってない!…はず。ちなみにマルチタッチを処理するにはmultipleTouchEnabledプロパティもYESにする(これはオライリー本にも載ってる)。

こんな感じ

// hoge.h
@interface TouchView : UIView {
}
@end

@interface MyViewController : UIViewController {
    TouchView *touchView;
}


// hoge.m
@implementation TouchView

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self != nil) {
        // いろいろ
        self.userInteractionEnabled = YES;
	self.multipleTouchEnabled = YES;
        // いろいろ
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
}
@end

@implementation MyViewController
// (ry
@end

ピンチイン&アウトの処理はオライリー本の4章に載ってるPinchMeに手を加えて適当に実装。

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    int finger = 0;
    NSEnumerator *enumerator = [touches objectEnumerator];
    UITouch *touch;
    CGPoint location[2];
    while ((touch = [enumerator nextObject]) && finger < 2) {
	location[finger] = [touch locationInView:self];
	finger++;
    }

    if (finger == 2) {
	CGPoint scale;
	scale.x = fabs(location[0].x - location[1].x);
	scale.y = fabs(location[0].y - location[1].y);

	double currLength = sqrt((scale.x * scale.x) + (scale.y * scale.y));  // 指と指の距離
	if (prevLength > 0) {  // クラス変数prevLengthは-1で初期化している
	    double scalingFactor = currLength / prevLength;  // 倍率を計算
			
	    CGRect rect = self.frame;
       	    rect.size.width *= scalingFactor;
	    rect.size.height *= scalingFactor;
	    rect.origin.x = (kDisplayWidth - rect.size.width) / 2;  // 中心を取る
	    rect.origin.y = (kDisplayHeight - rect.size.height) / 2;  // 中心を取る
	    self.frame = rect;
        }
	prevLength = currLength;
    }
    [super touchesMoved:touches withEvent:event];
}


[追記]
タッチ終了時にするべきことは特にないかなぁと思っていたけどあった。touchesEnded:withEvent:をオーバーライドしてあげてprevLength = -1しておくと良い。そうすると、次にピンチするとき「初ピンチ」ということになるのでピンチイン(アウト)→1回指を離して→更にピンチイン(アウト)ができるようになる。というかこれをしておかないと例えば画像を縮小したいとき、指と指がこれ以上くっつかないくらいまでピンチインして更にピンチインしたいとき、1回指を離してまた新しいところからピンチしようとするとprevLength < currLengthになってしまうので画像が拡大してしまう。

UIImagePickerControllerの妙

UIImagePickerControllerを使ってピッカービュー(?)で洋服の写真を選択したら、その写真を薄く表示しつつカメラを起動するということをやりたかった(着せ替えカメラだから)。

ピッカービューで画像を選択したことをimagePickerController:picker:image:editingInfoメソッドで検知したら、[self dismissModalViewControllerAnimated:YES]でピッカービューを消してから、[self presentModalViewController:cameraPicker animated:YES]のようにしてカメラを起動してみたのだけど、

Terminating app due to uncaught
exception 'NSInternalInconsistencyException', reason: 'Attempting to begin a modal
transition from to
while a transition is already in progress. Wait for viewDidAppear/viewDidDisappear
to know the current transition has completed'

というエラーが。調べてみたらこのページに行き着いて、「トランジション中に新しいモーダルトランジションを発生させようとしているぞ!」と怒られているらしいということがわかった。また、viewDidAppearを実装しろと教えてくれたので次のように実装。

- (void)viewDidAppear:(BOOL)animated {
    if (imageIsPicked) {
        cameraPicker = [[UIImagePickerController alloc] init];
	cameraPicker.sourceType = UIImagePickerControllerSourceTypeCamera;
	[cameraPicker setDelegate:self];
	[cameraPicker setCameraOverlayView:overlayView];
	[self presentModalViewController:cameraPicker animated:YES];
    }
}

- (void)imagePickerController:(UIImagePickerController *)picker
	didFinishPickingImage:(UIImage *)image
		  editingInfo:(NSDictionary *)editingInfo {
    if (picker == imagePicker) {
	[overlayView setImage:image];
	imageIsPicked = YES;
	[self dismissModalViewControllerAnimated:YES];  // ここでモーダルビューが消える(=下に隠れてたビューが現れる)
    } else if (picker == cameraPicker) {
	// Something
    }
}

viewDidAppearというのはビューが画面に現れたときに呼び出されるものなので、モーダルビューが消えるタイミングでカメラが起動する。