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

SwiftUI中多ScrollView切换时保存各自滚动位置的问题

解决SwiftUI切换ScrollView时滚动位置重置的问题

核心原因是切换视图时,未显示的ScrollView会被SwiftUI销毁,再次显示时重新创建,导致滚动位置丢失。下面提供两种解决方案:

方案一:iOS17+ 简洁实现(推荐)

利用iOS17新增的scrollPosition修饰符,直接绑定滚动位置,无需额外处理:

import SwiftUI

// 用枚举管理当前激活的滚动视图,替代两个Bool变量,更清晰
enum ActiveScrollView {
    case one, two
}

struct TestView: View { // SwiftUI视图命名建议首字母大写
    @StateObject var viewModel = Fetcher() // 类名同样建议首字母大写
    @State private var activeView: ActiveScrollView = .one
    
    // 存储两个滚动视图的滚动位置(绑定到列表元素的id)
    @State private var scrollPosition1: Int?
    @State private var scrollPosition2: Int?

    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button {
                    activeView = .one
                } label: {
                    ZStack{
                        Color.blue
                        Text("scroll view 1")
                            .foregroundColor(.white)
                    }
                    .frame(width: 100, height: 100)
                }
                Button {
                    activeView = .two
                } label: {
                    ZStack{
                        Color.blue
                        Text("scroll view 2")
                            .foregroundColor(.white)
                    }
                    .frame(width: 100, height: 100)
                }
                Spacer()
            }
            .padding(.bottom)
            
            if activeView == .one {
                ScrollView {
                    LazyVStack {
                        ForEach(viewModel.content.indices, id: \.self) { i in
                            SomeView(content: i) // 组件名首字母大写
                                .id(i) // 必须给每个元素添加唯一id,让scrollPosition识别位置
                        }
                    }
                    .scrollPosition(id: $scrollPosition1) // 绑定滚动位置
                }
            } else {
                ScrollView {
                    LazyVStack {
                        ForEach(viewModel.content.indices, id: \.self) { i in
                            SomeView(content: i)
                                .id(i)
                        }
                    }
                    .scrollPosition(id: $scrollPosition2)
                }
            }
        }
    }
}

// 补充示例所需的类和组件(原代码中缺失)
class Fetcher: ObservableObject {
    @Published var content = Array(0...100) // 模拟长列表数据
}

struct SomeView: View {
    let content: Int
    var body: some View {
        Text("Item \(content)")
            .frame(height: 50)
            .background(.gray.opacity(0.2))
            .padding(.horizontal)
    }
}

方案二:兼容iOS16及以下版本

通过ScrollViewReader和自定义PreferenceKey捕获滚动偏移,手动保存和恢复位置:

import SwiftUI

enum ActiveScrollView {
    case one, two
}

// 自定义PreferenceKey,用于捕获ScrollView的滚动偏移
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

struct TestView: View {
    @StateObject var viewModel = Fetcher()
    @State private var activeView: ActiveScrollView = .one
    
    // 存储两个滚动视图的滚动偏移
    @State private var scrollOffset1: CGFloat = 0
    @State private var scrollOffset2: CGFloat = 0

    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button {
                    activeView = .one
                } label: {
                    ZStack{
                        Color.blue
                        Text("scroll view 1")
                            .foregroundColor(.white)
                    }
                    .frame(width: 100, height: 100)
                }
                Button {
                    activeView = .two
                } label: {
                    ZStack{
                        Color.blue
                        Text("scroll view 2")
                            .foregroundColor(.white)
                    }
                    .frame(width: 100, height: 100)
                }
                Spacer()
            }
            .padding(.bottom)
            
            if activeView == .one {
                ScrollViewReader { proxy in
                    ScrollView {
                        LazyVStack {
                            ForEach(viewModel.content.indices, id: \.self) { i in
                                SomeView(content: i)
                            }
                        }
                        .background(
                            GeometryReader { geo in
                                Color.clear
                                    .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).minY)
                            }
                        )
                        .onPreferenceChange(ScrollOffsetKey.self) { offset in
                            // 实时保存滚动偏移
                            scrollOffset1 = -offset
                        }
                    }
                    .coordinateSpace(name: "scrollView")
                    .onAppear {
                        // 恢复滚动位置
                        withAnimation {
                            proxy.scrollTo(scrollOffset1, anchor: .top)
                        }
                    }
                }
            } else {
                ScrollViewReader { proxy in
                    ScrollView {
                        LazyVStack {
                            ForEach(viewModel.content.indices, id: \.self) { i in
                                SomeView(content: i)
                            }
                        }
                        .background(
                            GeometryReader { geo in
                                Color.clear
                                    .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView2")).minY)
                            }
                        )
                        .onPreferenceChange(ScrollOffsetKey.self) { offset in
                            scrollOffset2 = -offset
                        }
                    }
                    .coordinateSpace(name: "scrollView2")
                    .onAppear {
                        withAnimation {
                            proxy.scrollTo(scrollOffset2, anchor: .top)
                        }
                    }
                }
            }
        }
    }
}

// 补充示例类和组件
class Fetcher: ObservableObject {
    @Published var content = Array(0...100)
}

struct SomeView: View {
    let content: Int
    var body: some View {
        Text("Item \(content)")
            .frame(height: 50)
            .background(.gray.opacity(0.2))
            .padding(.horizontal)
    }
}

关键说明

  1. 用枚举替代两个Bool变量:避免状态冲突,代码更易维护。
  2. 保存滚动位置:每个ScrollView对应独立的位置变量,切换时不会互相干扰。
  3. 视图命名规范:Swift中类型名建议首字母大写,这是官方推荐的编码规范。

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 23:51:37