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

SwiftUI中关闭视图后如何保留ScrollView的滚动位置?

问题:ScrollView返回后回到起始位置,如何保留选中项的滚动位置

我有一个水平ScrollView,遍历数字数组生成红色卡片。点击卡片时,卡片会展开单独显示。但关闭展开视图返回原状态时,整个视图会重新渲染,导致ScrollView回到起始位置(附示例动图)。希望返回后能保留选中元素的位置,不让ScrollView回到开头。

示例动图

struct TestView: View {
    
    @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    @State private var isAnimationActive = false
    @State private var selectedItem = ""
    @Namespace private var animation
    
    var body: some View {
        ZStack {
            if !isAnimationActive {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack {
                        ForEach(items, id: \.self) { item in
                            ZStack {
                                RoundedRectangle(cornerRadius: 30, style: .continuous)
                                    .fill(Color.red)
                                    .matchedGeometryEffect(id: "card\(item.description)", in: animation)
                                    .frame(width: 250, height: 250)
                                
                                if selectedItem == item.description && isAnimationActive {
                                    Rectangle()
                                        .fill(.green)
                                } else {
                                    Text(item.description)
                                        .foregroundColor(.white)
                                        .matchedGeometryEffect(id: "text\(item.description)", in: animation)
                                }
                            }
                            .onTapGesture {
                                withAnimation(.easeOut(duration: 0.3)) {
                                    isAnimationActive = true
                                    
                                    selectedItem = item.description
                                }
                            }
                        }
                    }
                    .padding(.leading, 20)
                }
            }
            
            if isAnimationActive {
                ZStack {
                    RoundedRectangle(cornerRadius: 30, style: .continuous)
                        .fill(Color.red)
                        .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation)
                        .frame(width: 300, height: 400)
                    
                    Text(selectedItem)
                        .foregroundColor(.white)
                        .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation)
                }
                .transition(.offset(x: 1, y: 1))
                .onTapGesture {
                    withAnimation(.easeOut(duration: 0.3)) {
                        isAnimationActive = false
                    }
                }
            }
        }
    }
}

解决方案

问题根源是isAnimationActive切换时,ScrollView被完全移除再重建,导致滚动位置丢失。以下两种方法可修复:

方法1:用透明度隐藏而非移除ScrollView

保留ScrollView的存在,通过透明度控制显隐,这样ScrollView的滚动状态会被完整保留:

struct TestView: View {
    
    @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    @State private var isAnimationActive = false
    @State private var selectedItem = ""
    @Namespace private var animation
    
    var body: some View {
        ZStack {
            // 保留ScrollView,用opacity控制显隐
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(items, id: \.self) { item in
                        ZStack {
                            RoundedRectangle(cornerRadius: 30, style: .continuous)
                                .fill(Color.red)
                                .matchedGeometryEffect(id: "card\(item.description)", in: animation)
                                .frame(width: 250, height: 250)
                            
                            if selectedItem == item.description && isAnimationActive {
                                Rectangle()
                                    .fill(.green)
                            } else {
                                Text(item.description)
                                    .foregroundColor(.white)
                                    .matchedGeometryEffect(id: "text\(item.description)", in: animation)
                            }
                        }
                        .onTapGesture {
                            withAnimation(.easeOut(duration: 0.3)) {
                                isAnimationActive = true
                                selectedItem = item.description
                            }
                        }
                    }
                }
                .padding(.leading, 20)
            }
            .opacity(isAnimationActive ? 0 : 1) // 核心修改:用透明度替代条件移除
            
            if isAnimationActive {
                ZStack {
                    RoundedRectangle(cornerRadius: 30, style: .continuous)
                        .fill(Color.red)
                        .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation)
                        .frame(width: 300, height: 400)
                    
                    Text(selectedItem)
                        .foregroundColor(.white)
                        .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation)
                }
                .transition(.offset(x: 1, y: 1))
                .onTapGesture {
                    withAnimation(.easeOut(duration: 0.3)) {
                        isAnimationActive = false
                    }
                }
            }
        }
    }
}

方法2:手动记录并恢复滚动位置

如果必须移除ScrollView,可借助ScrollViewReader记录选中项的位置,返回时自动滚动到目标位置:

struct TestView: View {
    
    @State private var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    @State private var isAnimationActive = false
    @State private var selectedItem = ""
    @Namespace private var animation
    
    var body: some View {
        ZStack {
            if !isAnimationActive {
                ScrollViewReader { proxy in
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(items, id: \.self) { item in
                                ZStack {
                                    RoundedRectangle(cornerRadius: 30, style: .continuous)
                                        .fill(Color.red)
                                        .matchedGeometryEffect(id: "card\(item.description)", in: animation)
                                        .frame(width: 250, height: 250)
                                    
                                    if selectedItem == item.description && isAnimationActive {
                                        Rectangle()
                                            .fill(.green)
                                    } else {
                                        Text(item.description)
                                            .foregroundColor(.white)
                                            .matchedGeometryEffect(id: "text\(item.description)", in: animation)
                                    }
                                }
                                .id("item_\(item)") // 给每个卡片添加唯一标识
                                .onTapGesture {
                                    withAnimation(.easeOut(duration: 0.3)) {
                                        isAnimationActive = true
                                        selectedItem = item.description
                                    }
                                }
                            }
                        }
                        .padding(.leading, 20)
                    }
                    .onAppear {
                        // 返回时自动滚动到选中的卡片位置
                        guard let selectedNum = Int(selectedItem), !selectedItem.isEmpty else { return }
                        proxy.scrollTo("item_\(selectedNum)", anchor: .center)
                    }
                }
            }
            
            if isAnimationActive {
                ZStack {
                    RoundedRectangle(cornerRadius: 30, style: .continuous)
                        .fill(Color.red)
                        .matchedGeometryEffect(id: "card\(selectedItem.description)", in: animation)
                        .frame(width: 300, height: 400)
                    
                    Text(selectedItem)
                        .foregroundColor(.white)
                        .matchedGeometryEffect(id: "text\(selectedItem.description)", in: animation)
                }
                .transition(.offset(x: 1, y: 1))
                .onTapGesture {
                    withAnimation(.easeOut(duration: 0.3)) {
                        isAnimationActive = false
                    }
                }
            }
        }
    }
}

方案对比

  • 方法1实现更简洁,动画连贯性更好,优先推荐。
  • 方法2适合需要完全移除ScrollView的场景,灵活性更高。

内容的提问来源于stack exchange,提问作者j-kobu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 19:47:57