`toolbarBackground` を使って Pure SwiftUI でツールバーの外観を変える

## SwiftUI と `UIBarAppearance`

iOS 15 まで、SwiftUI では NavigationBar / TabBar / ToolBar などのツールバーの背景色を設定する手段がありませんでした。そのため、以下のように UIKit の `UIBarAppearance` を指定する方法がとられていたと思います。

```swift
let appearance = UITabBarAppearance()
appearance.backgroundColor = .systemBackground
UITabBar.appearance().standardAppearance = appearance
UITabBar.appearance().scrollEdgeAppearance = appearance
```

しかし、この方法はグローバルな設定を書き換えてしまうという難点があります。具体的には、以下の記事で書かれているような問題を引き起こすことがあります。

https://qiita.com/noppefoxwolf/items/5ba81e93bef8b91f1484

また、 `NavigationView` や `TabView` の裏に UIKit がいるという内部実装に依存しているので、新しい iOS でも常に想定通り動き続けるという保証はありません。とは言え、他にツールバーの背景色を指定する手段があるわけでもないので、これまでは `UIBarAppearance` を指定するしかありませんでした。

## `.toolbarBackground`

iOS 16 で [`toolbarBackground(_:for:)`](https://developer.apple.com/documentation/swiftui/view/toolbarbackground(_:for:)-5ybst) という新しい API が追加され、 SwiftUI の範囲内でツールバーの背景色を指定できるようになりました。`toolbarBackground` の第一引数に適用したい [`ShapeStyle`](https://developer.apple.com/documentation/swiftui/shapestyle) を、第二引数に対象の [`ToolBarPlacement`](https://developer.apple.com/documentation/swiftui/toolbarplacement) を指定することで背景色を設定することができます。

```swift
struct FirstTab: View {
    var body: some View {
        NavigationStack {
            List {
                ForEach(1..<30) { i in
                    Text("Text \(i)")
                }
            }
            .navigationTitle("First Tab")
            // NavigationBar の背景色を設定
            .toolbarBackground(.blue, for: .navigationBar)
        }
    }
}

struct ContentView: View {
    var body: some View {
        TabView {
            FirstTab()
	            // TabBar の背景色を設定
                .toolbarBackground(.gray, for: .tabBar)
                .tabItem {
                    Label("first", systemImage: "1.circle")
                }
        }
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/550f0fea-9065-b6c3-7093-6e429dd6c2a4.png" width="300px" />


:::note warn
上記の例では `toolbarBackground` の第一引数に `Color` を渡していますが、 `ShapeStyle` protocol に準拠しているものであれば渡すことができます。ただし、 Xcode 14.1 Beta で試した限りでは `.linearGradient` / `.angularGradient` / `.image` などは動作しないように見えます。これは仕様かもしれませんが、単なる不具合で今後修正される可能性もあると思います。いずれにしても、現実的には `Color` を指定したいケースがほとんどだと思うので困ることはあまりないでしょう。
:::

## エッジまでスクロールした状態の背景色指定

`toolbarBackground(_:for:)` に単に `ShapeStyle` を渡しただけだと、iOS の場合エッジまでスクロールするとツールバーは透明になり、渡した設定が反映されなくなります。スクロール位置に関わらず設定を反映させるには  [`toolbarBackground(_:for:)`](https://developer.apple.com/documentation/swiftui/tabview/toolbarbackground(_:for:)) を使います。 `ShapeStyle` を渡す API と名前は同じですが、第一引数は [`Visibility`](https://developer.apple.com/documentation/swiftui/visibility) です。これを `.visible` に指定することで常に設定した背景色が使われるようになります。 

```diff_swift
NavigationStack {
    List {
	    // ...
    }
    .navigationTitle("First Tab")
    .toolbarBackground(.blue, for: .navigationBar)
+   .toolbarBackground(.visible, for: .navigationBar)
}
```

これを NavigationBar / TabBar に対して適用した前後の様子は以下のようになります。

| .visible 適用前 | .visible 適用後 |
| --- | --- |
| ![2022-09-16 10.11.52.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/b6143156-83fd-bf0e-17bd-94e393bccb00.gif) | ![2022-09-16 10.11.34.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/3f3e299f-f3b8-a64c-212c-da9c47288296.gif) | 

:::note warn
自分の手元で試した限り、 Xcode 14.0 の時点では TabBar に対して `.visible` の指定が効いてないようでした。もしうまく動作しない場合は Xcode 14.1 以上で試してみてください。
:::

## 複数のタブに対する TabBar の背景色指定

TabBar に対する指定は、指定したタブがアクティブになっている時だけ有効になります。すべてのタブに対して同じ背景色を指定したい場合は `TabView` の子を `Group` にして、それに対して `.toolbarBackground` を指定すればよいです。

```swift
TabView {
    Group {
        FirstTab()
            .tabItem { Label("first", systemImage: "1.circle") }
        SecondTab()
            .tabItem { Label("second", systemImage: "2.circle") }
    }
    .toolbarBackground(.gray, for: .tabBar)
}
```

逆にアクティブなタブごとに TabBar に異なる背景色を指定したい場合は Tab ごとに `toolbarBackground` を指定します。

## ツールバーの背景色指定の今後

`.toolbarBackground` のおかげで `UIBarAppearance` を直接操作する時代は終わったのかというと、 `.toolbarBackground` は iOS 16 以降でしか使えないので、当然ながら iOS 15 以前をサポートしているアプリでは `UIBarAppearance` を指定する必要があるでしょう。

また、 iOS 16 以上のみをサポートする場合であっても `.toolbarBackground` よりも `UIBarAppearance` の方ができることの幅が広いので、特殊な要件のあるアプリでは引き続き `UIBarAppearance` を指定したい場合があるかもしれません。
例えば、 iOS の `.toolbarBackground` では、エッジまでスクロールした場合の TabBar の見た目は

- 通常の状態と同じ（`.toolbarBackground(.visible, for: .tabBar)`）
- 透明（`.toolbarBackground(.automatic, for: .tabBar)`）

の２通りしか指定できないように思います。これに対して、 `UIBarAppearance` を使う方法では、 `UITabBar` の `scrollEdgeAppearance` プロパティにエッジまでスクロールした場合の見た目を別途指定できます。

しかし、ほとんどの場合は `.toolbarBackground` で行える範囲の指定で十分だし、その範囲の指定に収めるのが望ましいと思うので、実質的には iOS 16 以上のみをサポートするアプリに関しては `UIBarAppearance` を指定する必要は無くなったと言っていいでしょう。

## 参考

- [`toolbarBackground(_:for:)` (`ShapeStyle` を指定) | Apple Developer](https://developer.apple.com/documentation/swiftui/view/toolbarbackground(_:for:)-5ybst)
- [`toolbarBackground(_:for:)` (`Visibility` を指定) | Apple Developer](https://developer.apple.com/documentation/swiftui/tabview/toolbarbackground(_:for:))