I have some simple code with a TextField
, Picker
, and Button
. The Button
is only enabled when the TextField
has text and the Picker
has a selected value.
I have it structured such that the TextField
, Button
and a ViewBuilder
closure called pickerContent
are in the class TestView
. Then in the class TestViewWithPicker
, I call TestView
and implement the Picker
in its pickerContent
closure. If it seems like a convoluted set up, it's because it's an MRE of the real code from my app where it makes more sense. Here is the code:
import SwiftUI
struct TestView<PickerContent: View>: View {
@ViewBuilder var additionalEnterButtonDisabledConditions: () -> Bool
@ViewBuilder var pickerContent: () -> PickerContent
@State private var userInput: String = ""
var body: some View {
HStack {
TextField("", text: $userInput)
.textFieldStyle(.roundedBorder)
.padding()
pickerContent()
Button("Enter") {}
.disabled(userInput.isEmpty || additionalEnterButtonDisabledConditions())
.buttonStyle(.borderedProminent)
.padding()
}
}
}
struct TestViewWithPicker: View {
@State private var selectedPickerItem: String? = nil
var body: some View {
TestView(
additionalEnterButtonDisabledConditions: {
return selectedPickerItem == nil
},
pickerContent: {
PickerView(selectedPickerItem: $selectedPickerItem)
}
)
}
}
struct PickerView: View {
@Binding var selectedPickerItem: String?
private let pickerItems: [String] = ["Option A", "Option B", "Option C"]
var body: some View {
Picker("", selection: $selectedPickerItem) {
Text("Select Picker Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { pickerItem in
Text(pickerItem).tag(pickerItem as String?)
}
}
.pickerStyle(.menu)
.fixedSize(horizontal: true, vertical: true)
}
}
struct HomeView: View {
var body: some View {
TestViewWithPicker()
}
}
The problem I'm having is that my Button's
.disabled
condition is not activating unless a selection is made on the Picker
:
.disabled(self.userInput.isEmpty || self.additionalEnterButtonDisabledConditions())
To reproduce the bug, if you run the code and select a non-nil item on the Picker
, then type some text into the TextField
, the Button will not become enabled like it should. But if you select another item on the Picker, it will.
If you look at the code, you'll see that self.additionalEnterButtonDisabledConditions()
is implemented in TextViewWithPicker
to check if self.selectedPickerItem
is nil
. The reason the issue is occurring is because I'm sending self.selectedPickerItem
as a Binding
to PickerView
. If I don't implement the Picker in a separate class and put it directly in the pickerContent
closure (and hence, don't send self.selectedPickerItem
as a binding), the issue does not occur.
Does anyone know how to fix this so I can still use the current code structure?
This is due to the short-circuiting nature of ||
.
The right hand side of ||
is not evaluated at all, when the text field is empty. Therefore, SwiftUI fails to find selectedPickerItem
as a dependency of TestView
. When you enter text in the text field, only TestView
is updated. TestViewWithPicker
is not. Therefore, TestView
does not get a new additionalEnterButtonDisabledConditions
closure that captures the new value of selectedPickerItem
. It still has the old closure that captured the old value of selectedPickerItem
, which is nil.
So a simple fix is always evaluate the closure:
let additionalConditions = additionalEnterButtonDisabledConditions()
Button("Enter") {}
.disabled(userInput.isEmpty || additionalConditions)
.buttonStyle(.borderedProminent)
.padding()
I would also recommend just taking a simple Bool
, not () -> Bool
, and evaluating the picker body in init
.