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

如何在垂直包含水平的嵌套ScrollView中编程滚动到指定单元格?

如何在垂直包含水平的嵌套ScrollView中编程滚动到指定单元格?

我明白你的痛点——嵌套ScrollView的滚动控制确实麻烦,尤其是不能手动计算偏移的情况下。结合你的需求(iOS16+兼容、保持嵌套结构),我整理了两种可行的方案,分别适配不同系统版本:


一、iOS 16 兼容方案(推荐)

这个方案基于ScrollViewReader,通过给目标元素添加唯一ID,分别控制水平和垂直方向的滚动:

步骤说明:

  1. 给目标元素添加唯一ID:给每个列的VStack添加列名作为ID(用于水平滚动定位),给每个单元格添加"列名-行号"格式的ID(用于垂直滚动定位)。
  2. 用单个ScrollViewReader包裹整个嵌套结构:这样代理可以访问所有嵌套视图的ID,无需分开处理两个ScrollView的代理。
  3. 依次触发滚动:先定位水平列,再定位垂直行(配合动画让过程更流畅)。

修改后的完整代码:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            NestedScrollingView()
        }
        .padding()
    }
}

struct NestedScrollingView: View {
    @StateObject var viewModel = NestedScrollingViewModel()
    let myMap = Dictionary(uniqueKeysWithValues: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"].map { ($0, Array(1...100)) })
    
    var body: some View {
        // 用ScrollViewReader包裹整个垂直ScrollView,统一管理滚动代理
        ScrollViewReader { proxy in
            ScrollView(.vertical, showsIndicators: false) {
                RoundedRectangle(cornerRadius: 8)
                    .foregroundStyle(Color.blue)
                    .frame(height: 100)
                    .padding()
                Button("Scroll to \(viewModel.targetKey):\(viewModel.targetRow)") {
                    viewModel.onClickScrollTo(proxy: proxy)
                }
                .buttonStyle(.borderedProminent)
                .padding()
                
                gridView
            }
        }
    }
    
    var gridView: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .top, spacing: 4) {
                ForEach(myMap.keys.sorted(), id: \.self) { key in
                    VStack(spacing: 4) {
                        Text(key)
                            .font(.caption)
                        
                        ForEach(myMap[key] ?? [], id: \.self) { value in
                            Text("\(key):\(value)")
                                .font(.caption)
                                .frame(width: 50, height: 50)
                                .border(Color.gray, width: 1)
                                .padding(4)
                                // 给单元格添加唯一ID,用于垂直滚动定位
                                .id("\(key)-\(value)")
                        }
                    }
                    // 给列的VStack添加ID,用于水平滚动定位
                    .id(key)
                }
            }
            .padding(8)
        }
    }
}

class NestedScrollingViewModel: ObservableObject {
    let targetKey = "K"
    let targetRow = 60
    
    func onClickScrollTo(proxy: ScrollViewProxy) {
        withAnimation {
            // 先水平滚动到目标列,确保列在视野内
            proxy.scrollTo(targetKey, anchor: .center)
            // 再垂直滚动到目标单元格,确保行在视野内
            proxy.scrollTo("\(targetKey)-\(targetRow)", anchor: .center)
        }
    }
}

为什么这个方案有效?

  • ScrollViewReader的代理可以访问其闭包内所有带有.id修饰符的视图,不管嵌套层级多深。
  • 分两步滚动先确保列可见,再定位行,避免因嵌套结构导致的滚动失效。
  • withAnimation让滚动过程更顺滑,提升用户体验。

二、iOS 17+ 优化方案(更简洁)

如果你的应用可以只适配iOS17及以上,那么可以用scrollPosition(id:)修饰符,代码更简洁直观:

修改后的核心代码:

struct NestedScrollingView: View {
    @StateObject var viewModel = NestedScrollingViewModel()
    @State private var horizontalScrollPosition: String?
    @State private var verticalScrollPosition: String?
    let myMap = Dictionary(uniqueKeysWithValues: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"].map { ($0, Array(1...100)) })
    
    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {
            RoundedRectangle(cornerRadius: 8)
                .foregroundStyle(Color.blue)
                .frame(height: 100)
                .padding()
            Button("Scroll to \(viewModel.targetKey):\(viewModel.targetRow)") {
                withAnimation {
                    // 直接设置绑定值触发滚动
                    horizontalScrollPosition = viewModel.targetKey
                    verticalScrollPosition = "\(viewModel.targetKey)-\(viewModel.targetRow)"
                }
            }
            .buttonStyle(.borderedProminent)
            .padding()
            
            gridView
        }
        // 绑定垂直滚动位置
        .scrollPosition(id: $verticalScrollPosition, anchor: .center)
    }
    
    var gridView: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .top, spacing: 4) {
                ForEach(myMap.keys.sorted(), id: \.self) { key in
                    VStack(spacing: 4) {
                        Text(key)
                            .font(.caption)
                        
                        ForEach(myMap[key] ?? [], id: \.self) { value in
                            Text("\(key):\(value)")
                                .font(.caption)
                                .frame(width: 50, height: 50)
                                .border(Color.gray, width: 1)
                                .padding(4)
                                .id("\(key)-\(value)")
                        }
                    }
                    .id(key)
                }
            }
            .padding(8)
        }
        // 绑定水平滚动位置
        .scrollPosition(id: $horizontalScrollPosition, anchor: .center)
    }
}

优势:

  • 无需手动管理ScrollViewProxy,直接通过绑定值控制滚动位置,代码更简洁。
  • 同样支持动画效果,滚动过程自然流畅。

注意事项:

  • 确保所有ID都是唯一的,否则滚动会出现定位异常。
  • 如果滚动后目标单元格没有完全符合预期位置,可以调整anchor参数(比如用.top或.bottom替代.center),根据你的UI需求灵活调整。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 18:10:29