You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()
    }
}

关键部分解释:

  1. 自定义PreferenceKey:ScrollOffsetPreferenceKey用来传递ScrollView的滚动偏移值,我们通过GeometryReader获取内容在指定坐标空间内的位置,计算出滚动的偏移量。
  2. 监听ScrollViewA的滚动:给ScrollViewA的内容添加背景GeometryReader,捕获滚动偏移并通过Preference传递,然后更新syncScrollOffset状态变量。
  3. 同步ScrollViewB的滚动:给ScrollViewB的内容添加.offset(x: syncScrollOffset),这样当syncScrollOffset变化时,ScrollViewB的内容会跟着移动,完美同步ScrollViewA的滚动位置。
  4. 可选反向同步:如果你需要ScrollViewB滚动时也带动ScrollViewA,只需要把注释掉的监听代码打开即可。

另外要注意:如果你的两个ScrollView的元素宽度或间距不一致,可能需要调整偏移量的计算比例,比如ScrollViewB的元素宽度是ScrollViewA的2倍,那么同步偏移量需要乘以2。不过在你的代码里,两个ScrollView的GridItem都是fixed(100),spacing也一致,所以直接同步偏移量就可以啦。

备注:内容来源于stack exchange,提问作者stock

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 13:24:36