バー付きのTabView

画面上部のボタンをタップすると、画面が左右にスクロールしてViewが切り替わると同時に、バーがボタンの下に移動するUIを作成しました。

ソースコード

import SwiftUI

struct BarPagerView: View {
    @State private var selectedTab: Int = 0
    
    let tabs: [TabButtonView] = [
        .init(icon: Image(systemName: "music.note"), title: "Music"),
        .init(icon: Image(systemName: "film.fill"), title: "Movies"),
        .init(icon: Image(systemName: "book.fill"), title: "Books")
    ]
    
    let views: [AnyView] = [
        .init(Text("View 01")),
        .init(Text("View 02")),
        .init(Text("View 03"))
    ]
    
    var body: some View {
        VStack {
            Tabs(tabButtonViews: tabs, selectedTab: $selectedTab)
            TabView(selection: self.$selectedTab) {
                ForEach(0 ..< views.count, id: \.self) { index in
                    views[index].background(Color.red).tag(index)
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
        }
    }
}

struct TabButtonView: View {
    var icon: Image?
    var title: String
    
    var body: some View {
        HStack {
            AnyView(icon)
                .foregroundColor(.black)
                .padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 0))

            Text(title)
                .font(Font.system(size: 18, weight: .semibold))
                .foregroundColor(Color.black)
                .padding(EdgeInsets(top: 10, leading: 3, bottom: 10, trailing: 15))
        }
    }
}

struct TabBarView: View {
    var selectedBarColor: Color
    var tabButtonViews: [TabButtonView]
    var cellWidth: CGFloat
    
    @Binding var selectedTab: Int
    
    var body: some View {
        HStack(spacing: 0) {
            ForEach(0 ..< tabButtonViews.count, id: \.self) { row in
                Button(action: {
                    withAnimation {
                        selectedTab = row
                    }
                }, label: {
                    VStack(spacing: 0) {
                        tabButtonViews[row]
                        .frame(width: cellWidth, height: 52)
                    }.fixedSize()
                })
                .accentColor(selectedBarColor)
                .buttonStyle(PlainButtonStyle())
            }
        }
    }
}

struct Tabs: View {
    var tabButtonViews: [TabButtonView]
    @Binding var selectedTab: Int
    
    var body: some View {
        let cellWidth: CGFloat = UIScreen.main.bounds.width / CGFloat(tabButtonViews.count)
        
        
        ScrollView(.horizontal, showsIndicators: false) {
            ScrollViewReader { proxy in
                VStack(spacing: 0) {
                    TabBarView(selectedBarColor: Color.blue, tabButtonViews: tabButtonViews, cellWidth: cellWidth, selectedTab: $selectedTab)
                    .onChange(of: selectedTab) { target in
                        withAnimation {
                            proxy.scrollTo(target)
                        }
                    }
                    // Bar Indicator
                    Rectangle()
                        .fill(Color.blue)
                        .frame(width: cellWidth, height: 3)
                        .offset(x: CGFloat(selectedTab - 1) * cellWidth, y: 0)
                        .animation(.spring(), value: selectedTab)
                }
            }
        }
        .onAppear(perform: {
            UIScrollView.appearance().bounces = false
        })
        .frame(height: 55)
        .onDisappear(perform: {
            UIScrollView.appearance().bounces = true
        })
    }
}

実装結果