由于选项卡视图无法放入滚动视图内,我尝试使用新的 iOS 17 方法来使用滚动视图创建自定义选项卡。在我下面的代码中,如果您注释掉标记为“不起作用”的第一个 VStack 并取消注释第二个可用的 vstack 视图,您将看到我的自定义选项卡视图功能良好。不过,我想制作一个配置文件视图,其中包含标题、向上滚动的中间内容(不固定)、固定在顶部的栏,然后是选项卡视图。这样做会导致我的自定义选项卡视图失败。
我找不到任何具有在滚动视图中运行的自定义选项卡视图的网站,所以我认为解决这个问题很重要。
import SwiftUI
#Preview(body: {
SwiftUIView()
})
struct SwiftUIView: View {
@State private var selectedTab: TabProfile?
@State private var tabProgress: CGFloat = 0
@Environment(\.colorScheme) var colorScheme
var body: some View {
//----- THIS DOESNT WORK
VStack(alignment: .leading){
Color.red.frame(height: 100)
.overlay {
Text("Header").foregroundStyle(.white)
.offset(y: 20).bold()
}
ScrollView {
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
Color.blue.frame(height: 100)
.overlay {
Text("Some content").foregroundStyle(.white).bold()
}
Section(header: buttonTab()) {
rcontent()
}
}
}
.scrollIndicators(.hidden)
}
.ignoresSafeArea()
//-----THIS WORKS---------
// VStack {
// buttonTab()
//
// rcontent()
// }
}
func rcontent() -> some View {
GeometryReader {
let size = $0.size
ScrollView(.horizontal) {
LazyHStack(spacing: 0) {
SampleView(.purple)
.id(TabProfile.hustles)
.containerRelativeFrame(.horizontal)
SampleView(.red)
.id(TabProfile.jobs)
.containerRelativeFrame(.horizontal)
SampleView(.blue)
.id(TabProfile.likes)
.containerRelativeFrame(.horizontal)
SampleView(.green)
.id(TabProfile.sale)
.containerRelativeFrame(.horizontal)
SampleView(.orange)
.id(TabProfile.question)
.containerRelativeFrame(.horizontal)
}
.scrollTargetLayout()
.offsetX { value in
let progress = -value / (size.width * CGFloat(TabProfile.allCases.count - 1))
tabProgress = max(min(progress, 1), 0)
}
}
.scrollPosition(id: $selectedTab)
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)
.scrollClipDisabled()
}
}
func buttonTab() -> some View {
HStack(spacing: 0) {
ForEach(TabProfile.allCases, id: \.rawValue) { tab in
HStack(spacing: 10) {
Text(tab.rawValue)
.font(.callout)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.contentShape(.capsule)
.onTapGesture {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
withAnimation(.snappy) {
selectedTab = tab
}
}
}
}
.tabMask(tabProgress, tabCount: TabProfile.allCases.count)
.padding(.horizontal, 8)
.background {
GeometryReader {
let size = $0.size
let capusleWidth = size.width / CGFloat(TabProfile.allCases.count)
ZStack(alignment: .leading){
RoundedRectangle(cornerRadius: 0)
.fill(.gray)
.frame(height: 1)
.offset(y: 40)
RoundedRectangle(cornerRadius: 5)
.fill(.blue)
.frame(width: capusleWidth, height: 2)
.offset(x: tabProgress * (size.width - capusleWidth), y: 40)
}
}
}
}
@ViewBuilder
func SampleView(_ color: Color) -> some View {
ScrollView(.vertical) {
LazyVGrid(columns: Array(repeating: GridItem(), count: 2), content: {
ForEach(1...10, id: \.self) { _ in
RoundedRectangle(cornerRadius: 15)
.fill(color.gradient)
.frame(height: 150)
}
})
.padding(15)
}
.scrollIndicators(.hidden)
.scrollClipDisabled()
.mask {
Rectangle()
.padding(.bottom, -100)
}
}
}
enum TabProfile: String, CaseIterable {
case hustles = "Hustles"
case jobs = "Jobs"
case likes = "Likes"
case sale = "4Sale"
case question = "???"
}
import SwiftUI
#Preview(body: {
SwiftUIView()
})
struct SwiftUIView: View {
@State private var selectedTab: TabProfile?
@State private var tabProgress: CGFloat = 0
@Environment(\.colorScheme) var colorScheme
let counts: [TabProfile.RawValue : Int] = ["Hustles" : 20, "Jobs" : 2, "Likes" : 20]
var body: some View {
VStack(alignment: .leading){
Color.red.frame(height: 100)
.overlay {
Text("Header").foregroundStyle(.white)
.offset(y: 20).bold()
}
GeometryReader {
let size = $0.size
ScrollViewReader(content: { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
Color.blue.frame(height: 100)
.overlay {
Text("Some content").foregroundStyle(.white).bold()
}
.id("top")
Section(header: buttonTab()) {
ScrollView(.horizontal) {
LazyHStack(alignment: .top, spacing: 0) {
SampleView(.purple, count: counts["Hustles"] ?? 20)
.id(TabProfile.hustles)
.containerRelativeFrame(.horizontal)
SampleView(.red, count: counts["Jobs"] ?? 2)
.id(TabProfile.jobs)
.containerRelativeFrame(.horizontal)
SampleView(.blue, count: counts["Likes"] ?? 20)
.id(TabProfile.likes)
.containerRelativeFrame(.horizontal)
}
.scrollTargetLayout()
.offsetX { value in
let progress = -value / (size.width * CGFloat(TabProfile.allCases.count - 1))
tabProgress = max(min(progress, 1), 0)
}
}
.scrollPosition(id: $selectedTab)
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)
.scrollClipDisabled()
}
}
}
.scrollIndicators(.hidden)
.onChange(of: selectedTab) { oldValue, newValue in
let old = counts[oldValue?.rawValue ?? ""] ?? 0
let new = counts[newValue?.rawValue ?? ""] ?? 0
if old > new {
withAnimation { proxy.scrollTo("top", anchor: .bottom) }
}
}
})
}
}
.ignoresSafeArea()
}
func buttonTab() -> some View {
HStack(spacing: 0) {
ForEach(TabProfile.allCases, id: \.rawValue) { tab in
HStack(spacing: 10) {
Text(tab.rawValue)
.font(.callout)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.contentShape(.capsule)
.onTapGesture {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
withAnimation(.snappy) {
selectedTab = tab
}
}
}
}
.tabMask(tabProgress, tabCount: TabProfile.allCases.count)
.padding(.horizontal, 8)
.background {
GeometryReader {
let size = $0.size
let capusleWidth = size.width / CGFloat(TabProfile.allCases.count)
ZStack(alignment: .leading){
RoundedRectangle(cornerRadius: 0)
.fill(.gray)
.frame(height: 1)
.offset(y: 40)
RoundedRectangle(cornerRadius: 5)
.fill(.blue)
.frame(width: capusleWidth, height: 2)
.offset(x: tabProgress * (size.width - capusleWidth), y: 40)
}
}
}
.background(colorScheme == .dark ? .black : .white)
}
@ViewBuilder
func SampleView(_ color: Color, count: Int) -> some View {
LazyVGrid(columns: Array(repeating: GridItem(), count: 2), content: {
ForEach(1...count, id: \.self) { _ in
RoundedRectangle(cornerRadius: 15)
.fill(color.gradient)
.frame(height: 150)
}
})
.padding(15)
.scrollIndicators(.hidden)
.scrollClipDisabled()
.mask {
Rectangle()
.padding(.bottom, -100)
}
}
}
enum TabProfile: String, CaseIterable {
case hustles = "Hustles"
case jobs = "Jobs"
case likes = "Likes"
case sale = "4Sale"
case question = "???"
}