简体   繁体   English

SwiftUI:更改数据源时选择器未正确更新

[英]SwiftUI : Picker does not update correctly when changing datasource

I have just started learning SwiftUI and got stuck somewhere!我刚开始学习 SwiftUI 并且卡在了某个地方!

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.当我将段从Length更改回Temperature时,它会以某种方式合并数组。 I tried to debug and print the arrData count in log, then it prints correct result but not updating the UI!我尝试调试并在日志中打印arrData计数,然后它打印出正确的结果但不更新 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 Nick Polychronakis 在这个分支中解决了它: https://github.com/nickpolychronakis/100DaysOfSwiftUI/tree/master/UnitCoverter

The solution is to add.id(:identifier:) to your picker so it is unique.解决方案是将 add.id(:identifier:) 添加到您的选择器中,使其独一无二。

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).我不确定为什么 SwiftUI 会这样,对我来说似乎是一个错误(如果我错了,请纠正我)。 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.注意:您必须使用不同的 state 变量来跟踪温度和长度选择。

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.githttps://github.com/hugofalkman/UnitConverter.git

FYI above answers do not work for the Wheelpickerstyle in SwiftUI.仅供参考,以上答案不适用于 SwiftUI 中的 Wheelpickerstyle。 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.如果您以另一种方式 go ,您的应用程序将因越界而崩溃。

It took me forever to work out a solution.我花了很长时间才找到解决方案。 It seems it is a bug in the Wheelpickerstyle.这似乎是 Wheelpickerstyle 中的一个错误。 The workaround is to update the ID of the picker, which prompts it to reload all data sources.解决方法是更新选取器的 ID,提示它重新加载所有数据源。 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).只需在 ForEach (Picker) 中将标签设置为您的文本。

.tag(String?.some(item))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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