3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-01-09

はじめに

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

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

サンプルアプリ

Simulator Screen Recording - iPhone 15 - 2024-01-09 at 23.42.50.gif

カスタムタブの実装

以下のようなシンプルなカスタムタブを作成しました。
Simulator Screenshot - iPhone 15 - 2024-01-09 at 23.08.20.png

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の実装

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)
        }
    }
}

使用側の実装

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だと、、、

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 simulator_screenshot_F470E3AA-4D86-4D49-8D25-F5CDE3A932A7.png

おわり

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

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

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

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

公式ドキュメント

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?