SwiftUI中实现ScrollViewA与嵌套垂直滚动视图内的ScrollViewB同步水平滚动的问题
SwiftUI中实现ScrollViewA与嵌套垂直滚动视图内的ScrollViewB同步水平滚动的问题
嗨,我来帮你搞定这个同步滚动的需求!你的场景是在一个VStack里,顶部有个水平的ScrollViewA,而ScrollViewB被嵌套在另一个垂直ScrollView里,想要让ScrollViewA滚动时,ScrollViewB也跟着同步水平滚动对吧?
其实核心思路就是捕获ScrollViewA的滚动偏移量,然后把这个偏移量同步给ScrollViewB。在SwiftUI里,我们可以用自定义PreferenceKey来监听滚动位置,再结合状态变量传递偏移量,就能实现这个效果。下面是修改后的完整代码,我会一步步给你解释关键点:
import SwiftUI // 自定义PreferenceKey,用来传递ScrollView的滚动偏移量 struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct ContentView: View { let grids = Array(repeating: GridItem(.fixed(100), spacing: 10, alignment: .leading), count: 10) let gridsHeader = Array(repeating: GridItem(.fixed(100), spacing: 10, alignment: .leading), count: 1) // 用来存储同步滚动的偏移量 @State private var syncScrollOffset: CGFloat = 0 var body: some View { VStack { // 第一个垂直ScrollView,包含需要被监听的ScrollViewA ScrollView(.vertical) { HStack { // ScrollViewA - 我们要监听它的滚动动作 ScrollView(.horizontal) { LazyHGrid(rows: gridsHeader, spacing: 30) { ForEach(photoArray0) { item in ForEach(0..<1) { num in Image(item.imageName) .resizable() .aspectRatio(contentMode: .fit) .cornerRadius(8) } } } .padding() // 添加背景GeometryReader捕获滚动偏移 .background(GeometryReader { proxy in Color.clear.preference( key: ScrollOffsetPreferenceKey.self, value: -proxy.frame(in: .named("syncScrollSpace")).minX ) }) } .coordinateSpace(name: "syncScrollSpace") // 监听偏移变化,更新同步变量 .onPreferenceChange(ScrollOffsetPreferenceKey.self) { newValue in syncScrollOffset = newValue } // 这个是你原代码里的另一个水平ScrollView,保持不变 ScrollView(.horizontal) { LazyHGrid(rows: gridsHeader, spacing: 30) { ForEach(photoArray) { item in ForEach(0..<1) { num in Image(item.imageName) .resizable() .aspectRatio(contentMode: .fit) .cornerRadius(8) } } } .padding() } } } // 第二个垂直ScrollView,包含需要同步的ScrollViewB ScrollView(.vertical) { HStack { // ScrollViewB - 和ScrollViewA同步滚动 ScrollView(.horizontal) { LazyHGrid(rows: grids, spacing: 30) { ForEach(photoArray0) { item in ForEach(0..<10) { num in Image(item.imageName) .resizable() .aspectRatio(contentMode: .fit) .cornerRadius(8) } } } .padding() // 应用同步偏移量,实现滚动跟随 .offset(x: syncScrollOffset) } // 如果你需要反向同步(ScrollViewB滚动时带动ScrollViewA),可以添加下面这段监听 // .background(GeometryReader { proxy in // Color.clear.preference( // key: ScrollOffsetPreferenceKey.self, // value: -proxy.frame(in: .named("syncScrollSpace")).minX // ) // }) // .onPreferenceChange(ScrollOffsetPreferenceKey.self) { newValue in // syncScrollOffset = newValue // } // 原代码里的另一个水平ScrollView,保持不变 ScrollView(.horizontal) { LazyHGrid(rows: grids, spacing: 30) { ForEach(photoArray) { item in ForEach(0..<10) { num in Image(item.imageName) .resizable() .aspectRatio(contentMode: .fit) .cornerRadius(8) } } } .padding() } } } } } } struct PhotoData: Identifiable { var id = UUID() var imageName:String var title:String } var photoArray = [ PhotoData(imageName: "A", title: "a"), PhotoData(imageName: "B", title: "b"), PhotoData(imageName: "C", title: "c"), PhotoData(imageName: "D", title: "d"), PhotoData(imageName: "E", title: "e"), PhotoData(imageName: "F", title: "f"), PhotoData(imageName: "G", title: "g"), PhotoData(imageName: "A", title: "a"), PhotoData(imageName: "B", title: "b"), PhotoData(imageName: "C", title: "c"), PhotoData(imageName: "D", title: "d"), PhotoData(imageName: "E", title: "e"), PhotoData(imageName: "F", title: "f"), PhotoData(imageName: "G", title: "g"), ] var photoArray0 = [ PhotoData(imageName: "A", title: "a"), ] struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
关键部分解释:
- 自定义PreferenceKey:
ScrollOffsetPreferenceKey用来传递ScrollView的滚动偏移值,我们通过GeometryReader获取内容在指定坐标空间内的位置,计算出滚动的偏移量。 - 监听ScrollViewA的滚动:给ScrollViewA的内容添加背景
GeometryReader,捕获滚动偏移并通过Preference传递,然后更新syncScrollOffset状态变量。 - 同步ScrollViewB的滚动:给ScrollViewB的内容添加
.offset(x: syncScrollOffset),这样当syncScrollOffset变化时,ScrollViewB的内容会跟着移动,完美同步ScrollViewA的滚动位置。 - 可选反向同步:如果你需要ScrollViewB滚动时也带动ScrollViewA,只需要把注释掉的监听代码打开即可。
另外要注意:如果你的两个ScrollView的元素宽度或间距不一致,可能需要调整偏移量的计算比例,比如ScrollViewB的元素宽度是ScrollViewA的2倍,那么同步偏移量需要乘以2。不过在你的代码里,两个ScrollView的GridItem都是fixed(100),spacing也一致,所以直接同步偏移量就可以啦。
备注:内容来源于stack exchange,提问作者stock
相关产品推荐
相关产品推荐

