SwiftUI:内层ScrollView触顶时如何实现外层滚动联动
页面结构示意图

页面组成
我需要实现如下页面,整体包含四个部分:
- Sticky header:页面上固定位置、宽高的导航栏,包含返回图标。
- 信息视图(Info View):用于展示特定信息和图片的视图。
- Tabs:包含两个标签的标签栏,用于切换卡片滚动列表。
- ScrollView中的帖子卡片:可滚动的卡片列表视图,采用瀑布流形式展示卡片(瀑布流已实现)。
问题一
第一个困扰我的问题是,当页面处于初始状态时,底部的列表会直接滚动,而非推动父级ScrollView协同滚动。
问题二

演示代码
我使用水平ScrollView和scrollTargetBehavior API实现标签切换,每个标签对应一个包含卡片列表VStack的垂直内层ScrollView。
import SwiftUI struct ContentView: View { @State private var currentTab: String? = "Posted" @State private var tabProgress: CGFloat = 0 var body: some View { let screenWidth: CGFloat = UIScreen.main.bounds.width let bw: CGFloat = UIScreen.main.bounds.width / 375 let screenHeight: CGFloat = UIScreen.main.bounds.height GeometryReader { geometry in ZStack { // Sticky top bar, on the top of any other views VStack { VStack { NavRow() .padding(.top, geometry.safeAreaInsets.top) } .frame(alignment: .top) .background(.green) Spacer() } .zIndex(999) // Outer Scroll View ScrollView (.vertical, showsIndicators: false) { VStack (spacing: 0) { // Same height as the top nav row Spacer().frame(height: 64 * bw + geometry.safeAreaInsets.top) // Info View InfoView() // Tab Row TabRow(currentTab: $currentTab) } // Scroll Views // Use horizontal paging scroll to implement tab switching ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 0) { ScrollView (.vertical, showsIndicators: false) { PostedList() } .frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top) .id("Posted") ScrollView (.vertical, showsIndicators: false) { LikedList() } .frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top) .id("Liked") } .scrollTargetLayout() .offsetX { value in /// Converting Offset into Progress let progress = -value / (screenWidth * CGFloat(1)) /// Capping Progress BTW 0-1 tabProgress = max(min(progress, 1), 0) } } .scrollPosition(id: $currentTab) .scrollTargetBehavior(.paging) .scrollClipDisabled() } .ignoresSafeArea() .frame(width: screenWidth, height: screenHeight) } .ignoresSafeArea() .frame(width: screenWidth, height: screenHeight) } } } // Top nav row struct NavRow: View { var body: some View { let screenWidth: CGFloat = UIScreen.main.bounds.width let bw: CGFloat = UIScreen.main.bounds.width / 375 Rectangle().fill(.clear) .frame(width: screenWidth, height: 64 * bw) .overlay(alignment: .center, content: { Text("Nav Row") .font(.system(size: 18, weight: .semibold)) .foregroundColor(.white) }) } } struct InfoView: View { var body: some View { let bw: CGFloat = UIScreen.main.bounds.width / 375 Rectangle().fill(.red.opacity(0.6)) .frame(width: 343 * bw, height: 212 * bw) .cornerRadius(12 * bw) .overlay { Text("Info View") .foregroundColor(.white) .font(.system(size: 24, weight: .semibold)) } } } struct TabRow: View { @Binding var currentTab: String? @ViewBuilder func SingleTab(tab: String) -> some View { let isCurrent = currentTab == tab Text(tab) .font(.system(size: isCurrent ? 16 : 15, weight: isCurrent ? .semibold : .regular)) .onTapGesture { if (!isCurrent) { withAnimation(.snappy) { currentTab = tab } } } } var body: some View { let screenWidth: CGFloat = UIScreen.main.bounds.width let bw: CGFloat = UIScreen.main.bounds.width / 375 Rectangle().fill(.yellow.opacity(0.5)) .frame(width: screenWidth, height: 64 * bw) .overlay(alignment: .leading, content: { HStack { SingleTab(tab: "Posted") Spacer().frame(width: 24 * bw) SingleTab(tab: "Liked") } .padding(.leading, 16 * bw) }) } } @ViewBuilder func sampleCard(cardText: String, cardColor: Color) -> some View { let bw: CGFloat = UIScreen.main.bounds.width / 375 Rectangle().fill(cardColor) .frame(width: 343 * bw, height: 166 * bw) .overlay { Text(cardText) .font(.system(size: 18, weight: .semibold)) } .padding(.bottom, 32 * bw) } struct PostedList: View { // 11 colors private let colorList: [Color] = [.gray, .red, .purple, .orange, .blue, .yellow, .pink, .brown, .cyan, .indigo, .teal] var body: some View { let screenWidth: CGFloat = UIScreen.main.bounds.width VStack (spacing: 0) { ForEach(0...colorList.count - 1, id: \.self) { i in sampleCard(cardText: "Post \(i)", cardColor: colorList[i]) } } .frame(width: screenWidth) } } struct LikedList: View { var body: some View { let screenWidth: CGFloat = UIScreen.main.bounds.width VStack (spacing: 0) { ForEach(0...19, id: \.self) { i in sampleCard(cardText: "Liked \(i)", cardColor: .gray.opacity(0.2)) } } .frame(width: screenWidth) } } /// Offset Key struct OffsetKey: PreferenceKey { static var defaultValue: CGFloat = .zero static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } extension View { @ViewBuilder func offsetX(completion: @escaping (CGFloat) -> ()) -> some View { self .overlay { GeometryReader { let minX = $0.frame(in: .scrollView(axis: .horizontal)).minX Color.clear .preference(key: OffsetKey.self, value: minX) .onPreferenceChange(OffsetKey.self, perform: completion) } } } }
我已被该问题困扰数日,恳请各位提供解决方案,万分感谢。
内容的提问来源于stack exchange,提问作者潘星宇
相关产品推荐
相关产品推荐

