0
1

【SwiftUI】インジケーターを最前面に表示する方法

Last updated at Posted at 2024-08-16

はじめに

最近、SwiftUIで開発していたアプリで、アクティビティインジケーターを最前面に表示したいという場面がありました。
アプリについては以下からダウンロードできますので、興味があればぜひダウンロードしてみてください。

App Storeのリンク

アプリアイコン

問題となったのは、以下のように、インジケーターを表示するとナビゲーションバーやタブバーが覆われずに表示されてしまうことです。
ZStackライブラリ などを使用して簡単に実装できそうに思えて、実はうまくいかないことがあります。

パッケージ使用前

インジケーターを最前面に表示したいのに、ナビゲーションバーやタブバーが覆われずに表示されてしまい、その場合、 バー部分が操作可能 な状態になってしまうため、今回は、このような悩みを解決するため、インジケーターを最前面に正しく表示し、ユーザーの操作を一時的に無効化する方法を解説します。

後述しますが、本記事の解決策でインジケーターを最前面に表示するためのパッケージを作成しています。

原因

SwiftUIは構造体を使用して、階層構造でビューを構築します。そのため、子ビュー(例えば、AnalyticsView)で表示したインジケーターは、その子ビューの中でのみ表示され、親ビュー(TabViewNavigationView)には表示されません。

MainView {
    TabView {
        NavigationView {
            HomeView()
        }
        NavigationView {
            AnalyticsView() {
                IndicatorView() // ❌ 子ビュー内でのみ表示される
            }
        }
    }
}

そのため、子ビュー内で表示されたインジケーターは、ナビゲーションバーやタブバーを覆わずに表示されてしまいます。

解決策

その1. ZStackを親ビューに配置する

インジケーターを表示させたい親ビューの上に、ZStackを使用してインジケーターを重ねて表示します。

globalObservableObjectを継承したクラスで、isVisibleはインジケーターの表示状態を管理する@Publishedプロパティとします。この方法では、インジケーターがナビゲーションバーやタブバーの下に隠れず、親ビューの最前面に表示されます。

MainView {
    ZStack {
        TabView {
            NavigationView {
               HomeView()
            }
            NavigationView {
                AnalyticsView()
            }
        }

        if global.isVisible {
            IndicatorView() // ✅ 親ビューの上に表示される
        }
    }
}

この方法では各画面がインジケーター表示のために親のビューに依存してしまいます。
個人的には各画面ごとに状態を管理して、なるべくビューが分割された実装が好きなので、グローバルオブジェクトを使わずに、各画面がそれぞれの状態を管理 し、必要に応じてインジケーターを最前面に表示する方法を検討することにしました。

その2. 最前面の画面を取得してインジケーターを表示する

SwiftUI単体では、画面の最前面のビューを取得することはできませんが、UIKitの UIApplicationUIWindow を使うことで、最前面にある画面を取得できます。

この方法を使って、最前面のビューの上にインジケーターを表示することが可能です。

以下は、パッケージの内容を一部抜粋した、最前面のビューコントローラを取得する方法です。これにより、タブビューのコントローラや、ナビゲーションビューのコントローラを取得できます。

  • 現在アクティブなビューコントローラを取得する
    現在アクティブというのは、例えば、タブバーの選択されているビューコントローラや、ナビゲーションコントローラの最初にプッシュされたビューコントローラなどです。
// 現在アクティブなビューコントローラを取得
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
      let rootViewController = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController else {
    return
}
  • 最前面のビューコントローラを取得する

プレゼンテッドビュー(モーダルやポップアップ)がある場合は、画面遷移群が本流から外れるため、最前面のビューコントローラを取得するために 再帰的 にビューコントローラを取得して、最前面のビューコントローラを取得するアプローチを取っています。

// 最前面のビューコントローラを取得
let topViewController = findTopViewController(rootViewController)

