SwiftUI : Picker does not update correctly when changing datasource

前端 未结 4 1154
花落未央
花落未央 2020-12-30 11:40

I have just started learning SwiftUI and got stuck somewhere!

I am trying to change segment styled picker datasource when changing value of another segment. But some

相关标签:
4条回答
  • 2020-12-30 12:24

    Nick Polychronakis solved it in this fork: https://github.com/nickpolychronakis/100DaysOfSwiftUI/tree/master/UnitCoverter

    The solution is to add .id(:identifier:) to your picker so it is unique.

    Observable var:

    @State var unit = 0
    

    Main picker:

    Picker("Length", selection: $unit) {
                        ForEach(0 ..< inputUnitTypes.count) {
                            Text("\(self.inputUnitTypes[$0].description)")
                        }
                    }
                    .pickerStyle(SegmentedPickerStyle())
    

    One of secondary pickers which content is determined by the unit variable.

    Picker("Length", selection: $inputUnit) {
                            ForEach(0 ..< selected.count) {
                                Text("\(self.selected[$0].description)")
                            }
                        }
                        .id(unit)
    
    0 讨论(0)
  • 2020-12-30 12:39

    FYI above answers do not work for the Wheelpickerstyle in SwiftUI. The count of units will stay at the initial value, so if you start with Temperature and then switch to Length, you will be missing the last two values of Length array. If you go the other way, your app will crash with an out of bounds.

    It took me forever to work out a solution. It seems it is a bug in the Wheelpickerstyle. The workaround is to update the ID of the picker, which prompts it to reload all data sources. I've included an example below.

    import SwiftUI  
    
    // Data  
    struct Item: Identifiable {  
        var id = UUID()  
        var category:String  
        var item:String  
    }  
    let myCategories:[String] = ["Category 1","Category 2"]  
    let myItems:[Item] = [  
        Item(category: "Category 1", item: "Item 1.1"),  
        Item(category: "Category 1", item: "Item 1.2"),  
        Item(category: "Category 2", item: "Item 2.1"),  
        Item(category: "Category 2", item: "Item 2.2"),  
        Item(category: "Category 2", item: "Item 2.3"),  
        Item(category: "Category 2", item: "Item 2.4"),  
    ]  
    
    // Factory  
    class MyObject: ObservableObject {  
        // Category picker variables  
        @Published var selectedCategory:String = myCategories[0]  
        @Published var selectedCategoryItems:[Item] = []  
        @Published var selectedCategoryInt:Int = 0 {  
            didSet {  
                selectCategoryActions(selectedCategoryInt)  
            }  
        }  
        // Item picker variables  
        @Published var selectedItem:Item = myItems[0]  
        @Published var selectedItemInt:Int = 0 {  
            didSet {  
                selectedItem = selectedCategoryItems[selectedItemInt]  
            }  
        }  
        @Published var pickerId:Int = 0  
        // Initial category selection  
        init() {  
            selectCategoryActions(selectedCategoryInt)  
        }  
        // Actions when selecting a new category  
        func selectCategoryActions(_ selectedCategoryInt:Int) {  
            selectedCategory = myCategories[selectedCategoryInt]  
            // Get items in category  
            selectedCategoryItems = myItems.filter{ $0.category.contains(selectedCategory)}  
            // Select initial item in category  
            let selectedItemIntWrapped:Int? = myItems.firstIndex { $0.category == selectedCategory }  
            if let selectedItemInt = selectedItemIntWrapped {  
                self.selectedItem = myItems[selectedItemInt]  
            }  
            self.pickerId += 1 // Hack to change ID of picker. ID is updated to force refresh  
        }  
    }  
    
    // View  
    struct ContentView: View {  
        @ObservedObject var myObject = MyObject()  
    
        var body: some View {  
                VStack(spacing: 10) {  
                    Section(header: Text("Observable Object")) {  
                        Text("Selected category: \(myObject.selectedCategory)")  
                        Text("Items in category: \(myObject.selectedCategoryItems.count)")  
                        Text("PickerId updated to force refresh  \(myObject.pickerId)")  
                        Text("Selected item: \(myObject.selectedItem.item)")  
                        Picker(selection: self.$myObject.selectedCategoryInt, label: Text("Select category")) {  
                            ForEach(0 ..< myCategories.count, id: \.self) {  
                                Text("\(myCategories[$0])")  
                            }  
                        }.labelsHidden()  
    
                        Picker(selection: self.$myObject.selectedItemInt, label: Text("Select object item")) {  
                            ForEach(0 ..< self.myObject.selectedCategoryItems.count, id: \.self) {  
                                Text("\(self.myObject.selectedCategoryItems[$0].item)")  
                            }  
                        }  
                        .labelsHidden()  
                        .id(myObject.pickerId) // Hack to get picker to reload data. ID is updated to force refresh.  
                    }  
                    Spacer()  
                }  
        }  
    }  
    
    struct ContentView_Previews: PreviewProvider {  
        static var previews: some View {  
            ContentView()  
        }  
    }  
    
    0 讨论(0)
  • 2020-12-30 12:43

    I'm not sure why SwiftUI behaves like this, seems like a bug to me (Correct me if I'm wrong). All I can suggest is to add separate pickers for temperature and length and hide those based on the current selected type. For code re-usability I've added the picker to another file.

    MyCustomPicker

    struct MyCustomPicker: View {
        var pickerData: [String]
        @Binding var binding: Int
        var body: some View {
            Picker("Convert", selection: $binding) {
                ForEach(0 ..< pickerData.count, id: \.self)
                { i in
                    Text(self.pickerData[i])
                }
            }
            .pickerStyle(SegmentedPickerStyle())
        }
    }
    

    ContentView

    struct ContentView: View {
    
        @State var selectedType = 0
        @State var inputTempUnit = 0
        @State var outputTempUnit = 1
        @State var inputLenUnit = 0
        @State var outputLenUnit = 1
    
        let arrTypes = ["Temperature", "Length"]
        let tempData = ["Celsius", "Fahrenheit", "Kelvin"]
        let lenData  = ["meters", "kilometers", "feet", "yards", "miles"]
    
    
        var body: some View {
            NavigationView {
                Form {
                    Section(header: Text("Choose type")) {
                        MyCustomPicker(pickerData: arrTypes, binding: $selectedType)
                    }
    
                    Section(header: Text("From")) {
                        if selectedType == 0 {
                            MyCustomPicker(pickerData: tempData, binding: $inputTempUnit)
                        } else {
                            MyCustomPicker(pickerData: lenData, binding: $inputLenUnit)
                        }
                    }
    
                    Section(header: Text("To")) {
                        if selectedType == 0 {
                            MyCustomPicker(pickerData: tempData, binding: $outputTempUnit)
                        } else {
                            MyCustomPicker(pickerData: lenData, binding: $outputLenUnit)
                        }
                    }
                }
            }
        }
    }
    

    Note: You have to use different state variables to keep track the temperature and length selection.

    0 讨论(0)
  • 2020-12-30 12:43

    Combining the two earlier answers:

    ContentView

        ...
    
        var units: [String] {
            symbols[unitType]
        }
    
        ...
    
                Section(header: Text("Unit Type")) {
                    UnitPicker(units: unitTypes, unit: $unitType)
                }
    
                Section(header: Text("From Unit")) {
                    UnitPicker(units: units, unit: $inputUnit)
                        .id(unitType)
                }
    
                Section(header: Text("To Unit")) {
                    UnitPicker(units: units, unit: $outputUnit)
                        .id(unitType)
                }
    
        ...
    

    UnitPicker

    struct UnitPicker: View {
        var units: [String]
    
        @Binding var unit: Int
    
        var body: some View {
            Picker("", selection: $unit) {
                ForEach(units.indices, id: \.self) { index in
                    Text(self.units[index]).tag(index)
                }
            }
            .pickerStyle(SegmentedPickerStyle())
            .font(.largeTitle)
        }
    }
    

    See https://github.com/hugofalkman/UnitConverter.git

    0 讨论(0)
提交回复
热议问题