📷
SwiftUI Sticky Tab Bar like Instagram
SwiftUI でインスタやTikTokのプロフィールのような、スクロールするとタブバーが固着するような UI を TabView を使って作りたい。CookPad さんのブログが詳しく解説されてますが offSet を使った調整が色々と難しく結局タブ切り替え時に余白ずれする問題を解決しきれませんでした。
しかしTabの中に表示するコンテンツの高さが一定なのであれば GeometryReader から計算してやってしまえば概ね理想通りに動きました。ただ posts の数が増えるとタブ切り替えがカクツク。
ScrollView in ScrollView は安定もしなければ悪手だし、UIKitを使ったサンプルを組み込むことも考えたがそれだと全部のView を ViewController x StoryBoard でまた作らないといけないので断念。Kavsoftがいくつかサンプルを出しているが、TabView を使わないサンプルだと横スワイプでのタブ切り替えができないなどイマイチだった。
なおこのコードはタブのスクロール量をタブごとに保持はしていないのでその点だけ注意。
import SwiftUI
struct ProfileView: View {
@Environment(\.colorScheme) var colorScheme
@StateObject var viewModel: PostGridViewModel
let tabs = [
"投稿順",
"撮影日順"
]
@State private var selection = 0
init(path: Binding<NavigationPath>) {
self._path = path
_viewModel = StateObject(wrappedValue: PostGridViewModel(user: user))
}
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
ProfileHeaderView()
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
Section {
// 3カラムなので3の倍数になるように切り上げる
let rows = ceil(Double(viewModel.posts.count) / 3.0)
let tabViewHeight = (proxy.size.width / 3) * rows
TabView(selection: $selection) {
PostGridView(posts: viewModel.posts, path: $path)
.tag(0)
PostGridView(posts: viewModel.posts, path: $path)
.tag(1)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxWidth: .infinity, minHeight: tabViewHeight)
} header: {
HStack(spacing: 0) {
ForEach(tabs.indices, id: \.self) { index in
Text(tabs[index])
.frame(maxWidth: .infinity)
.contentShape(Rectangle()) // for tap gesture
.tabSelectionStyle(isSelected: index == selection)
.tag(index)
.highPriorityGesture(
TapGesture().onEnded {
withAnimation {
selection = index
}
}
)
}
}
.background(colorScheme == .dark ? .black : .white)
}
}
}
// TODO: keep scroll position for each tab
}
}
}
Reference
- MaterialUI(ただしiOS17以上)
- UIKit(Storyboard and ViewController)
Discussion