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

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

インジケーターを最前面に表示したいのに、ナビゲーションバーやタブバーが覆われずに表示されてしまい、その場合、 バー部分が操作可能 な状態になってしまうため、今回は、このような悩みを解決するため、インジケーターを最前面に正しく表示し、ユーザーの操作を一時的に無効化する方法を解説します。
後述しますが、本記事の解決策でインジケーターを最前面に表示するためのパッケージを作成しています。
原因
SwiftUIは構造体を使用して、階層構造でビューを構築します。そのため、子ビュー(例えば、AnalyticsView
)で表示したインジケーターは、その子ビューの中でのみ表示され、親ビュー(TabView
やNavigationView
)には表示されません。
MainView {
TabView {
NavigationView {
HomeView()
}
NavigationView {
AnalyticsView() {
IndicatorView() // ❌ 子ビュー内でのみ表示される
}
}
}
}
そのため、子ビュー内で表示されたインジケーターは、ナビゲーションバーやタブバーを覆わずに表示されてしまいます。
解決策
その1. ZStackを親ビューに配置する
インジケーターを表示させたい親ビューの上に、ZStack
を使用してインジケーターを重ねて表示します。
global
はObservableObject
を継承したクラスで、isVisible
はインジケーターの表示状態を管理する@Published
プロパティとします。この方法では、インジケーターがナビゲーションバーやタブバーの下に隠れず、親ビューの最前面に表示されます。
MainView {
ZStack {
TabView {
NavigationView {
HomeView()
}
NavigationView {
AnalyticsView()
}
}
if global.isVisible {
IndicatorView() // ✅ 親ビューの上に表示される
}
}
}
この方法では各画面がインジケーター表示のために親のビューに依存してしまいます。
個人的には各画面ごとに状態を管理して、なるべくビューが分割された実装が好きなので、グローバルオブジェクトを使わずに、各画面がそれぞれの状態を管理 し、必要に応じてインジケーターを最前面に表示する方法を検討することにしました。
その2. 最前面の画面を取得してインジケーターを表示する
SwiftUI単体では、画面の最前面のビューを取得することはできませんが、UIKitの UIApplication や UIWindow を使うことで、最前面にある画面を取得できます。
この方法を使って、最前面のビューの上にインジケーターを表示することが可能です。
以下は、パッケージの内容を一部抜粋した、最前面のビューコントローラを取得する方法です。これにより、タブビューのコントローラや、ナビゲーションビューのコントローラを取得できます。
- 現在アクティブなビューコントローラを取得する
現在アクティブというのは、例えば、タブバーの選択されているビューコントローラや、ナビゲーションコントローラの最初にプッシュされたビューコントローラなどです。
// 現在アクティブなビューコントローラを取得
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
プロパティは画面ごとに持つことができます。
使い方
-
SPMでパッケージを追加します
-
各画面にモディファイアを追加して表示します
@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") }
}
}
}
まとめ
今回は、インジケーターを最前面に表示する方法をご紹介しました。モディファイアとして実装することで、ビューの構造をシンプルに保つことができます。また、画面間の依存を減らすことができるため、コードの保守性も向上します。