7883


# 環境

Xcode 6.2/iOS SDK 8.2/iOS Simulator iPhone 5s(iOS 7.1), 6(iOS 8.2)

# テストコード

**註** : ストーリーボードを使わず、コードのみで画面を作成します。

[code]

    @interface AppDelegate : UIResponder <UIApplicationDelegate>
    
    @property (strong, nonatomic) UIWindow *window;
    @property (strong, nonatomic) UIWindow *secondWindow;
    
    @end
    
    @implementation AppDelegate
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
      
      self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
      self.window.restorationIdentifier = @"back window";
      self.window.backgroundColor = [UIColor lightGrayColor];
      [self.window addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(didTap:)]];
      [self.window makeKeyAndVisible];
      
      self.secondWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
      self.secondWindow.restorationIdentifier = @"front window";
      self.secondWindow.backgroundColor = [UIColor colorWithRed: 1.0f green: 0 blue: 0 alpha: 0.25f];
      
      // (1)
      [self.secondWindow addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(didTap:)]];
      
      self.secondWindow.windowLevel = self.window.windowLevel + 1;
      self.secondWindow.hidden = NO;
      
      return YES;
    }
    
    #pragma mark - Responses
    
    - (void)didTap:(UITapGestureRecognizer *)tap {
      
      NSLog(@"Did tap %@.", tap.view.restorationIdentifier);
    }
    
    @end
    
[/code]

2枚のウィンドウが同じサイズで生成され、重ねて表示されています。キーウィンドウは背面のほうです。どちらのウィンドウにもタップジェスチャーの認識が組み込まれています。

# やりたいこと

このテストコードで画面をタップすると

> Did tap front window.

と出力されます。

テストコードで、(1)の行をコメントアウトして、 **前面のウィンドウがイベントを処理しなくなったとき、背面のウィンドウがタッチイベントを認識するように**
し、

> Did tap back window.

と出力されるようにしたいです。

# 既に分かっていること・調べたこと

  * レスポンダーチェーンは最前面のビューから始まり、イベントを処理できるレスポンダが見つかるまでスーパービューを遡ってUIWindow、UIApplicationへと終着するため、ビュー階層の異なるUIWindowの位置を重ねても背面へタッチが伝搬することはない
  * UIWindowがキーウィンドウであるかどうかで異なるのは、モーションイベントと遠隔操作イベントを受け取るか否かであり、タッチイベントはそれがキーウィンドウであるかに関係なく受け取る

**参考にしているリファレンスやガイド**

  * [UIResponder Class Reference](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIResponder_Class/index.html) およびクラスのヘッダファイル
  * [UIWindow Class Reference](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIWindow_Class/index.html) およびクラスのヘッダファイル
  * [Understanding Windows and Screens](https://developer.apple.com/library/prerelease/ios/documentation/WindowsViews/Conceptual/WindowAndScreenGuide/WindowScreenRolesinApp/WindowScreenRolesinApp.html)
  * [iOS Developer Center 日本語ドキュメント](https://developer.apple.com/jp/documentation/) に掲載されている 
    * iOSイベント処理ガイド（日本語版PDF）

# 制約・要件

  * 前面のウィンドウはデバイス画面全体を覆ってください。前面のウィンドウのサイズを変更して背面のウィンドウをタップできるようにする方法は採れません（ただし、妥協案としては考えています）
  * 前面のウィンドウを背面のウィンドウのサブビューにすることはできません。同じ理由で、ウィンドウを使用せず、UIViewをルートビューの最前面に貼付ける、といった別方向からの解決法は採れません
  * 実使用環境ではどちらのウィンドウにもrootViewControllerがセットされ、ルートビュー以下に多くのサブビューが追加されます。テストコードでは焦点を明確にするためにウィンドウのみを扱っています

# 現在じぶんが取り組んでいる方向性

レスポンダーチェーンの流れに沿ってどのビューもイベントを処理せずにUIWindowへイベントが到着したら、ウィンドウ自身も処理をせずUIApplicationへイベントを送るところを捕まえて別のUIWindowへ送信できないか、と考えています。

**解決法のひとつとして探っているものの図解**

![UIWindowから別のUIWindowへイベントを伝搬](https://i.stack.imgur.com/hiw6q.png)


CC BY-SA 3.0
2015-03-11T21:34:33.410
7805
2015-03-13T18:34:09.463
2020-06-17T08:14:45.997
-1
question
ios objective-c
UIWindowから別のUIWindowへタッチイベントを伝搬したい


とりあえず下記のようなコードでそれっぽく出来ますがどうでしょうか？  
もう少しやりたいことが具体的に分かると回答し易くなります。  
色とかは分かり易いものに変えています。  
RootViewControllerは適当に用意してください。

[code]

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
        self.window.restorationIdentifier = @"back window";
        self.window.backgroundColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:0.5f];
        [self.window addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(didTap:)]];
        RootViewController* vc = [[RootViewController alloc] init];
        self.window.rootViewController = vc;
        [self.window makeKeyAndVisible];
    
        self.secondWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
        self.secondWindow.restorationIdentifier = @"front window";
        self.secondWindow.backgroundColor = [UIColor colorWithRed: 1.0f green: 0 blue: 0 alpha: 0.5f];
    
        // (1)
        [self.secondWindow addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(didTap:)]];
    
        self.secondWindow.windowLevel = self.window.windowLevel + 1;
        self.secondWindow.hidden = YES;
        self.secondWindow.userInteractionEnabled = NO;
        vc = [[RootViewController alloc] init];
        self.secondWindow.rootViewController = vc;
    
        return YES;
    }
    
    - (void)didTap:(UITapGestureRecognizer *)tap {
    
        NSLog(@"Did tap %@.", tap.view.restorationIdentifier);
    
        UIWindow* frontTargetWindow = (tap.view == self.window)?self.secondWindow:self.window;
        tap.view.userInteractionEnabled = NO;
        tap.view.hidden = YES;
        frontTargetWindow.hidden = NO;
        frontTargetWindow.userInteractionEnabled = YES;
        [frontTargetWindow makeKeyAndVisible];
    }
    
