ナビゲーションバーの背景色・文字色を変更する

SwiftUI では toolbarBackground を使用してナビゲーションバーの背景色をカスタマイズすることができますが、この API は iOS 16+ からしか利用できず、かつ文字色を自由に変更する API は iOS 17 時点でも提供されていません。

この記事では SwiftUI-Introspect を利用して、裏にある UINavigationController にアクセスし、ナビゲーションバーの背景色・文字色を変更する方法について紹介します。

iOS 16+ からのナビゲーションバーのカスタマイズ

iOS 16+ からは以下のモディファイアを使用してナビゲーションバーのカスタマイズをすることが可能になりました。

以下は背景色をオレンジに、カラースキームをダークモードに設定する例です。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink {
                    Text("Hello")
                        .navigationTitle("SwiftUI")
                } label: {
                    Text("SwiftUI")
                }
            }
            .navigationTitle("Sunabalab")
            .navigationBarTitleDisplayMode(.inline)
            .toolbarBackground(Color.orange, for: .navigationBar) // 背景色を変更
            .toolbarBackground(.visible, for: .navigationBar)     // 背景色を常に適用
            .toolbarColorScheme(.dark, for: .navigationBar)       // ダークモードの配色を適用(文字色が白に)
        }
    }
}


これらの API はシンプルで扱いやすいですが、以下が課題になるケースもあります。

  1. iOS 16+ からしか利用できない
  2. 文字色を自由にカスタマイズできない

後者は SwiftUI の設計方針である可能性もありますが、アプリによっては UIKit のように自由な文字色を設定したいケースもあるでしょう。

SwiftUI Introspect を使って UIKit コンポーネントにアクセスする

SwiftUI-Introspect は SwiftUI の裏側にある UIKit コンポーネントへアクセスする OSS ライブラリです。

README では ScrollView の裏にある UIScrollView へアクセスする例として以下が記載されています。(最近のリリースで導入された新しい API 形式でのコード例になっています)

ScrollView {
    Text("Item 1")
        .introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17), scope: .ancestor) { scrollView in
            // do something with UIScrollView
        }
}

SwiftUI は裏側では UIKit のコンポーネントを利用して実現されているものが多く、SwiftUI 側で API が不足していても、直接 UIKit にアクセスすることで解決できるケースが多いかと思います。

SwiftUI-Introspect を使用すると、基本的なコードベースとしては SwiftUI を使いつつ、必要な場面でのみ最低限 UIKit にアクセスするというコードになるので、生産性・メンテナンス性の面からバランスが取れるケースが多いと個人的には感じています。

ナビゲーションバーの背景色・文字色をカスタマイズする

まず、UINavigationController の extension として背景色・文字色を変更する関数を作成しておきます。

private extension UINavigationController {
    func configureNavigationBarColor(foreground: Color, background: Color) {
        let foreground = UIColor(foreground)
        let background = UIColor(background)

        // Special Thanks:
        // https://reigle.info/entry/2022/07/20/100000

        // ナビゲーションバーの背景色
        navigationBar.barTintColor = background
        // ナビゲーションバーのアイテムの色 (戻る < とか 読み込みゲージとか)
        navigationBar.tintColor = foreground
        // ナビゲーションバーのテキストを変更する
        navigationBar.titleTextAttributes = [.foregroundColor: foreground]
        // ナビゲーションバーの透過禁止
        navigationBar.isTranslucent = false
        navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
        // 下線非表示
        navigationBar.shadowImage = UIImage()

        if #available(iOS 15.0, *) {
            let appearance = UINavigationBarAppearance()
            // 背景色
            appearance.titleTextAttributes = [.foregroundColor: foreground]
            appearance.backgroundColor = background

            // ナビゲーションバーに反映
            navigationBar.standardAppearance = appearance
            navigationBar.scrollEdgeAppearance = appearance
        }
    }
}

コード中にもコメントしていますが、このカスタマイズコードは以下の記事を参考にさせていただきました。

SwiftでUINavigationBarの色が設定されなくなった場合、下線を非表示にしたい場合(iOS15以上)

次に View の extension として SwiftUI-Introspect を使用して UINavigationController にアクセスし、前述の関数を呼び出す関数を追加します。

extension View {
    @ViewBuilder
    func navigationBarColor(foreground: Color, background: Color) -> some View {
        // 記述を簡略化する目的でローカル関数を導入
        let apply: (UINavigationController) -> Void = {
            $0.configureNavigationBarColor(foreground: foreground, background: background)
        }

        if #available(iOS 16, *) {
            introspect(.navigationStack, on: .iOS(.v16, .v17), scope: .ancestor, customize: apply)
        } else {
            introspect(.navigationView(style: .stack), on: .iOS(.v13, .v14, .v15), scope: .ancestor, customize: apply)
       }
    }
}

ここでは iOS 16+ では NavigationStack 、それ未満では NavigationView を使用している想定で introspect の対象を .navigationStack.navigationView(style: .stack) で変更しています。通常のプロジェクトでこれらを混在させるケースは少ないと思うので、必要に応じて変更すると良いでしょう。

最後に、実際の NavigationStack、NavigationView に適用するコードです。

struct ContentView: View {
    var body: some View {
        if #available(iOS 16, *) {
            NavigationStack {
                content()
            }
        } else {
            NavigationView {
                content()
            }
        }
    }

    func content() -> some View {
        List {
            NavigationLink {
                Text("Hello")
                    .navigationTitle("SwiftUI")
            } label: {
                Text("SwiftUI")
            }
        }
        .navigationTitle("Sunabalab")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarColor(foreground: .red, background: .yellow) // ✅ 文字色:赤、背景色:黄
    }
}


まとめ

この記事では SwiftUI-Introspect を使用して、裏側にある UIKit コンポーネントである UINavigationController にアクセスし、ナビゲーションバーの背景色・文字色を変更する方法を紹介しました。

”裏側にある UIKit にアクセスする” と聞くと難しく聞こえますが、SwiftUI-Introspect を活用することで簡単に扱えると感じられたのではないでしょうか。

今回の例に限らず、SwiftUI-Introspect は SwiftUI に不足した API を補う上で強力な武器になることが多いので、UIKit のコンポーネントにアクセスできれば実現できる、というケースで役立つことも多いかと思います。

また機会があれば、別の例も記事として取り上げたいと思っています。