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

SwiftUI中@Environment(\.dismiss)引发列表滚动时频繁重建的原因及解决方案

我来帮你分析这个问题并给出针对性的解决方案:

问题原因分析

当你在视图中声明@Environment(\.dismiss)时,这个属性会让视图绑定到SwiftUI环境中的dismiss闭包。在导航栈场景下,滚动列表的操作可能会间接触发导航相关的环境状态更新——比如系统调整导航栏的透明度、布局,或者更新导航栈的内部状态,这会导致dismiss闭包被重新生成(即使它的功能没有变化,但引用地址发生了改变)。

SwiftUI的视图重建机制会检测到这种依赖项的变化,因此会频繁重建DismissListView及其内部的List内容,这就是你看到body is rebuilding持续输出的原因。

而当某个TextField获得焦点后,@FocusState的状态成为了视图的主要依赖项,此时SwiftUI的优化机制会优先处理焦点相关的更新,滚动操作带来的环境变化不再触发视图的全量重建,所以打印会停止。

可行解决方案

我们的目标是:保留dismiss功能、实现焦点滚动、避免滚动时的频繁重建。这里有两种可靠的方案:

方案1:将dismiss闭包捕获到@State变量中

通过在视图出现时把环境中的dismiss闭包存储到@State变量里,让视图不再直接依赖环境变量,而是依赖不会频繁变化的@State值:

struct DismissListView: View {
    @Environment(\.dismiss) private var dismissEnv
    @State private var dismissAction: (() -> Void)?
    
    enum Field: Hashable {
        case line(Int)
    }
    
    @FocusState private var focus: Field?
    @State private var text: String = ""
    
    var body: some View {
        ScrollViewReader { proxy in
            List {
                let _ = print("body is rebuilding")
                Button("Dismiss me") {
                    dismissAction?()
                }
                Section("Section") {
                    ForEach((1...100), id: \.self) {num in
                        TextField("text", text: $text)
                            .id(Field.line(num))
                            .focused($focus, equals: .line(num))
                    }
                }
            }
            .listStyle(.insetGrouped)
            .onChange(of: focus) {_ in
                withAnimation {
                    proxy.scrollTo(focus, anchor: .center)
                }
            }
            .onAppear {
                // 在视图出现时捕获dismiss闭包
                dismissAction = { dismissEnv() }
            }
        }
    }
}

这样,视图不再直接依赖@Environment(\.dismiss),滚动时的环境更新不会触发视图重建,同时你仍然可以通过dismissAction调用关闭视图。

方案2:拆分视图,隔离环境依赖

把List的内容提取到一个独立的子视图中,让子视图不直接依赖@Environment(\.dismiss),而是通过参数接收dismiss闭包。这样即使父视图因为环境变化偶尔重建,子视图只要参数没有变化(闭包功能不变),SwiftUI会优化避免不必要的重建:

struct DismissListView: View {
    @Environment(\.dismiss) private var dismiss
    
    enum Field: Hashable {
        case line(Int)
    }
    
    @FocusState private var focus: Field?
    @State private var text: String = ""
    
    var body: some View {
        ScrollViewReader { proxy in
            ListContent(
                focus: $focus,
                text: $text,
                dismissAction: dismiss
            )
            .listStyle(.insetGrouped)
            .onChange(of: focus) {_ in
                withAnimation {
                    proxy.scrollTo(focus, anchor: .center)
                }
            }
        }
    }
}

// 子视图:仅依赖传递的参数,不直接引用环境
struct ListContent: View {
    @Binding var focus: DismissListView.Field?
    @Binding var text: String
    let dismissAction: () -> Void
    
    var body: some View {
        List {
            let _ = print("body is rebuilding")
            Button("Dismiss me") {
                dismissAction()
            }
            Section("Section") {
                ForEach((1...100), id: \.self) {num in
                    TextField("text", text: $text)
                        .id(DismissListView.Field.line(num))
                        .focused($focus, equals: .line(num))
                }
            }
        }
    }
}

这个方案通过视图拆分隔离了环境依赖,让List的内容视图只关注必要的状态和动作,减少了不必要的重建触发点。

两种方案都能解决你的问题,你可以根据实际代码的复杂度选择适合的方式。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 20:27:32