5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIでナビゲーションバー非表示でもスワイプバックを有効にする

Last updated at Posted at 2025-12-14

この記事は、GENDA Advent Calendar 2025 シリーズ1 Day15 の記事です。

はじめに

SwiftUIアプリで、ナビゲーションバーを非表示にしたらスワイプバック(画面左端からスワイプして前の画面に戻る機能)が動かなくなる問題に遭遇したことはありませんか?

この記事では、SwiftUIビューでスワイプバック機能を有効化する SwipeBackEnabler の実装と、その背景にある技術的な課題を解説します。

問題の発生

UIKitのUINavigationControllerには、画面左端からスワイプして前の画面に戻る「スワイプバック」機能が標準で備わっています。しかし、ナビゲーションバーを非表示にすると、この機能が自動的に無効化されてしまうという挙動があります。

例えば、こんなカスタムヘッダーのビューを実装するとき

struct ContentView: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        ZStack {
            Image("hero")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .ignoresSafeArea()

            VStack {
                // カスタムヘッダー
                HStack {
                    Button { dismiss() } label: {
                        Image(systemName: "chevron.left")
                            .foregroundColor(.white)
                            .padding()
                    }
                    Spacer()
                }
                Spacer()
            }
        }
        .navigationBarHidden(true)  // ナビゲーションバーを非表示
    }
}

SwiftUIで.navigationBarHidden(true)を設定すると、内部的にUIKitのnavigationController.isNavigationBarHidden = trueが呼ばれます。

ナビゲーションバーを非表示にすると、システムがinteractivePopGestureRecognizer.isEnabledfalseに設定するため、スワイプバックが無効化されます。

iOSユーザーは画面端からスワイプして戻る操作に慣れているので不便に思うことも多いと思います。

デモ

SwipeBackEnablerの有無による違いを動画で確認できます。

❌ SwipeBackEnablerなし

swipeback-without.gif

ナビゲーションバーを非表示にした状態では、画面左端からスワイプしても戻ることができません。

✅ SwipeBackEnablerあり

swipeback-with.gif

.enableSwipeBack()を追加すれば、ナビゲーションバーを非表示にしていても、スワイプバックで前の画面に戻れます。

解決のアプローチ

この問題を解決するため、UIViewControllerRepresentableを使った SwipeBackEnabler を実装しました。

実装のポイント

  1. UIGestureRecognizerDelegateの設定

    • interactivePopGestureRecognizerのdelegateを適切に設定(自前のVCに任せる)
  2. ライフサイクルの管理

    • ViewControllerのライフサイクルに合わせて有効化/元の状態へ復元
  3. 元のdelegateの保存と復元

    • delegateisEnabled を保存し、他画面への影響を防ぐ

制約・リスク

  • NavigationViewまたはNavigationStackを使用している必要があります(内部的にUINavigationControllerを使用するため)
  • カスタムナビゲーション実装では動作しません
  • エラーが発生することはありませんが、条件が満たされない場合は単に何も行いません

SwipeBackEnablerの実装

完成したコードがこちら

import SwiftUI
import UIKit

struct SwipeBackEnabler: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        SwipeBackEnablerViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

private class SwipeBackEnablerViewController: UIViewController, UIGestureRecognizerDelegate {
    private weak var originalDelegate: UIGestureRecognizerDelegate?
    private var originalIsEnabled: Bool?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        enableSwipeBack()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        restoreOriginalDelegate()
    }

    deinit {
        restoreOriginalDelegate()
    }

    private func enableSwipeBack() {
        guard let navigationController,
              let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
        if originalDelegate == nil {
            originalDelegate = gestureRecognizer.delegate
        }
        if originalIsEnabled == nil {
            originalIsEnabled = gestureRecognizer.isEnabled
        }
        gestureRecognizer.delegate = self
        gestureRecognizer.isEnabled = true
    }

    private func restoreOriginalDelegate() {
        guard let navigationController,
              let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
        if let originalDelegate {
            gestureRecognizer.delegate = originalDelegate
        }
        if let originalIsEnabled {
            gestureRecognizer.isEnabled = originalIsEnabled
        }
    }

    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        navigationController?.viewControllers.count ?? 0 > 1
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // スワイプバック用のジェスチャーのみ同時認識を許可
        gestureRecognizer is UIScreenEdgePanGestureRecognizer
    }
}

extension View {
    func enableSwipeBack() -> some View {
        background(
            SwipeBackEnabler()
                .frame(width: 0, height: 0)
                .opacity(0)
        )
    }
}

コードの解説

1. UIGestureRecognizerDelegateの適用先を自前VCに一本化

private class SwipeBackEnablerViewController: UIViewController, UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        navigationController?.viewControllers.count ?? 0 > 1
    }
    ...
}

