【问题标题】:Picker for optional data type in SwiftUI?SwiftUI 中可选数据类型的选择器?
【发布时间】:2020-04-08 09:54:42
【问题描述】:

通常我可以在 SwiftUI 中显示这样的项目列表:

enum Fruit {
    case apple
    case orange
    case banana
}

struct FruitView: View {

    @State private var fruit = Fruit.apple

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit)
            }
        }
    }
}

这非常有效,让我可以选择我想要的任何水果。但是,如果我想将 fruit 切换为可为空(又名可选),则会导致问题:

struct FruitView: View {

    @State private var fruit: Fruit?

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit)
            }
        }
    }
}

选择的水果名称不再显示在第一个屏幕上,无论我选择什么选择项,它都不会更新水果值。

如何使用带有可选类型的 Picker?

【问题讨论】:

  • 你能分享你的Fruit class/struct
  • @SimonBachmann:这是一个枚举,更新了问题

标签: swiftui


【解决方案1】:

通过阅读Jim Doveythis blog,我几乎了解了有关 SwiftUI 绑定(使用核心数据)的所有知识。剩下的就是一些研究和几个小时的错误。

所以当我使用 Jim 的技术在 SwiftUI Binding 上创建 Extensions 时,我们最终会得到这样的结果......

public extension Binding where Value: Equatable {
    init(_ source: Binding<Value>, deselectTo value: Value) {
        self.init(get: { source.wrappedValue },
                  set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
        )
    }
}

然后可以像这样在整个代码中使用...

Picker("country", selection: Binding($selection, deselectTo: nil)) { ... }

Picker("country", selection: Binding($selection, deselectTo: someOtherValue)) { ... }

或者当使用.pickerStyle(.segmented)

Picker("country", selection: Binding($selection, deselectTo: -1)) { ... }

根据UISegmentedControlselectedSegmentIndex 的文档,将分段样式选择器的index 设置为-1。

默认值是 noSegment (不选择段),直到用户 触及一个片段。将此属性设置为 -1 以关闭当前 选择。

