shduke
shduke

Reputation: 121

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)
    }
}

Upvotes: 0

Views: 56

Answers (2)

malhal
malhal

Reputation: 30746

Dependency tracking is broken, to fix it content() needs to be called in init and the result stored in a let. Or you could use ViewModifier instead of View which does it for you.

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

    init(…
        self.content = content()

Upvotes: 1

Sweeper
Sweeper

Reputation: 273540

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 {
    ...
}

Upvotes: 1

Related Questions