/// 最前面のビューコントローラを見つけるヘルパーメソッド
/// - Parameter rootViewController: ルートビューコントローラ
/// - Returns: 最前面のビューコントローラ
func findTopViewController(_ rootViewController: UIViewController) -> UIViewController {
    // プレゼンテッドビューコントローラがある場合は再帰的に呼び出す
    if let presentedViewController = rootViewController.presentedViewController {
        return findTopViewController(presentedViewController)
    }

    // ナビゲーションコントローラーがある場合は、最後にプッシュされたビューコントローラを返す
    if let navigationController = rootViewController as? UINavigationController {
        return navigationController.visibleViewController ?? navigationController
    }

    // タブバーコントローラーがある場合は、選択されているビューコントローラを返す
    if let tabBarController = rootViewController as? UITabBarController {
        return tabBarController.selectedViewController ?? tabBarController
    }

    return rootViewController
}

この方法を使えば、どこの画面からインジケーターを表示しても、最前面のビューコントローラを見つけることができます。あとは、このビューコントローラのビューを取得し、その上にインジケーターを表示することで、ナビゲーションバーやタブバーの上にインジケーターを配置できます。

これにより、子画面が親画面に依存することなく、インジケーターを表示させることができます。

パッケージ使用後

詳細な実装については、パッケージ化したソースをご参照ください。

パッケージ化

上記の方法をパッケージ化して使用できるようにしました。

画面にモディファイアを追加することで、簡単にインジケーターを最前面に表示することができます。
インジケーターの表示・非表示を管理するための @State プロパティは画面ごとに持つことができます。

使い方

  1. SPMでパッケージを追加します

  2. 各画面にモディファイアを追加して表示します

@State var isVisible = false

AnayticsView()
    .activityIndicator(isVisible: $isVisible,
                       backgroundColor: .gray.withAlphaComponent(0.5),
                       indicatorColor: .white)

パラメーターは以下の通りです。

パラメーター 説明 デフォルト
isVisible Binding<Bool> アクティビティインジケーターの表示状態を管理する変数 -
backgroundColor UIColor オーバーレイの背景色 .clear
indicatorColor UIColor アクティビティインジケーターの色 .darkGray

README.mdにも使い方が記載されていますので、そちらのコードをコピペすれば、タブバーやナビゲーションバーの上にインジケーターが表示されることを確認できます。

README.mdに記載されているサンプルコード
import ForegroundActivityIndicator // パッケージをインポート
import SwiftUI

struct ContentView: View {
    @State var isShowingActivityIndicator = false // アクティビティインジケーターの表示状態

    var body: some View {
        TabView {
            NavigationView {
                VStack(spacing: 20) {
                    Image(systemName: "1.circle.fill")
                        .resizable()
                        .frame(width: 100, height: 100)

                    Button(action: {
                        isShowingActivityIndicator = true

                        // 3秒後に非表示
                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                            isShowingActivityIndicator = false
                        }
                    }, label: {
                        Text("表示")
                            .padding(.vertical, 5)
                            .padding(.horizontal, 40)
                            .foregroundColor(.white)
                            .background(Color.blue)
                            .cornerRadius(5)
                    })
                }
                .activityIndicator(isVisible: $isShowingActivityIndicator, backgroundColor: .gray.withAlphaComponent(0.5), indicatorColor: .white) // モディファイアを適用
                .navigationTitle("画面1")
                .toolbarBackground(.visible, for: .navigationBar)
                .toolbarBackground(Color.blue, for: .navigationBar)
                .toolbarColorScheme(.dark, for: .navigationBar)
                .navigationBarTitleDisplayMode(.inline)
            }
            .tag(0)
            .tabItem { Label("One", systemImage: "1.circle") }

            NavigationView {
                VStack {
                    Image(systemName: "2.circle.fill")
                        .resizable()
                        .frame(width: 100, height: 100)
                }
            }
            .tag(1)
            .tabItem { Label("Two", systemImage: "2.circle") }
        }
    }
}

まとめ

今回は、インジケーターを最前面に表示する方法をご紹介しました。モディファイアとして実装することで、ビューの構造をシンプルに保つことができます。また、画面間の依存を減らすことができるため、コードの保守性も向上します。

0
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
0
1