简体   繁体   中英

SwiftUI : Picker does not update correctly when changing datasource

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 somehow it is not working as expected. Or else I might have coded something wrong? Can anyone figure it out please?

Here is my piece of code:

import SwiftUI

struct ContentView: View {    

@State var selectedType = 0
@State var inputUnit = 0
@State var outputUnit = 1

let arrTypes = ["Temperature", "Length"]

var arrData: [String] {
    switch self.selectedType {
    case 0:
        return ["Celsius", "Fahrenheit", "Kelvin"] //Temperature
    case 1:
        return ["meters", "kilometers", "feet", "yards", "miles"] //Length        
    default:
        return ["Celsius", "Fahrenheit", "Kelvin"]
    }        
}


var body: some View {
    NavigationView{
        Form
        {
            Section(header: Text("Choose type"))
            {
                Picker("Convert", selection: $selectedType) {
                    ForEach(0 ..< 2, id: \.self)
                    { i in
                        Text(self.arrTypes[i])
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
            }

            Section(header: Text("From"))
            {
                Picker("", selection: $inputUnit) {
                    ForEach(0 ..< arrData.count, id: \.self)
                    {
                        Text(self.arrData[$0])
                    }
                }
                .pickerStyle(SegmentedPickerStyle())                    
            }

            Section(header: Text("To"))
            {
                Picker("", selection: $outputUnit) {
                    ForEach(0 ..< arrData.count, id: \.self)
                    {
                        Text(self.arrData[$0])
                    }
                }
                .pickerStyle(SegmentedPickerStyle())
            }                

        }
    }
}
}

When I change segment from Length back to Temperature it merges the array somehow. I tried to debug and print the arrData count in log, then it prints correct result but not updating the UI!

First segment selected by default: 在此处输入图像描述

Change segment:

在此处输入图像描述

Change segment back to first:

在此处输入图像描述

Any help or suggestion would greatly be appreciated.

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)

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.

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

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

Just set tag to your Text inside ForEach (Picker).

.tag(String?.some(item))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM