FlutterでiOSのネイティブの画面をモーダル表示したい時、FlutterViewControllerからiOSのUIViewControllerをpresent()していたのだが、この際ハンドラが定義されてない部分タップすると後ろに隠れてるFlutter側のイベントが発火する挙動に困ってた。↓こういうの
問題の再現
- サンプルコードを作成して確認
- https://github.com/maeharin/flutter_ios_present_sample
- v1.7.8+hotfix.3(2019/7時点最新版のstable)とv1.5.4-hotfix.2で再現すること確認
原因調査
FlutterViewControllerの実装(engineのv1.5.4-hotfixesブランチ)を見ると、以下のようにtouchesBeganメソッド等の4メソッドをoverrideしており、ここからFlutter側にイベントを伝播させていることがわかる
https://github.com/flutter/engine/blob/v1.5.4-hotfixes/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L649-L663
touchesBeganがどう呼び出されるかについては以下のappleのドキュメントを参照。iOS側のtouchイベントはResponder Chainを駆け上っていくようになっている
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events
今回の挙動に関係ある部分をまとめると
- タッチイベントなら、タッチされたビューがファーストレスポンダとなる
- 各ビューのnextプロパティにresponder chain上の次のレスポンダが入っているので、それで確認できる
- UIViewオブジェクト
- ビューがView Controllerのルートビューの場合、次のレスポンダはView Controller
- それ以外の場合、次のレスポンダはビューのスーパービュー
- UIViewController オブジェクト
- View Controllerのビューがウィンドウのルートビューの場合、次のレスポンダはウィンドウオブジェクト
- View Controllerが別のView Controllerによって表示されている場合、次のレスポンダは表示側のView Controller
つまり、今回のケースは以下のようになっていた
- イベントハンドラがないUIの部分をタップする
- タップイベントがiOSのResponder Chainを駆け上っていき、FlutterViewControllerのtouchesBegan()まで伝播
- Flutter側のイベントハンドラが呼び出される

解決策
FlutterViewControllerのtouchesBegan()等が呼び出されないようにすればよい。以下2つの方法が考えられる
1. FlutterViewControllerからpresent()したUIViewControllerのtouchesBegan等の4メソッドをoverrideする
FlutterViewControllerからpresentされるUIViewControllerに以下を追記する。これにより、FlutterViewControllerにアンハンドルなイベントを伝播しないようにする
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {}
参考:UIKitのヘッダファイルの該当箇所
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
2. FlutterViewControllerからpresent()するのをやめる
iOS側のrootViewControllerをFlutterViewControllerではなくNavigationControllerにするなどして、FlutterViewControllerからUIViewControllerを直接present()しないようにする。
rootViewControllerを変更する方法はこちらの記事が参考になる
https://qiita.com/najeira/items/fa52ca4a9f544ae08300