SwiftUI中的多组件拾取器(UIPickerView)

zvms9eto  于 2023-05-21  发布在  Swift
关注(0)|答案(8)|浏览(99)

我试图将一个三组件的Picker(UIPickerView)添加到SwiftUI应用程序中(在传统的UIKit应用程序中,数据源将从numberOfComponents方法返回3),但我在任何地方都找不到这样的例子。
我尝试过添加一个包含三个单组件Picker的HStack,但是如果它们都是单个Picker的一部分,那么这个视角就不一样了。

ltqd579y

ltqd579y1#

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

Demo:

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

}

结果:

piwo6bdm

piwo6bdm2#

下面是使用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)
        }
        context.coordinator.parent = self // fix
    }
    
    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()
    }
}
rta7y2nd

rta7y2nd3#

最简单的方法是使用UIDatePicker创建一个 Package 的UI视图,并将datePickerMode设置为.countDownTimer
将下面的代码粘贴到名为“TimeDurationPicker”的新SwiftUI视图文件中。选择器使用DatePicker中的countDownDuration的值更新duration
您可以在画布上预览选取器。

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

thigvfpy4#

即使使用.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())
2exbekwf

2exbekwf5#

就像这个论坛中的其他几个开发者一样,我发现纯SwiftUI解决方案在iOS 15或更高版本中并不好用。顺便说一下,带有UIRepresentableView的UIPickerView解决方案也不太好用。高度与需要用户输入的其他视图重叠。在苹果开发者论坛上,TommyL提出了一个非常优雅和简单的解决方案。基本上,您必须使用以下代码扩展UIPickerView:

extension UIPickerView {
    open override var intrinsicContentSize: CGSize {
        return CGSize(width: UIView.noIntrinsicMetric, height: 60)
    }
}

您应该填写高度值,以防止Picker在垂直方向上延伸太多。这将限制拾取器的视图和触摸区域。它适用于Xcode 13和14以及iOS 15及以上版本。

4dbbbstv

4dbbbstv6#

我很喜欢woko的回答,但最终的结果在视觉上还有点不尽如人意。元素感觉有点间隔开了,所以我把几何形状。大小。宽度乘数从2改为5,并在拾取器的两侧添加了间隔物。(我还包括woko的答案中缺少的hoursIndex和mintuesIndex变量。
以下是在iOS 14上使用Xcode 12在iPhone 12 Pro Max模拟器上进行的测试。

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

hfwmuf9z7#

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

5f0d552i

5f0d552i8#

这不是很优雅,但它不涉及任何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中

相关问题