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

Posted

技术标签:

【中文标题】SwiftUI:更改数据源时选择器无法正确更新【英文标题】:SwiftUI : Picker does not update correctly when changing datasource 【发布时间】:2019-10-12 09:35:53 【问题描述】:

我刚开始学习 SwiftUI,但在某个地方卡住了!

我正在尝试在更改另一个段的值时更改段样式选择器数据源。但不知何故,它没有按预期工作!否则我可能编码错误。请问有大神能解答一下吗?

这是我的一段代码:

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

        
    


当我将段从Length 更改回Temperature 时,它会以某种方式合并数组。我尝试在日志中调试并打印 arrData 计数,然后它打印正确的结果但不更新 UI!

默认选择第一段:

更改细分:

将片段改回第一个:

任何帮助或建议将不胜感激。

【问题讨论】:

【参考方案1】:

Nick Polychronakis 在这个 fork 中解决了这个问题: https://github.com/nickpolychronakis/100DaysOfSwiftUI/tree/master/UnitCoverter

解决方案是将 .id(:identifier:) 添加到您的选择器中,使其独一无二。

可观察变量:

@State var unit = 0

主选择器:

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

内容由单元变量决定的二级选择器之一。

Picker("Length", selection: $inputUnit) 
                        ForEach(0 ..< selected.count) 
                            Text("\(self.selected[$0].description)")
                        
                    
                    .id(unit)

【讨论】:

我只有 1 个选择器,我发现如果您添加 id(unit),您将无法再滚动选择器,因为它会弹回零项。但是,如果您将 self.$unit 设置为 Picker 的选择,则它可以工作。我仍然认为这是一个错误,但这是一个明显的解决方法。 我明白了,tnx 分享! SwiftUI 真正棘手的一件事是,您永远无法确定它是否是一个错误,或者它应该像那样工作 XD【参考方案2】:

我不确定为什么 SwiftUI 会这样,对我来说似乎是一个错误(如果我错了,请纠正我)。我只能建议为温度和长度添加单独的选择器,并根据当前选择的类型隐藏它们。为了代码可重用性,我已将选择器添加到另一个文件中。

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

内容视图

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)
                    
                
            
        
    

注意:您必须使用不同的状态变量来跟踪温度和长度选择。

【讨论】:

感谢您的回复 :) 但是我发现@roblack 的解决方案如果我有更多类型而不是 2 种,则更容易和方便。【参考方案3】:

结合之前的两个答案:

内容视图

    ...

    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)
            

    ...

单位选择器

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)
    

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

【讨论】:

【参考方案4】:

以上答案仅供参考,不适用于 SwiftUI 中的 Wheelpickerstyle。单位计数将保持在初始值,因此如果您从温度开始,然后切换到长度,您将丢失长度数组的最后两个值。如果你走另一条路,你的应用程序将崩溃并越界。

我花了很长时间才找到解决方案。这似乎是 Wheelpickerstyle 中的一个错误。解决方法是更新选取器的 ID,提示它重新加载所有数据源。我在下面提供了一个示例。

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

【讨论】:

以上是关于SwiftUI:更改数据源时选择器无法正确更新的主要内容,如果未能解决你的问题,请参考以下文章

选择器更改时,SwiftUI 环境变量中的字符串插值未更新

如何在swiftui中使用选择器更改文本大小

SwiftUI 选择器在 ObservedObject 公共变量更新期间滚动时跳转

导航回视图时更新 SwiftUI 选择器最小值和最大值

Swiftui Multi Picker 以最小最大值更改选择

onAppear 不允许保存/更新选择器值的更改?