swiftuibinding

ViewBuilder content not updating on Binding change in SwiftUI


I have an Optional config isAttending which starts out as nil. When the "+" button is pressed it should set that value to a provided a default value and the Picker should show that as the selected value.

However on "+" button press, the Picker is not highlighting the selected value. Current behavior:

enter image description here

Desired behavior:

enter image description here

Note: If some other UI component changes then the Picker gets updated to the correct visual state in that render cycle

import SwiftUI

struct QuestionView<Option, Content: View>: View {
    @Binding var option: Option?
    let defaultOption: Option?
    @ViewBuilder let content: () -> Content

    var body: some View {
        VStack {
            if let _ = option {
                content()
            } else {
                Button {
                    option = defaultOption
                } label: {
                    Label("Add Filter Option", systemImage: "plus")
                        .labelStyle(.iconOnly)
                }
            }
        }
    }
}

struct Config {
    var isAttending: Bool?
}

#Preview {
    @Previewable @State var config: Config = Config()
    QuestionView(option: $config.isAttending, defaultOption: false) {
        Picker("RSVP", selection: $config.isAttending) {
            Text("Attending").tag(true)
            Text("Not Attending").tag(false)
        }
        .pickerStyle(.segmented)
    }
}

Solution

  • This is probably due to some quirks of the underlying UIKit UISegmentedControl that is backing the SwiftUI Picker. The .wheel picker style behaves correctly.

    Extracting the Picker into its own view also works correctly.

    struct AttendPicker: View {
        @Binding var isAttending: Bool?
        
        var body: some View {
            Picker("RSVP", selection: $isAttending) {
                Text("Attending").tag(true)
                Text("Not Attending").tag(false)
            }
            .pickerStyle(.segmented)
        }
    }
    
    QuestionView(option: $config.isAttending, defaultOption: false) {
        AttendPicker(isAttending: $config.isAttending)
    }
    

    Side note: though it is unrelated to this specific problem, I'd suggest calling the content closure of QuestionView in init. This makes SwiftUI eagerly pick up all the dependencies the content has.

    @Binding var option: Option?
    let defaultOption: Option?
    let content: Content // <----
    
    init(option: Binding<Option?>, defaultOption: Option?, @ViewBuilder content: () -> Content) {
        self._option = option
        self.defaultOption = defaultOption
        self.content = content() // <----
    }
    
    // ...
    
    if let _ = option {
        content // <----
    } else {
        ...
    }