はじめに (対象読者・この記事でわかること)
この記事は、SwiftUIでアプリを開発していて「ViewにTapGestureを付けたら、なぜかUITabBarがタップできなくなった!」という現象に悩んでいる方を対象にしています。
SwiftUIのGestureシステムとUIKitのUITabBarが混在する場面で起こる、タッチイベントの奪い合い(ガベージ)を解消する方法を、実装コードとともに解説します。
記事を読み終えると、単純に.onTapGestureを付けるだけでなく、なぜTabBarが反応しなくなるのかを理解し、simultaneousGesture(delegate:)を使った確実な回避策を実装できるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - SwiftUIの基本的なView構築(VStack, Button, .onTapGesture) - UIKitのUITabBarControllerとUITabBarの存在を知っていること - Delegateパターンの基礎(UITabBarDelegateなど)
なぜTapGestureを付けるとTabBarが反応しなくなるのか
SwiftUIで.onTapGestureをViewに付与すると、SwiftUIのGestureDispatcherがそのViewを「タッチイベントの最終到達点」と認識します。
UIKit側のUITabBarは、タッチイベントをhitTest(_:with:)で探索する際、すでにSwiftUIのGestureがイベントを「消費」してしまっているため、タップを検出できません。
これは「ガベージコレクション」ではなく「ガベージ(奪い取り)」と呼ばれる現象で、特にTabViewの上に半透明のOverlayやSheetの背景を置いてTapGestureで閉じる処理を入れると、100%再現します。
具体的な手順と実装:delegateを使った確実な回避策
ステップ1:カスタムGestureDelegateを用意する
まず、UIGestureRecognizerDelegateのgestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)を返す専用のクラスを用意します。
これにより、SwiftUIのTapGestureとUITabBarのタップを「同時認証」させます。
Swiftimport UIKit import SwiftUI /// SwiftUIのTapGestureとUITabBarのタップを共存させるDelegate final class AllowSimultaneousTapDelegate: NSObject, UIGestureRecognizerDelegate { static let shared = AllowSimultaneousTapDelegate() // 同時認証を許可 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // UITabBarのクラスかどうかを判定 if let view = otherGestureRecognizer.view, String(describing: type(of: view)).contains("TabBar") { return true } return false } }
ステップ2:SwiftUIのGestureをDelegate付きに差し替える
.onTapGestureは使わず、UIKitのUITapGestureRecognizerをUIViewRepresentableでラップし、delegateを接続します。
Swiftstruct SimultaneousTapGesture: UIViewRepresentable { let action: () -> Void func makeUIView(context: Context) -> UIView { let view = UIView(frame: .zero) let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handle)) tap.delegate = AllowSimultaneousTapDelegate.shared view.addGestureRecognizer(tap) return view } func updateUIView(_ uiView: UIView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(action: action) } final class Coordinator: NSObject { let action: () -> Void init(action: @escaping () -> Void) { self.action = action } @objc func handle() { action() } } }
使い方は簡単で、TapGestureを付けたいViewに.overlayで乗せるだけ:
Swiftstruct ContentView: View { var body: some View { TabView { Color.red .overlay( SimultaneousTapGesture { print("Overlay tapped") // ここが反応 } ) .tabItem { Label("Red", systemImage: "paintbrush") } Color.blue .tabItem { Label("Blue", systemImage: "paintbrush.fill") } } } }
ハマった点とエラー解決
最初は.onTapGestureに.simultaneousGestureを付けてもTabBarが反応しませんでした。
これはSwiftUIのGestureはUIKitのUIGestureRecognizerをラップしておらず、UITabBarの内部で使われているUITabBarGestureRecognizerと「同一レベル」で認証できないためです。
また、SwiftUI 5.0時点では.onTapGestureに直接delegateを指定するAPIが存在しないため、UIKitの gestureRecognizerを使わざるを得ませんでした。
解決策
結局、UIKitのUITapGestureRecognizerを使い、delegateをカスタマイズすることで、UITabBarのタップとSwiftUIのTapGestureを同時に認証させることに成功しました。
この方法なら、iOS 15〜17までの全バージョンで動作確認済みです。
まとめ
本記事では、SwiftUIでViewにTapGestureを付けるとUITabBarが反応しなくなる原因と、UIKitのdelegateを使った確実な回避策を解説しました。
- SwiftUIの
.onTapGestureはUITabBarのタップイベントを「消費」してしまう UIGestureRecognizerDelegateのshouldRecognizeSimultaneouslyWithを活用すれば両立可能- UIViewRepresentableでラップすることで、SwiftUIでも簡単に再利用できる
この手法を使えば、Sheetの背景タップで閉じつつ、TabBarも正常にタップできる、ユーザーに優しいアプリを実装できます。
次回は、同様の問題がUIScrollViewとSwipeGestureで起こった場合の共存方法を掘り下げていきます。
参考資料
- Apple Developer Forums - TabBar not responding after adding tap gesture
- SwiftUIのGestureとUIKitの共存について - Qiita
- UITabBarController Class Reference
