UIScrollView の上で UIView を動かしたい
今作っているiPadアプリで、画面をピンチインアウトで拡大縮小して、さらにその上でドラッグで部品を動かせるようにしたい。
まずは画面をピンチインアウトで拡大縮小する方法を調べたところ、UIScrollView の上に部品を置いてあげればそれでよいとのこと。
iPhoneアプリ開発、その(118) UIScrollViewはどうやって使うのか?|テン*シー*シー
viewForZoomingScrollView メソッドも実装する必要がある。
ここまでは簡単だったのだが、拡大縮小できるようにして、さらに一部の UIView をドラッグできるようにしようとすると、touchesBegan たちが飛んでこない。
ここに同様のことが書かれていた。
iPhoneアプリ開発、その(121) なぜにtouchesBeganが来ない?|テン*シー*シー
nextResponder にタッチ情報を渡せばよいかと思ったが、そもそも UIScrollView 上に置いている部品に touchesBegan たちが呼ばれないようだ。
どうやら、UIScrollView がタッチ情報を取得して拡大縮小をしているので、それにより touchesBegan などは呼び出されないように見える。
かわりに、UIScrollView の scrollViewWillBeginDragging などは呼び出されるけれども、これだとtouchesMovedがないので使えない。
ということで、何とかUIScrollView上に置いた部品で touchesBegan 達をとれる方法を調べたところ、Stack Overflow で同様の質問がいろいろと見つかった。
UIScrollView に関してはかなりいろいろ混乱が起きていてノイズが多いのだが、UIWindow ををサブクラス化して sendEvent でメッセージをインターセプトする方法を使ったところ、やりたいことが実現できた。
iphone – Observing pinch multi-touch gestures in a UITableView – Stack Overflow
これは、UIWebView に独自にタッチジェスチャーを追加したりするときに使う方法のようで、これを使えばUIWindowに来るイベントを何でもインターセプトできるようだ。
ということで touchesBegan 達を UIView に送ることができた。
Stack Overflow で説明されていた、EventInterceptWindowDelegate の実装
#import <Foundation/Foundation.h> @protocol EventInterceptWindowDelegate - (BOOL)interceptEvent:(UIEvent *)event; // return YES if event handled @end @interface EventInterceptWindow : UIWindow { // It would appear that using the variable name 'delegate' in any UI Kit // subclass is a really bad idea because it can occlude the same name in a // superclass and silently break things like autorotation. id <EventInterceptWindowDelegate> eventInterceptDelegate; } @property(nonatomic, assign) id <EventInterceptWindowDelegate> eventInterceptDelegate; @end
UIWindow の sendEvent をオーバーライドして、自分で設定した delegate に情報を送る
#import "EventInterceptWindow.h" @implementation EventInterceptWindow @synthesize eventInterceptDelegate; - (void)sendEvent:(UIEvent *)event { if ([eventInterceptDelegate interceptEvent:event] == NO) { NSLog(@"sendEvent", event); [super sendEvent:event]; } } @end
AppDelegate で delegate をセットする
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after app launch. // Set the view controller as the window's root view controller and display. self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; self.window.eventInterceptDelegate = self.viewController; return YES; }
呼び出された側で、動かしたいUIViewにタッチ情報を送る
- (BOOL)interceptEvent:(UIEvent *)event { NSLog(@"interceptEvent"); NSSet *touches = [event allTouches]; UITouch *oneTouch = [touches anyObject]; if([testView isObjTouched:[oneTouch locationInView:testView]]) { switch(oneTouch.phase) { case UITouchPhaseBegan: [testView touchesBegan:touches withEvent:event]; break; case UITouchPhaseMoved: [testView touchesMoved:touches withEvent:event]; break; case UITouchPhaseEnded: [testView touchesEnded:touches withEvent:event]; break; default: break; } return YES; } return NO; }
追記 2011/06/06
もう一度試作していたところ、上記の UIWindow ををサブクラス化して sendEvent でメッセージをインターセプトする方法 を使わなくても、scrollViewのcontentviewの方にメッセージがとぶようになった。
そもそも、UIScrollView は、Appleの説明 や UIScrollView touch handling – Stack Overflow を参照すると、タッチしてからタイマーを開始し、タイマーが発火する前に指の移動を検知したらスクロールと判断するようになっている。
Because a scroll view has no scroll bars, it must know whether a touch signals an intent to scroll versus an intent to track a subview in the content. To make this determination, it temporarily intercepts a touch-down event by starting a timer and, before the timer fires, seeing if the touching finger makes any movement.
このため、タッチしてすぐに指を動かすとスクロール、ある程度指を置いてから動かすとcontentViewの方にメッセージが飛ぶようになっている。
ただ、この挙動はUIScrollViewのcanCancelContentTouches で変更することができる。
canCancelContentTouches を YES に設定すると、contentView にはメッセージが飛ぶことはないようだ。以前この記事を書いたときは、canCancelContentTouches が YES になっていたのでつねにUIScrollViewの方が反応していたのかも知れない。
canCancelContentTouches を NO の設定しておけば、ちゃんと contentViewにメッセージが飛んでいる。
UIWindow ををサブクラス化して sendEvent でメッセージをインターセプトする方法 は不要だったようだ。
ただ、今回は、
1) あるUIImageViewをタッチしてドラッグしているときにはスクロールさせず、そのUIImageViewを移動させる
2) そのUIImageViewでない、何もないところをタッチしてドラッグしているときにはスクロールさせる
ようにしたい。
この時の問題は、タッチしてちょっとしてcontentViewの方にメッセージが送られてしまうと、2)が実現されない。
このため、2)の時にはcanCancelContentTouches = YES として、contentView にメッセージが送られないようにした。
これにより、上記の挙動が実現できたようだ。
その後、delaysContentTouches を NO (デフォルトはYES) にしておけば、タッチしてすぐドラッグしてもcontentViewの方にメッセージが送られることもわかった。
関連記事
-
Lexical or Preprocessor issue ‘xxx.h’ file not found
NSMutableArray に要素をランダムに並び替える機能を入れようと思い、 objective
-
[iOS9] viewWillAppear で部品の位置が確定しなくなった
どうやら iOS9 から viewWillAppear では部品の位置が確定しなくなったらしく、vi
-
GTD用にOmniFocusを購入
半年くらいMacBookとiPhoneでOmniFocusを使っている。 なかなか良いので、紹介して
-
MacPeople 2012 5月号にはほしいガジェットがたくさん紹介されてる
定期購読しているMacPeopleの今月号(5月号)で面白い製品がたくさん紹介されていたのでメモを書
-
ようやくAppStore に Submit
@ITの 日本人がつまずかないためのiPhone開発ポイント を読みながらAppStoreにアプリ公
-
iPhone用 OmniGraffle ステンシル
ここのページを参考にして、iPhone用OmniGraffleステンシルを入手してみた。 確かにこれ
-
Universal アプリのテスト
iPhoneアプリとBluetoothで通信するUniversalなアプリをしばらく作っていたが、よ
-
KickstarterでMiselu の iPad用キーボード C.24 に出資してみた
C.24 - The Music Keyboard for iPad by Miselu —
-
Withings WS-50 不具合発生
昨年12月に購入した Withings WS-50をまずまず便利に使っていたのだが、数週間前から、電
-
iOS7 から導入されるキッズカテゴリーにアプリを載せるための作業をしてみる
News and Announcements for Apple Developers でア