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

SwiftUI中切换横向列表显示状态时如何保留滚动位置?

解决SwiftUI横向列表切换时滚动位置保存问题

你的核心问题在于错误获取滚动偏移量,且未正确利用ScrollViewReader恢复位置。以下是修正方案:

问题分析

  1. 原代码通过proxy.frame(in: .named("vertical_offset")).minX获取的是内容相对于垂直ScrollView的偏移,并非横向列表自身的滚动偏移,数值完全无效。
  2. 恢复滚动时仅修改状态变量,未通过ScrollViewReader的scrollTo方法主动触发滚动。
  3. 两个列表的滚动位置管理逻辑混乱,未独立维护各自状态。

修正实现

我们需要:

  • 正确捕获横向ScrollView的滚动位置
  • 用ScrollViewReader精准恢复滚动状态
  • 为每个列表单独保存滚动位置
import SwiftUI

struct ContentView: View {
    @State private var isList1Visible = true
    // 用元素ID记录滚动位置(比CGFloat更可靠,适配动态内容)
    @State private var list1ScrollID: Int = 0
    @State private var list2ScrollID: Int = 0
    
    var body: some View {
        ScrollView {
            VStack {
                Image(systemName: "person.crop.circle.fill")
                    .resizable()
                    .frame(width: 100, height: 100)
                    .padding()
                
                Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed in velit magna. Donec laoreet felis sapien, vel pulvinar orci elementum quis.")
                    .padding()
                
                // 第一个横向列表
                if isList1Visible {
                    ScrollViewReader { proxy in
                        ScrollView(.horizontal) {
                            HStack(spacing: 10) {
                                ForEach(0..<10) { index in
                                    Text("\(index)")
                                        .frame(width: 100, height: 100)
                                        .background(Color.blue)
                                        .cornerRadius(10)
                                        .id(index)
                                }
                            }
                            .padding(.horizontal, 10)
                        }
                        .frame(height: 150)
                        .onAppear {
                            // 恢复滚动位置
                            proxy.scrollTo(list1ScrollID, anchor: .leading)
                        }
                        // 实时捕获可见元素ID
                        .overlay(
                            GeometryReader { geo in
                                Color.clear
                                    .preference(key: VisibleElementKey.self, value: getVisibleFirstElement(in: geo))
                            }
                        )
                        .onPreferenceChange(VisibleElementKey.self) { id in
                            guard let id = id else { return }
                            list1ScrollID = id
                        }
                    }
                }
                
                Text("Aliquam eget semper ipsum, quis finibus quam. Nulla facilisi. Praesent rutrum sapien eu tortor commodo fringilla.")
                    .padding()
                
                // 第二个横向列表
                if !isList1Visible {
                    ScrollViewReader { proxy in
                        ScrollView(.horizontal) {
                            HStack(spacing: 10) {
                                ForEach(0..<10) { index in
                                    Text("\(index)")
                                        .frame(width: 100, height: 100)
                                        .background(Color.green)
                                        .cornerRadius(10)
                                        .id(index)
                                }
                            }
                            .padding(.horizontal, 10)
                        }
                        .frame(height: 150)
                        .onAppear {
                            proxy.scrollTo(list2ScrollID, anchor: .leading)
                        }
                        .overlay(
                            GeometryReader { geo in
                                Color.clear
                                    .preference(key: VisibleElementKey.self, value: getVisibleFirstElement(in: geo))
                            }
                        )
                        .onPreferenceChange(VisibleElementKey.self) { id in
                            guard let id = id else { return }
                            list2ScrollID = id
                        }
                    }
                }
                
                Text("Etiam eget orci dolor. Fusce eu sapien euismod, pharetra est eget, consequat libero. Sed sed tristique nibh.")
                    .padding()
                
                Button(action: {
                    isList1Visible.toggle()
                }) {
                    Text(isList1Visible ? "显示列表2" : "显示列表1")
                }
                
                Spacer()
            }
        }
    }
    
    // 自定义PreferenceKey,传递可见元素ID
    struct VisibleElementKey: PreferenceKey {
        static var defaultValue: Int? = nil
        static func reduce(value: inout Int?, nextValue: () -> Int?) {
            value = value ?? nextValue()
        }
    }
    
    // 获取可见区域内的第一个元素ID
    private func getVisibleFirstElement(in geo: GeometryProxy) -> Int? {
        let visibleRect = geo.frame(in: .local)
        for index in 0..<10 {
            let elementFrame = geo.frame(in: .local, for: index)
            if visibleRect.intersects(elementFrame) {
                return index
            }
        }
        return nil
    }
}

关键改进点

  1. 元素ID替代偏移量:相比直接存储CGFloat偏移,记录元素ID更稳定,避免布局变化导致的偏移误差。
  2. 精准捕获可见元素:通过GeometryReader和自定义PreferenceKey,实时获取当前可见的第一个元素ID。
  3. 主动恢复滚动状态:在列表onAppear时,调用ScrollViewReader的scrollTo方法,精准滚动到保存的位置。
  4. 独立状态隔离:两个列表的滚动位置分别存储,切换时互不干扰。

若你偏好使用偏移量而非元素ID,可修改捕获逻辑为获取横向ScrollView的contentOffset,但元素ID的方式在动态内容场景下适配性更强。

内容的提问来源于stack exchange,提问作者Xavier Duvan Melo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 19:48:17