【SwiftUI】カスタムタブを表示するのはoverlayではなくsafeAreaInsetの方がいい

# はじめに
カスタムタブを作成した際にoverlayを使って表示するとコンテンツに被ってしまうという問題に当たりました。
safeAreaInsetを使ったところ解決したので、記事にしておきます。

今回のサンプルアプリでは「ホーム」「通知」「プロフィール」のタブがある想定で進めます。
しかし、ちゃんと実装するのは「ホーム」だけで「通知」「プロフィール」にはTextを配置します。


# サンプルアプリ
![Simulator Screen Recording - iPhone 15 - 2024-01-09 at 23.42.50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/89a5a91c-ec1c-deb9-4182-802e0bde7ae1.gif)

# カスタムタブの実装
以下のようなシンプルなカスタムタブを作成しました。
![Simulator Screenshot - iPhone 15 - 2024-01-09 at 23.08.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/9af84c3c-6752-1e3c-9267-54c1dfb54ac0.png)

```swift
import SwiftUI

struct CustomTabView: View {
    @Binding private var selectedTabItem: TabItem

    init(selectedTabItem: Binding<TabItem>) {
        self._selectedTabItem = selectedTabItem
    }

    var body: some View {
        HStack(spacing: 0) {
            ForEach(TabItem.allCases, id: \.id) { tabItem in
                Button {
                    self.selectedTabItem = tabItem
                } label: {
                    (selectedTabItem == tabItem ? tabItem.activeTabImage : tabItem.inactiveTabImage)
                        .resizable()
                        .scaledToFit()
                        .frame(height: 24)
                }
                .foregroundStyle(selectedTabItem == tabItem ? Color.primary : Color.secondary)
                .frame(width: 55, height: 40)
                .frame(maxWidth: tabItem == TabItem.notification ? .infinity : nil)
            }
        }
        .padding(.horizontal, 42)
        .padding(.top, 7)
        .background(Material.bar)
    }
}

enum TabItem: String, CaseIterable, Hashable, Identifiable {
    case home
    case notification
    case profile

    var id: String { rawValue }

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.rawValue == rhs.rawValue
    }
}

extension TabItem {
    var activeTabImage: Image {
        switch self {
        case .home:
            return Image(systemName: "house.fill")
        case .notification:
            return Image(systemName: "bell.fill")
        case .profile:
            return Image(systemName: "person.fill")
        }
    }

    var inactiveTabImage: Image {
        switch self {
        case .home:
            return Image(systemName: "house")
        case .notification:
            return Image(systemName: "bell")
        case .profile:
            return Image(systemName: "person")
        }
    }
}
```

# HomeViewの実装
```swift
import SwiftUI

struct HomeView: View {
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<10) { _ in
                    RoundedRectangle(cornerRadius: 10)
                        .frame(height: 80)
                        .foregroundStyle(Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)))
                }
            }
            .padding(.horizontal, 20)
        }
    }
}
```

# 使用側の実装
```swift
import SwiftUI

struct ContentView: View {
    @State private var selectedTabItem: TabItem = .home
    
    var body: some View {
        TabView(selection: $selectedTabItem) {
            HomeView().tag(TabItem.home)
            
            Text("通知").tag(TabItem.notification)
            
            Text("プロフィール").tag(TabItem.profile)
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
        .safeAreaInset(edge: .bottom, spacing: 0) {
            CustomTabView(selectedTabItem: $selectedTabItem)
        }
    }
}
```

# overlayだと、、、
```swift
import SwiftUI

struct ContentView: View {
    @State private var selectedTabItem: TabItem = .home
    
    var body: some View {
        TabView(selection: $selectedTabItem) {
            HomeView().tag(TabItem.home)
            
            Text("通知").tag(TabItem.notification)
            
            Text("プロフィール").tag(TabItem.profile)
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
        .overlay(alignment: .bottom) {
            CustomTabView(selectedTabItem: $selectedTabItem)
        }
    }
}
```

1番下までスクロールすると違いがわかります。
`safeAreaInset`の方は重ならないところまでスクロールできます。
しかし、`overlay`は1番下までスクロールしてもタブとコンテンツが重なったままです。

|`safeAreaInset`|`overlay`|
|-|-|
|![simulator_screenshot_795062D9-A2CB-4646-8879-B845A509EA88.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/4f72ebf4-8133-65f0-f582-955a4ff184d7.png)|![simulator_screenshot_F470E3AA-4D86-4D49-8D25-F5CDE3A932A7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/201a26ca-c095-75f6-fb6c-8ebef2621b00.png)|

# おわり
書いた後に気づいたのですが、これであればVStackでいいですね。
ただ、safeAreaInsetを使うのが正攻法っぽい？かな？
overlayの透過してあるタブもかっこいいので、透過のままタブコンテンツを重ならないところまでスクロールできる方法を考えたいですね。
調査します。

こんな感じで`ScrollView`に`safeAreaInset`を付与すれば、タブは透過してて、コンテンツは重ならないといった最高の状態になるんですけどね。
```swift
import SwiftUI

struct ContentView: View {
    @State private var selectedTabItem: TabItem = .home
    
    var body: some View {
        ScrollView {
            HomeView()
        }
        .safeAreaInset(edge: .bottom, spacing: 0) {
            CustomTabView(selectedTabItem: $selectedTabItem)
        }
    }
}
```

![Simulator Screen Recording - iPhone 15 - 2024-01-09 at 23.52.45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/0c573402-faeb-79a9-cab9-89863be4f84f.gif)

Tabの機能を維持したままだと難しいです、、、

# 公式ドキュメント
https://developer.apple.com/documentation/swiftui/view/safeareainset(edge:alignment:spacing:content:)-4s51l