skip to Main Content

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


  1. SwiftUI has a dependency tracking feature. It will only call body again when a @State is set if its getter was previously called within body. Doing [validationMessage] in is faking a call to the getter which sets up the depdendency tracking and is why body 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 a Binding<Bool> computed var from the message to use with the popover or use the new popover(item:) instead.

    Login or Signup to reply.
  2. The core issue lies in how the showValidationMessage is being used as the trigger for the popover, 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:

    1. Use popover(item:)

    The popover(item:) API is designed for showing content based on an optional identifiable item. You can make validationMessage an optional String:

    @State private var validationMessage: String? = nil
    
    private func checkGeneralFormCompletion() {
        if isFormValid {
            validationMessage = nil
        } else {
            validationMessage = firstName.isEmpty ? "First name empty" : "Last name empty"
        }
    }
    
    .popover(item: $validationMessage) { message in
        VStack {
            Text(message)
        }
        .padding()
        .frame(minWidth: 200)
    }
    
    • This directly binds the validationMessage to the popover, and changes to it automatically handle showing/hiding the popover without needing a separate boolean flag.
    1. Use a Computed Binding for showValidationMessage

    Instead of explicitly managing showValidationMessage, derive it directly from validationMessage:

    private var showValidationMessage: Bool {
        !validationMessage.isEmpty
    }
    
    • Update checkGeneralFormCompletion to ensure validationMessage is the single source of truth:
    private func checkGeneralFormCompletion() {
        validationMessage = firstName.isEmpty ? "First name empty" : lastName.isEmpty ? "Last name empty" : ""
    }
    
    • Update popover to rely on showValidationMessage:
    .popover(isPresented: Binding<Bool>(
        get: { !validationMessage.isEmpty },
        set: { if !$0 { validationMessage = "" } }
    )) {
        VStack {
            Text(validationMessage)
        }
        .padding()
        .frame(minWidth: 200)
    }
    
    Login or Signup to reply.
  3. 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 of validationMessage 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-rendering

    When 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 of validationMessage 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

    1. 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.

    2. 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.

    3. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search