【SwiftUI】NavigationStackとTabViewとPickerを組み合わせて使用すると何故かタブバーが透明になる問題

2023.02.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

NavigationStackの入れ子にTabViewを配置し、そのTabView内の子ViewにPickerを配置すると、何故かタブバーが透明になってしまったので対処法を模索しました。

環境

  • Xcode 14.2
  • iOS 16.1

現象

現象①

タブバーの背景が透明になってしまいます。

コード

import SwiftUI

struct ContentView: View {
    var body: some View {

        NavigationStack {
            TabView {

                ListView()
                    .tabItem {
                        Image(systemName: "1.circle")
                    }

                Text("")
                    .tabItem {
                        Image(systemName: "2.circle")
                    }

                Text("")
                    .tabItem {
                        Image(systemName: "3.circle")
                    }
            }
            .navigationTitle("List")
        }
    }
}

// 子View
struct ListView: View {

    enum Category: String, Identifiable, CaseIterable {
        case wait
        case inProgress
        case done

        var id: String {
            return self.rawValue
        }
    }

    @State private var category: Category = .wait

    var body: some View {

        VStack {

            Picker("", selection: $category) {
                ForEach(Category.allCases) {
                    Text($0.rawValue)
                        .tag($0)
                }
            }
            .pickerStyle(.segmented)
            .padding()

            List {
                ForEach(0..<20) { index in
                    Text("Item")
                }
            }
        }
    }
}

現象②

子ViewのPickerListの位置を入れ替えると、タブバーが表示されるようになります。

コード

// 子View
struct ListView: View {

    enum Category: String, Identifiable, CaseIterable {
        case wait
        case inProgress
        case done

        var id: String {
            return self.rawValue
        }
    }

    @State private var category: Category = .wait

    var body: some View {

        VStack {

            // Listを上に変更
            List {
                ForEach(0..<20) { index in
                    Text("Item")
                }
            }

            // Pickerを下に変更
            Picker("", selection: $category) {
                ForEach(Category.allCases) {
                    Text($0.rawValue)
                        .tag($0)
                }
            }
            .pickerStyle(.segmented)
            .padding()
        }
    }
}

タブバーが透明になってしまうケースとは

同じケースではないが、Stack Overflowで透明になっているケースを発見しました。

iOS 15から変更になったタブバーの仕様で、中身のコンテンツが短い場合にタブバーは透明になるとのこと。

WWDC21 - What's new in UIKitの中(5:50~)でもiOS 15からのタブバーの変更点について説明されていました。

iOS 15での UI の改良点をいくつか紹介します。

UIToolbar と UITabBar の外観を改良しました。この更新された外観では、下にスクロールすると背景素材が削除され、コンテンツがより視覚的に明確になります。

今回発生しているケースとは違いますが、タブバーが透明になってしまうケースが分かりました。

対応策

コードで強制的に背景をつける

onAppear時にタブバーに背景を付ける処理を行なっています。

struct ContentView: View {
    var body: some View {

        NavigationStack {
            TabView {

                ListView()
                    .tabItem {
                        Image(systemName: "1.circle")
                    }

                Text("")
                    .tabItem {
                        Image(systemName: "2.circle")
                    }

                Text("")
                    .tabItem {
                        Image(systemName: "3.circle")
                    }
            }
            .navigationTitle("List")
        }
        .onAppear {
            // タブバーに背景を付ける
            let tabBarAppearance = UITabBarAppearance()
            tabBarAppearance.configureWithDefaultBackground()
            UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
        }
    }
}

結果

背景が反映された状態になりました。

NavigationStackとTabViewの入れ子を入れ替える

WWDC22 - Explore navigation design for iOSの内容を見てみると、そもそもTabViewNavigationStackの入れ子にするのがApple的にはアンチパターンのように感じました。

タブバーは最上位のコンテンツを表し、アプリの階層の最上位にある必要があります。

なので、入れ子の箇所を入れ替えてみます。

コード

import SwiftUI

struct ContentView: View {
    var body: some View {

        TabView {

            // NavigationStackを入れ子にする
            NavigationStack {
                ListView()
            }
            .tabItem {
                Image(systemName: "1.circle")
            }

            Text("")
                .tabItem {
                    Image(systemName: "2.circle")
                }

            Text("")
                .tabItem {
                    Image(systemName: "3.circle")
                }
        }
    }
}

struct ListView: View {

    enum Category: String, Identifiable, CaseIterable {
        case wait
        case inProgress
        case done

        var id: String {
            return self.rawValue
        }
    }

    @State private var category: Category = .wait

    var body: some View {

        VStack {

            Picker("", selection: $category) {
                ForEach(Category.allCases) {
                    Text($0.rawValue)
                        .tag($0)
                }
            }
            .pickerStyle(.segmented)
            .padding()

            List {
                ForEach(0..<20) { index in
                    Text("Item")
                }
            }
        }
        .navigationTitle("List")
    }
}

結果

こちらでも同様にタブバーの背景が表示されました。

UITabBarAppearanceを操作する方法より、こちらの方がより自然なのでこちらを採用したいと思います。

おわりに

原因の特定は出来ませんでしたが、挙動的にSwiftUI側のバグのような気はします。

今回対応方法を模索していく中でApple側が意図しているTabViewの使い方を理解することが出来て良かったです。

NavigationStack(またはNavigationView)とTabViewを合わせて使用した時におかしな挙動をする時は、入れ子の順序を変更してみると良いかもしれないです。

参考