I have a simple register form, when user press Continue Button
, I will show a popover if they not type in any fields.
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")
// }
}
}
If I comment out the strong capture [validationMessage] in
, when user first press button, popover will show an empty text and body
is not rerender ( base on _printChanges
call ). But if I provide strong capture to closure
of .popover
or add .onChange
modifier to VStack
, the body will be rerender and popover will show correct text.
My question is why when I provide strong capture of @State
, it cause body
to rerender? Any insights or suggestions would be appreciated!
3
Answers
SwiftUI has a dependency tracking feature. It will only call
body
again when a@State
is set if its getter was previously called withinbody
. Doing[validationMessage] in
is faking a call to the getter which sets up the depdendency tracking and is whybody
is called when it changes. Usually this means your source of truth is wrong. Seems in your case you are using a boolean var as source of truth but it should actually when there is a message to show. You can either make aBinding<Bool>
computed var from the message to use with the popover or use the newpopover(item:)
instead.The core issue lies in how the
showValidationMessage
is being used as the trigger for thepopover
, while the actual meaningful state (validationMessage
) isn’t consistently integrated into SwiftUI’s dependency system.The current approach (forcing strong capture) is a workaround rather than a clean design. Instead, consider the following solutions:
popover(item:)
The
popover(item:)
API is designed for showing content based on an optional identifiable item. You can makevalidationMessage
an optionalString
:showValidationMessage
Instead of explicitly managing
showValidationMessage
, derive it directly fromvalidationMessage
:checkGeneralFormCompletion
to ensurevalidationMessage
is the single source of truth:popover
to rely onshowValidationMessage
:The issue you’re observing is related to SwiftUI’s state binding and how closures work in combination with
@State
variables.In SwiftUI,
@State
variables are used to trigger view updates when their values change. When you use@State
, SwiftUI automatically monitors the state and re-renders the view when its value changes.Now, when you pass
@State
variables directly into closures (like the ones used in.popover
), you need to understand the concept of capturing values in closures. When you capture a value strongly in a closure (like[validationMessage]
), you’re explicitly keeping a reference to the current value ofvalidationMessage
at the time the closure is executed.Why the body is not rerendering without strong capture
When you do not use a strong capture, SwiftUI does not automatically know that the
validationMessage
state has changed within the closure, and hence it does not trigger a view update. This is because closures do not implicitly observe changes to state unless they are explicitly marked to do so. The body won’t re-render unless SwiftUI is aware of changes to any observable values that affect the view. Without the strong capture, you’re not keeping a direct reference to the state inside the closure, so SwiftUI doesn’t know that it should refresh the view.Why using strong capture (
[validationMessage]
) causes re-renderingWhen you strongly capture the
@State
variable inside the closure, you’re explicitly telling SwiftUI to track that specific piece of state within the closure. As a result, when the value ofvalidationMessage
changes, SwiftUI knows that the view needs to be updated, because the closure is referencing a part of the view’s state.This behavior is necessary for SwiftUI to efficiently manage view updates. Since the
@State
variable is bound to the view, any changes to it should automatically trigger a re-render, but if it’s being captured strongly inside a closure, that closure will "observe" it directly, and thus, any change in the state will cause the view to update accordingly.Key Insights
State Binding: When using
@State
in SwiftUI, changes to the state variables automatically cause the view to update, but only if the state is directly referenced in the body of the view or passed into closures with strong capture.Closure Capture: If a closure (like the one in
.popover
) is capturing a@State
variable, and that state changes, SwiftUI needs to know that the closure is tied to a state variable. By using strong capture ([validationMessage]
), you’re ensuring that SwiftUI is aware of the change.Avoid Unnecessary Closures: If you’re not using the captured value inside the closure directly, you may not need the strong capture. However, in cases where you need the closure to react to state changes, strong capture or state-binding is required.
Suggested Approach
You should only use strong capture for state variables that are directly used inside the closure and are intended to trigger view updates. If you do not need a variable inside the closure, you can avoid strong capture to prevent unnecessary updates.
For example, if you’re only displaying
validationMessage
inside the popover and don’t need it in other parts of the closure, you can safely capture it as shown in your code. If you need to modify it or react to changes in a more complex way, you might want to keep that reference.Alternatively, you can use
.onChange
modifiers to observe changes to the state, which will trigger re-renders when the state changes.