闭包强捕获@State导致View的body重渲染问题问询
场景与代码
我实现了一个简单的注册表单,点击「Continue Button」时,若未填写任何字段会弹出popover。代码如下:
struct ContentView: View { @State var firstName = "" @State var lastName = "" @State private var validationMessage = "" @State private var showValidationMessage = false private var isFormValid: Bool { !firstName.isEmpty && !lastName.isEmpty } private func checkGeneralFormCompletion() { if isFormValid { showValidationMessage = false } else { let errorText = { if firstName.isEmpty { return "First name empty" } else if lastName.isEmpty { return "Last name empty" } else { return "" } }() validationMessage = errorText showValidationMessage = !errorText.isEmpty } } var body: some View { VStack(spacing: 12) { let _ = Self._printChanges() TextField("First Name", text: $firstName) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.bottom, 10) TextField("Last Name", text: $lastName) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.bottom, 10) Button(action: { checkGeneralFormCompletion() }) { Text("Continue") .foregroundColor(.white) .padding(.horizontal, 45) .padding([.top, .bottom], 10) .background( Color.red) .cornerRadius(5) } .popover(isPresented: self.$showValidationMessage, attachmentAnchor: .point(.top), arrowEdge: .top, content: { [validationMessage] in let _ = print("validationMessage :: popover :: ", validationMessage) VStack { Text(validationMessage) } .multilineTextAlignment(.center) .lineLimit(0) .foregroundStyle(.black) .font(.system(size: 18, weight: .semibold, design: .rounded)) .padding() .presentationCompactAdaptation(.none) .fixedSize(horizontal: false, vertical: true) .frame(minWidth: 200) }) .padding(.top, 70) } .padding() // .onChange(of: validationMessage) { oldValue, newValue in // print("CHANGE text VALIDATE MSG") // } // .onChange(of: showValidationMessage) { oldValue, newValue in // print("CHANGE VALIDATE MSG") // } } }
现象说明
- 注释掉popover闭包中的强捕获
[validationMessage] in时,首次点击按钮,popover显示空文本,且body未触发重渲染(通过_printChanges输出验证)。 - 若在闭包中强捕获该
@State变量,或给VStack添加.onChange监听validationMessage/showValidationMessage,body会触发重渲染,popover也能显示正确文本。
核心问题解析
这本质是SwiftUI的依赖追踪机制在起作用:
未捕获
validationMessage时:
SwiftUI触发视图重渲染的逻辑是:只有当body中直接使用的@State变量发生变化时,才会触发更新。此时validationMessage只出现在popover的闭包里且未被显式捕获,SwiftUI无法追踪到这个变量与当前视图的依赖关系。虽然修改了showValidationMessage,但popover闭包初始化时捕获的是validationMessage的初始空值,后续变量更新后闭包不会重新获取最新值,同时body没重渲染,popover内容自然不会更新。显式捕获
validationMessage时:
在popover的content闭包中显式捕获validationMessage后,SwiftUI会将这个@State变量标记为当前视图的依赖项。当validationMessage被修改时,SwiftUI检测到视图依赖的状态变化,就会触发body重渲染。重渲染过程中,popover的content闭包会重新执行,捕获最新的validationMessage值,因此popover能显示正确文本。添加
.onChange监听的情况:.onChange修饰符会让SwiftUI把validationMessage/showValidationMessage标记为视图的依赖项,只要这些变量变化,就会触发视图重渲染,间接让popover闭包重新获取最新值。
优化建议
不需要显式捕获@State变量,更合理的做法是将popover内容抽成独立子View,通过参数传递validationMessage:
// 抽成独立子View struct PopoverContent: View { let message: String var body: some View { VStack { Text(message) } .multilineTextAlignment(.center) .lineLimit(0) .foregroundStyle(.black) .font(.system(size: 18, weight: .semibold, design: .rounded)) .padding() .presentationCompactAdaptation(.none) .fixedSize(horizontal: false, vertical: true) .frame(minWidth: 200) } } // 在原View中使用 .popover(isPresented: $showValidationMessage, attachmentAnchor: .point(.top), arrowEdge: .top) { PopoverContent(message: validationMessage) }
这种方式下,SwiftUI能正确追踪validationMessage的依赖,变量变化时子View会自动更新,还能避免不必要的父View重渲染。
内容的提问来源于stack exchange,提问作者duckSern1108

