Search code examples
swiftpicker

Updating Picker on update of ObservedObject


I'm trying up dynamically add new rows to a Picker as follows:

class ViewModel: ObservableObject {    
    @Published private (set) var drinks = ["Tea", "Coffee", "Wine"]

    func addDrink(_ drink: String) {
        drinks.append(drink)
    }
}

struct PickerTest: View {

    @State private var selectedDrink = "Tea"
    @State private var customDrink = ""
    @ObservedObject private var viewModel = ViewModel()

    var body: some View {
        VStack {
            HStack {
                TextField("Enter a drink", text: $customDrink)
                Spacer()
                Button("Add") {
                    self.viewModel.addDrink(self.customDrink)
                }
            }
            Picker("Drinks", selection: $selectedDrink) {  // Removing the wrapping Picker works
                ForEach(viewModel.drinks, id: \.self) { drink in
                    Text(drink)
                }
            }
        }.padding().labelsHidden()
    }
}

This doesn't work. If I remove the Picker wrapping the ForEach, the ForEach updates as expected.

Is there a way to update the Picker dynamically?


Solution

  • It looks like Pickers bug - I hope, that Apple fixes it in future releases of SwiftUI.

    I found ugly (I really don't like it) workaround for this problem:

    class ViewModel: ObservableObject {
        @Published var selectedDrink = "Tea"
        @Published var drinks = ["Tea", "Coffee", "Wine"]
        @Published var drinksChanged = true
    
        func addDrink(_ drink: String) {
            drinks.append(drink)
            drinksChanged.toggle()
        }
    }
    
    struct DrinksPicker: View {
        @ObservedObject var viewModel: ViewModel
    
        var body: some View {
            Picker("Drinks", selection: $viewModel.selectedDrink) {
                ForEach(viewModel.drinks, id: \.self) { drink in
                    Text(drink)
                }
            }
        }
    }
    
    struct PickerTest: View {
        @State private var customDrink = ""
        @ObservedObject private var viewModel = ViewModel()
    
        var body: some View {
            VStack {
                HStack {
                    TextField("Enter a drink", text: $customDrink)
                    Spacer()
                    Button("Add") {
                        self.viewModel.addDrink(self.customDrink)
                        self.customDrink = ""
                    }
                }
                if viewModel.drinksChanged {
                    DrinksPicker(viewModel: viewModel)
                } else {
                    DrinksPicker(viewModel: viewModel)
                }
            }.padding().labelsHidden()
        }
    }
    

    You can also hide this if-else in some another container:

    struct DrinksPickerContainer: View {
        @ObservedObject var viewModel: ViewModel
    
        var body: some View {
            Group {
                if viewModel.drinksChanged {
                    DrinksPicker(viewModel: viewModel)
                } else {
                    DrinksPicker(viewModel: viewModel)
                }
            }
        }
    }
    

    and then use only DrinksPickerContainer(viewModel: viewModel) in PickerTest