gestureRecognizerShouldBeginメソッドで、スワイプバックジェスチャーを開始すべきかを判定します。

ナビゲーションスタックの判定

  • navigationController.viewControllers.count が2つ以上ある場合のみスワイプバックを許可(ルート画面では無効)

2. 同時認識の許可

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    // スワイプバック用のジェスチャーのみ同時認識を許可
    gestureRecognizer is UIScreenEdgePanGestureRecognizer
}

スワイプバック用のジェスチャー(UIScreenEdgePanGestureRecognizer)のみ、他のジェスチャー(ScrollViewのスクロールなど)と同時に認識できるようにします。これにより、不要なジェスチャーの干渉を防ぎます。

3. SwipeBackEnablerViewControllerのライフサイクル管理

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    enableSwipeBack()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    restoreOriginalDelegate()
}

deinit {
    restoreOriginalDelegate()
}

enableタイミング

  • viewWillAppear: 画面が表示される直前に有効化(毎回ここで十分なため1か所に集約)

viewWillDisappearでの復元

  • 画面が消える際に、元のdelegateとisEnabledを復元
  • 他の画面に影響を与えないようにする

deinitでの復元

  • ViewControllerが予期せず破棄される場合でも、確実に元の状態に復元
  • viewWillDisappearが呼ばれないケースへの対応

4. 元のdelegateの保存と復元

private func enableSwipeBack() {
    guard let navigationController,
          let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
    if originalDelegate == nil {
        originalDelegate = gestureRecognizer.delegate
    }
    if originalIsEnabled == nil {
        originalIsEnabled = gestureRecognizer.isEnabled
    }
    gestureRecognizer.delegate = self
    gestureRecognizer.isEnabled = true
}

private func restoreOriginalDelegate() {
    guard let navigationController,
          let gestureRecognizer = navigationController.interactivePopGestureRecognizer else { return }
    if let originalDelegate {
        gestureRecognizer.delegate = originalDelegate
    }
    if let originalIsEnabled {
        gestureRecognizer.isEnabled = originalIsEnabled
    }
}

delegateを自前に差し替える理由

  • ナビゲーションバー非表示時でもスワイプバックを許可するため、interactivePopGestureRecognizer.delegate を自分で実装したVCに差し替える
  • 差し替え前の delegate と isEnabled は保存し、画面離脱時に復元することで他の画面に影響を与えない

使い方

SwiftUIのViewに.enableSwipeBack()モディファイアを追加するだけです

struct ContentView: View {
    var body: some View {
        VStack {
            // コンテンツ
        }
        .background(Asset.Color.bgBaseMain.swiftUIColor)
        .enableSwipeBack()  // これだけ!
    }
}

補足

  • スワイプバックを有効にしたい画面に追加してください
  • NavigationStack(iOS 16+)、NavigationViewのどちらでも使用可能です

モディファイアの実装

extension View {
    func enableSwipeBack() -> some View {
        background(
            SwipeBackEnabler()
                .frame(width: 0, height: 0)
                .opacity(0)
        )
    }
}

backgroundに透明なViewを配置する理由

  • UIViewControllerRepresentableは、Viewツリーに組み込まれる必要がある
  • サイズ0、不透明度0にすることで、視覚的には影響を与えない
  • ライフサイクルイベントは正しく発火する

なぜこの実装が必要なのか

UINavigationControllerのスワイプバック制約

UINavigationControllerは以下の条件でスワイプバックを無効化します

  1. カスタムバックボタンの存在

    // UIKit
    navigationItem.leftBarButtonItem = UIBarButtonItem(...)
    
    // SwiftUI
    .navigationBarBackButtonHidden(true)
    .toolbar {
        ToolbarItem(placement: .navigationBarLeading) {
            Button("戻る") { /* pop */ }
        }
    }
    
  2. delegateが設定されている

    navigationController.interactivePopGestureRecognizer?.delegate = someDelegate
    
  3. NavigationBarが非表示(今回のケース)

    // UIKit
    navigationController.isNavigationBarHidden = true
    
    // SwiftUI
    .navigationBarHidden(true)
    

特に3番目のケースは、全画面表示やカスタムヘッダーを実装する際によく遭遇します。ナビゲーションバーを非表示にしつつ、スワイプバックでの戻る操作は維持したいケースでも、UINavigationControllerの標準仕様では両立できません。

他のアプローチとの比較

UINavigationController.appearanceでの設定

UINavigationController.appearance().interactivePopGestureRecognizer?.isEnabled = true

→ アプリ全体のすべてのUINavigationControllerに影響を与えるため、スワイプバックを無効にしたい画面でも有効になってしまう

まとめ

UINavigationControllerの仕様でスワイプバックが無効化される問題を.enableSwipeBack() を追加することで解決できます。

同じ問題で困っている方の参考になれば:christmas_tree:

参考

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?