【讨论】:

    【解决方案2】:

    我实际上更喜欢 @Senseful 的点解决方案,但为了后代:您还可以创建一个包装器枚举,如果您的应用中有大量实体类型,它可以通过协议扩展很好地扩展。

    // utility constraint to ensure a default id can be produced
    protocol EmptyInitializable {
        init()
    }
    
    // primary constraint on PickerValue wrapper
    protocol Pickable {
        associatedtype Element: Identifiable where Element.ID: EmptyInitializable
    }
    
    // wrapper to hide optionality
    enum PickerValue<Element>: Pickable where Element: Identifiable, Element.ID: EmptyInitializable {
        case none
        case some(Element)
    }
    
    // hashable & equtable on the wrapper
    extension PickerValue: Hashable & Equatable {
        func hash(into hasher: inout Hasher) {
            hasher.combine(id)
        }
        
        static func ==(lhs: Self, rhs: Self) -> Bool {
            lhs.id == rhs.id
        }
    }
    
    // common identifiable types
    extension String: EmptyInitializable {}
    extension Int: EmptyInitializable {}
    extension UInt: EmptyInitializable {}
    extension UInt8: EmptyInitializable {}
    extension UInt16: EmptyInitializable {}
    extension UInt32: EmptyInitializable {}
    extension UInt64: EmptyInitializable {}
    extension UUID: EmptyInitializable {}
    
    // id producer on wrapper
    extension PickerValue: Identifiable {
        var id: Element.ID {
            switch self {
                case .some(let e):
                    return e.id
                case .none:
                    return Element.ID()
            }
        }
    }
    
    // utility extensions on Array to wrap into PickerValues
    extension Array where Element: Identifiable, Element.ID: EmptyInitializable {
        var pickable: Array<PickerValue<Element>> {
            map { .some($0) }
        }
        
        var optionalPickable: Array<PickerValue<Element>> {
            [.none] + pickable
        }
    }
    
    // benefit of wrapping with protocols is that item views can be common
    // across data sets.  (Here TitleComponent { var title: String { get }})
    extension PickerValue where Element: TitleComponent {
        @ViewBuilder
        var itemView: some View {
            Group {
                switch self {
                    case .some(let e):
                        Text(e.title)
                    case .none:
                        Text("None")
                            .italic()
                            .foregroundColor(.accentColor)
                }
            }
            .tag(self)
        }
    }
    

    然后使用非常紧张:

    Picker(selection: $task.job, label: Text("Job")) {
        ForEach(Model.shared.jobs.optionalPickable) { p in
            p.itemView
        }
    }
    

    【讨论】:

      【解决方案3】:

      我在这里使用 Senseful 的解决方案进行了公开回购: https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection

      编辑:感谢 cmets 发布链接。这是回答问题的代码。复制/粘贴就可以了,或者从链接中克隆 repo。

      import SwiftUI
      
      struct ContentView: View {
          @State private var selectionOne: String? = nil
          @State private var selectionTwo: String? = nil
          
          let items = ["Item A", "Item B", "Item C"]
          
          var body: some View {
              NavigationView {
                  Form {
                      // MARK: - Option 1: NIL by SELECTION
                      Picker(selection: $selectionOne, label: Text("Picker with option to select nil item [none]")) {
                          Text("[none]").tag(nil as String?)
                              .foregroundColor(.red)
      
                          ForEach(items, id: \.self) { item in
                              Text(item).tag(item as String?)
                              // Tags must be cast to same type as Picker selection
                          }
                      }
                      
                      // MARK: - Option 2: NIL by BUTTON ACTION
                      Picker(selection: $selectionTwo, label: Text("Picker with Button that removes selection")) {
                          ForEach(items, id: \.self) { item in
                              Text(item).tag(item as String?)
                              // Tags must be cast to same type as Picker selection
                          }
                      }
                      
                      if selectionTwo != nil { // "Remove item" button only appears if selection is not nil
                          Button("Remove item") {
                              self.selectionTwo = nil
                          }
                      }
                  }
              }
          }
      }
      
      struct ContentView_Previews: PreviewProvider {
          static var previews: some View {
              ContentView()
          }
      }
      

      【讨论】:

      • 请不要只发布答案的链接。贴出相关代码和链接。这样你的答案就不会消失,它本可以提供的价值也不会丢失。
      • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
      • 我很感激,先生们(@Andrew,@kenny_k)。已编辑!
      【解决方案4】:

      为什么不用默认值来扩展枚举?如果这不是您想要实现的目标,也许您还可以提供一些信息,为什么要拥有它optional

      enum Fruit: String, CaseIterable, Hashable {
          case apple = "apple"
          case orange = "orange"
          case banana = "banana"
          case noValue = ""
      }
      
      struct ContentView: View {
      
          @State private var fruit = Fruit.noValue
      
          var body: some View {
              VStack{
                  Picker(selection: $fruit, label: Text("Fruit")) {
                      ForEach(Fruit.allCases, id:\.self) { fruit in
                          Text(fruit.rawValue)
                      }
                  }
                  Text("Selected Fruit: \(fruit.rawValue)")
              }
          }
      }
      

      【讨论】:

      • 您的评论对于enum 来说已经足够公平了,但是如果它是从数组中选择的或者在“以上都不是”可能是有效选择的集合中呢? @Senseful 的回答非常有用!
      【解决方案5】:

      标签必须与绑定包装的确切数据类型匹配。在这种情况下,提供给tag 的数据类型是Fruit,但$fruit.wrappedValue 的数据类型是Fruit?。您可以通过在 tag 方法中转换数据类型来解决此问题:

      struct FruitView: View {
      
          @State private var fruit: Fruit?
      
          var body: some View {
              Picker(selection: $fruit, label: Text("Fruit")) {
                  ForEach(Fruit.allCases) { fruit in
                      Text(fruit.rawValue).tag(fruit as Fruit?)
                  }
              }
          }
      }
      

      奖励:如果您想要为nil 自定义文本(而不仅仅是空白),并希望允许用户选择nil(注意:这里要么全有要么全无) ,您可以为nil 添加一个项目:

      struct FruitView: View {
      
          @State private var fruit: Fruit?
      
          var body: some View {
              Picker(selection: $fruit, label: Text("Fruit")) {
                  Text("No fruit").tag(nil as Fruit?)
                  ForEach(Fruit.allCases) { fruit in
                      Text(fruit.rawValue).tag(fruit as Fruit?)
                  }
              }
          }
      }
      

      不要忘记同时转换 nil 值。

      【讨论】:

      • 此语法的几个变体:.tag(nil as Fruit?).tag(Fruit?.none).tag(Fruit?(nil)).tag(Optional&lt;Fruit&gt;.none)。 ?
      • 这是一个主要的警告(编译器警告在这里会有所帮助)。我花了至少几天的时间来追踪这个。 ??‍♂️
      猜你喜欢
      • 2021-12-23
      • 2019-11-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-30
      • 1970-01-01
      • 2021-02-07
      • 1970-01-01
      相关资源
      最近更新 更多