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

