【问题标题】:Multi-Component Picker (UIPickerView) in SwiftUISwiftUI 中的多组件选择器(UIPickerView)
【发布时间】:2019-10-27 06:47:37
【问题描述】:

我正在尝试将三组件 Picker (UIPickerView) 添加到 SwiftUI 应用程序(在传统的 UIKit 应用程序中,数据源将从 numberOfComponents 方法返回 3),但我找不到任何地方的一个例子。

我尝试添加一个由三个单组件 Picker 组成的 HStack,但如果它们都是单个 Picker 的一部分,那么视角会有所不同。

【问题讨论】:

    标签: swift uipickerview picker swiftui


    【解决方案1】:

    在iOS15中,这个解决方案:https://stackoverflow.com/a/56568715/12847995很好,但它需要在“.clipped()”修饰符之前使用修饰符“.compositingGroup()”。

    【讨论】:

      【解决方案2】:

      执行此操作的最简单方法是使用UIDatePickerdatePickerMode 设置为.countDownTimer 创建一个包装好的UI 视图。

      将以下代码粘贴到名为“TimeDurationPicker”的新 SwiftUI 视图文件中。选择器将 duration 更新为 DatePicker 中的 countDownDuration 值。

      您可以在画布上预览选择器。

      struct TimeDurationPicker: UIViewRepresentable {
          typealias UIViewType = UIDatePicker
          
          @Binding var duration: TimeInterval
         
          func makeUIView(context: Context) -> UIDatePicker {
              let timeDurationPicker = UIDatePicker()
              timeDurationPicker.datePickerMode = .countDownTimer
              timeDurationPicker.addTarget(context.coordinator, action: #selector(Coordinator.changed(_:)), for: .valueChanged)
              return timeDurationPicker
          }
      
          func updateUIView(_ uiView: UIDatePicker, context: Context) {
              uiView.countDownDuration = duration
          }
      
          func makeCoordinator() -> TimeDurationPicker.Coordinator {
              Coordinator(duration: $duration)
          }
      
          class Coordinator: NSObject {
              private var duration: Binding<TimeInterval>
      
              init(duration: Binding<TimeInterval>) {
                  self.duration = duration
              }
      
              @objc func changed(_ sender: UIDatePicker) {
                  self.duration.wrappedValue = sender.countDownDuration
              }
          }
      }
      
      struct TimeDurationPicker_Previews: PreviewProvider {
          static var previews: some View {
              TimeDurationPicker(duration: .constant(60.0 * 30.0))
          }
      }
      

      【讨论】:

        【解决方案3】:

        我非常喜欢 woko 的回答,但最终的结果在视觉上有些不尽人意。元素感觉有点间隔,所以我将 geometry.size.width 乘数从 2 更改为 5,并在选择器的两侧添加了垫片。 (我还包括了 woko 的答案中缺少的 hoursIndex 和 mintuesIndex 变量。)

        以下是在 iPhone 12 Pro Max 模拟器上使用 Xcode 12 在 iOS 14 上进行的测试。

        struct TimerView: View {
            @State private var hours = Calendar.current.component(.hour, from: Date())
            @State private var minutes = Calendar.current.component(.minute, from: Date())
        
            var body: some View {
                TimeEditPicker(selectedHour: $hours, selectedMinute: $minutes)
            }
        }
        
        struct TimeEditPicker: View {
            @Binding var selectedHour: Int
            @Binding var selectedMinute: Int
        
            var body: some View {
                GeometryReader { geometry in
                    HStack(spacing: 0) {
                        Spacer()
                        Picker("", selection: self.$selectedHour) {
                            ForEach(0..<24) {
                                Text(String($0)).tag($0)
                            }
                        }
                        .labelsHidden()
                        .fixedSize(horizontal: true, vertical: true)
                        .frame(width: geometry.size.width / 5, height: 160)
                        .clipped()
        
                        Picker("", selection: self.$selectedMinute) {
                            ForEach(0..<60) {
                                Text(String($0)).tag($0)
                            }
                        }
                        .labelsHidden()
                        .fixedSize(horizontal: true, vertical: true)
                        .frame(width: geometry.size.width / 5, height: 160)
                        .clipped()
        
                        Spacer()
                    }
                }
                .frame(height: 160)
                .mask(Rectangle())
            }
        }
        

        【讨论】:

          【解决方案4】:

          即使使用.clipped(),底层选择器也不会缩小,并且往往会与其他选择器重叠。我设法剪辑甚至底层选择器视图的唯一方法是将.mask(Rectangle()) 添加到父容器中。别问为什么,我不知道。

          一个有 2 个选择器(小时和分钟)的工作示例:

          GeometryReader { geometry in
              HStack(spacing: 0) {
                  Picker("", selection: self.$hoursIndex) {
                      ForEach(0..<13) {
                          Text(String($0)).tag($0)
                      }
                  }
                  .labelsHidden()
                  .fixedSize(horizontal: true, vertical: true)
                  .frame(width: geometry.size.width / 2, height: 160)
                  .clipped()
                  
                  Picker("", selection: self.$minutesIndex) {
                      ForEach(0..<12) {
                          Text(String($0*5)).tag($0*5)
                      }
                  }
                  .labelsHidden()
                  .fixedSize(horizontal: true, vertical: true)
                  .frame(width: geometry.size.width / 2, height: 160)
                  .clipped()
              }
          }
          .frame(height: 160)
          .mask(Rectangle())
          

          【讨论】:

            【解决方案5】:

            这是对上述解决方案的改编,使用 UIKit 选择器:

            import SwiftUI
            
            struct PickerView: UIViewRepresentable {
                var data: [[String]]
                @Binding var selections: [Int]
            
                //makeCoordinator()
                func makeCoordinator() -> PickerView.Coordinator {
                    Coordinator(self)
                }
            
                //makeUIView(context:)
                func makeUIView(context: UIViewRepresentableContext<PickerView>) -> UIPickerView {
                    let picker = UIPickerView(frame: .zero)
            
                    picker.dataSource = context.coordinator
                    picker.delegate = context.coordinator
            
                    return picker
                }
            
                //updateUIView(_:context:)
                func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<PickerView>) {
                    for i in 0...(self.selections.count - 1) {
                        view.selectRow(self.selections[i], inComponent: i, animated: false)
                    }
                }
            
                class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
                    var parent: PickerView
            
                    //init(_:)
                    init(_ pickerView: PickerView) {
                        self.parent = pickerView
                    }
            
                    //numberOfComponents(in:)
                    func numberOfComponents(in pickerView: UIPickerView) -> Int {
                        return self.parent.data.count
                    }
            
                    //pickerView(_:numberOfRowsInComponent:)
                    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
                        return self.parent.data[component].count
                    }
            
                    //pickerView(_:titleForRow:forComponent:)
                    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
                        return self.parent.data[component][row]
                    }
            
                    //pickerView(_:didSelectRow:inComponent:)
                    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
                        self.parent.selections[component] = row
                    }
                }
            }
            
            import SwiftUI
            
            struct ContentView: View {
                private let data: [[String]] = [
                    Array(0...10).map { "\($0)" },
                    Array(20...40).map { "\($0)" },
                    Array(100...200).map { "\($0)" }
                ]
            
                @State private var selections: [Int] = [5, 10, 50]
            
                var body: some View {
                    VStack {
                        PickerView(data: self.data, selections: self.$selections)
            
                        Text("\(self.data[0][self.selections[0]]) \(self.data[1][self.selections[1]]) \(self.data[2][self.selections[2]])")
                    } //VStack
                }
            }
            
            struct ContentView_Previews: PreviewProvider {
                static var previews: some View {
                    ContentView()
                }
            }
            

            【讨论】:

              【解决方案6】:

              更新了纯SwiftUI的答案-在本例中,数据的类型为String

              Xcode 11.1 上测试 - 可能不适用于以前的版本。

              struct MultiPicker: View  {
              
                  typealias Label = String
                  typealias Entry = String
              
                  let data: [ (Label, [Entry]) ]
                  @Binding var selection: [Entry]
              
                  var body: some View {
                      GeometryReader { geometry in
                          HStack {
                              ForEach(0..<self.data.count) { column in
                                  Picker(self.data[column].0, selection: self.$selection[column]) {
                                      ForEach(0..<self.data[column].1.count) { row in
                                          Text(verbatim: self.data[column].1[row])
                                          .tag(self.data[column].1[row])
                                      }
                                  }
                                  .pickerStyle(WheelPickerStyle())
                                  .frame(width: geometry.size.width / CGFloat(self.data.count), height: geometry.size.height)
                                  .clipped()
                              }
                          }
                      }
                  }
              }
              

              演示:

              struct ContentView: View {
              
                  @State var data: [(String, [String])] = [
                      ("One", Array(0...10).map { "\($0)" }),
                      ("Two", Array(20...40).map { "\($0)" }),
                      ("Three", Array(100...200).map { "\($0)" })
                  ]
                  @State var selection: [String] = [0, 20, 100].map { "\($0)" }
              
                  var body: some View {
                      VStack(alignment: .center) {
                          Text(verbatim: "Selection: \(selection)")
                          MultiPicker(data: data, selection: $selection).frame(height: 300)
                      }
                  }
              
              }
              

              结果:

              【讨论】:

              • 不知道你能不能告诉我 makeCoordinator 在哪里被调用?
              • 我相信它是由 SwiftUI 框架调用的。我不是 100% 确定,但我相信它只被调用一次,然后即使视图被更新也会被缓存。
              • 我在 init() 方法中的 $data 和 $selection 上收到“无法分配给属性:'$data' 是不可变的”错误。
              • 对于那些盲目复制这篇文章的人:在每个选择器上使用 .clipped() 修饰符至关重要 ,因为彼此相邻的多个选取器水平分布重叠的手势空间,导致在 iOS 设备上的点击位置获取错误的坐标。 “.clipped()”将解决这个问题!我花了三天时间才弄清楚这是错误的根源。
              • 这在 iOS 15 中对我来说开始失败。触摸区域似乎超出了视图宽度,因此尝试旋转第一个 Picker 会导致第二个 Picker 移动。 (我的屏幕上只有两个)
              【解决方案7】:

              这不是很优雅,但它不涉及移植任何 UIKit 的东西。我知道您在回答中提到了透视图,但也许这里的几何图形解决了这个问题

              GeometryReader { geometry in
              
                  HStack
                  {
                       Picker(selection: self.$selection, label: Text(""))
                       {
                            ForEach(0 ..< self.data1.count)
                            {
                                Text(self.data1[$0])
                                   .color(Color.white)
                                   .tag($0)
                            }
                        }
                        .pickerStyle(.wheel)
                        .fixedSize(horizontal: true, vertical: true)
                        .frame(width: geometry.size.width / 2, height: geometry.size.height, alignment: .center)
              
              
                        Picker(selection: self.$selection2, label: Text(""))
                        {
                             ForEach(0 ..< self.data2.count)
                             {
                                 Text(self.data2[$0])
                                     .color(Color.white)
                                     .tag($0)
                             }
                        }
                        .pickerStyle(.wheel)
                        .fixedSize(horizontal: true, vertical: true)
                        .frame(width: geometry.size.width / 2, height: geometry.size.height, alignment: .center)
              
                  }
              }
              

              使用几何图形并像这样固定大小显示两个拾取器在每一半中整齐地占据屏幕宽度的一半。现在您只需要处理来自两个不同状态变量而不是一个的选择,但我更喜欢这种方式,因为它将所有内容都保留在 swift UI 中

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2019-11-13
                • 2023-03-11
                • 2017-07-08
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多