[/code]


CC BY-SA 3.0
2015-03-12T16:51:44.377
7847
2015-03-12T16:51:44.377
7364
7805
answer


**UPDATE:**
レスポンダチェーンを使う意義はほぼありません。それよりも、`UIWindow`が重なっていても、ヒットテストが正しく実装されていれば、ヒットテストビューを背面のウインドウにすることが可能です。それが一番手っ取り早いのではないでしょうか。

[code]

    class AnotherWindow : UIWindow {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        var hitTestView = super.hitTest(point, withEvent: event)
            return hitTestView === self ? nil : hitTestView;
        }
    }
    
[/code]

これは非常に単純な例です。ヒットテストの結果、「前面のウィンドウ自身が結果として取得された場合」、に`nil`に置き換え、背面のウインドウに移譲します。この実装では期待する要件に合わないかもしれません。しかし指定が非常にふわふわしているので、実現方法そのものは存在するという点についてだけ言及させて頂きます。

> ヒットテストについて理解しても、ヒットテストの性質上「できない」という答えしか見つかりません。「できない」という結論しか導きだせない

いや、そんなことはない、不可能だというのであれば、その根拠をお願いします。「 **ヒットテストの性質上「できない」という答え**
」がどこにあるのか、私気になります。

* * *

以下の古い記述は、「UIWindowから別のUIWindowへタッチイベントを伝搬したい」という質問文に対して、

  * 異なる`UIWindow`間での座標変換をどうするのか
  * タッチイベントは転送できたとして、そこからどう`UIView`のイベントの発火に繋げるのか
  * 放棄されるイベントを回収して、再度実行するのは無限ループの可能性が高いがどのような対処を考えているのか
  * 「タッチイベント」と「タップジェスチャ」の違いを認識しているのか

など突っ込みどころが多かったので、「無理ですよ」に留めた解答です。

* * *

「`AppDelegate`は`UIResponder`に準拠している」のですから、タッチイベントの転送はたとえば`touchesBegin()`を呼び出して終わりです。

しかし`UIApplicationDelegate`実装クラスはレスポンダチェーンの末尾にいるため、ここから別の`UIWindow`に再転送しても無限ループが発生するだけです。

**レスポンダチェーンはヒットテストビューから上位方向へしか辿りません**
、別の`UIWindow`へ転送したとして、どこかのビューが反応することはありません。

[code]

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
    
        // 省略
    
        override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
            if let touch = touches.anyObject() as? UITouch {
                if(touch.window !== self.window){        
                    //keyWindow座標に変換して赤い点を描く（合ってるかあまり自信ない）
                    let location = touch.locationInView(nil)
                    let convertedLocation = self.window!.convertPoint(location, fromWindow: touch.window)
                    let pointer = UIView(frame : CGRect(origin: convertedLocation, size: CGSize(width: 1, height: 1)))
                    pointer.backgroundColor = UIColor.redColor();
                    self.window!.addSubview(pointer)
    
                    // getterしかないプロパティを書き換えるのにKVCを利用
                    // 処理されないイベントが無限ループしてStackOverflowになります
                    touches.setValue(self.window!, forKey: "window")
                    // タッチ開始を別ウインドウに渡す
                    self.window!.touchesBegan(touches, withEvent: event)
                }
            }
        }
    }
    
[/code]


CC BY-SA 3.0
2015-03-13T13:04:24.833
7883
2015-03-13T18:34:09.463
2015-03-13T18:34:09.463
5337
5337
7805
answer