SwiftUI 中的多组件选择器 (UIPickerView)
Multi-Component Picker (UIPickerView) in SwiftUI
我正在尝试将三组件选择器 (UIPickerView) 添加到 SwiftUI 应用程序(在传统的 UIKit 应用程序中,数据源将 return 3
来自 numberOfComponents
方法),但我在任何地方都找不到这样的例子。
我已经尝试添加一个包含三个单组件选择器的 HStack,但如果它们都是单个选择器的一部分,则透视图会有所不同。
纯 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 { "\([=11=])" }),
("Two", Array(20...40).map { "\([=11=])" }),
("Three", Array(100...200).map { "\([=11=])" })
]
@State var selection: [String] = [0, 20, 100].map { "\([=11=])" }
var body: some View {
VStack(alignment: .center) {
Text(verbatim: "Selection: \(selection)")
MultiPicker(data: data, selection: $selection).frame(height: 300)
}
}
}
结果:
这不是很优雅,但它不涉及移植任何 UIKit 内容。我知道您在回答中提到透视不正确,但也许这里的几何形状可以解决这个问题
GeometryReader { geometry in
HStack
{
Picker(selection: self.$selection, label: Text(""))
{
ForEach(0 ..< self.data1.count)
{
Text(self.data1[[=10=]])
.color(Color.white)
.tag([=10=])
}
}
.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[[=10=]])
.color(Color.white)
.tag([=10=])
}
}
.pickerStyle(.wheel)
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: geometry.size.height, alignment: .center)
}
}
像这样使用几何形状并固定大小显示两个选择器在每一半中整齐地占据了屏幕宽度的一半。现在你只需要处理来自两个不同状态变量的选择,而不是一个,但我更喜欢这种方式,因为它将所有内容都保存在 swift UI
下面是对上述解决方案的改编,使用了 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 { "\([=10=])" },
Array(20...40).map { "\([=10=])" },
Array(100...200).map { "\([=10=])" }
]
@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()
}
}
即使使用 .clipped()
,底层拾取器也不会收缩并且倾向于与其他拾取器重叠。我设法剪辑甚至底层选择器视图的唯一方法是将 .mask(Rectangle())
添加到父容器。别问为什么,我也不知道。
一个有 2 个选择器的工作示例(小时和分钟):
GeometryReader { geometry in
HStack(spacing: 0) {
Picker("", selection: self.$hoursIndex) {
ForEach(0..<13) {
Text(String([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: 160)
.clipped()
Picker("", selection: self.$minutesIndex) {
ForEach(0..<12) {
Text(String([=10=]*5)).tag([=10=]*5)
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: 160)
.clipped()
}
}
.frame(height: 160)
.mask(Rectangle())
我很喜欢woko的回答,但最终的结果在视觉上还有一点不尽如人意。这些元素感觉有点间隔,所以我将 geometry.size.width 乘数从 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([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 5, height: 160)
.clipped()
Picker("", selection: self.$selectedMinute) {
ForEach(0..<60) {
Text(String([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 5, height: 160)
.clipped()
Spacer()
}
}
.frame(height: 160)
.mask(Rectangle())
}
}
zto 执行此操作的最简单方法是使用 UIDatePicker
将 datePickerMode
设置为 .countDownTimer
。
创建一个包装的 UI 视图
将下面的代码粘贴到名为“TimeDurationPicker”的新 SwiftUI 视图文件中。选择器使用 DatePicker
中 countDownDuration
的值更新 duration
。
您可以在 Canvas 上预览选择器。
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))
}
}
在iOS15中,这个解决方案:很好,但它需要在“.clipped()”修饰符之前使用修饰符“.compositingGroup()”。
我正在尝试将三组件选择器 (UIPickerView) 添加到 SwiftUI 应用程序(在传统的 UIKit 应用程序中,数据源将 return 3
来自 numberOfComponents
方法),但我在任何地方都找不到这样的例子。
我已经尝试添加一个包含三个单组件选择器的 HStack,但如果它们都是单个选择器的一部分,则透视图会有所不同。
纯 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 { "\([=11=])" }),
("Two", Array(20...40).map { "\([=11=])" }),
("Three", Array(100...200).map { "\([=11=])" })
]
@State var selection: [String] = [0, 20, 100].map { "\([=11=])" }
var body: some View {
VStack(alignment: .center) {
Text(verbatim: "Selection: \(selection)")
MultiPicker(data: data, selection: $selection).frame(height: 300)
}
}
}
结果:
这不是很优雅,但它不涉及移植任何 UIKit 内容。我知道您在回答中提到透视不正确,但也许这里的几何形状可以解决这个问题
GeometryReader { geometry in
HStack
{
Picker(selection: self.$selection, label: Text(""))
{
ForEach(0 ..< self.data1.count)
{
Text(self.data1[[=10=]])
.color(Color.white)
.tag([=10=])
}
}
.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[[=10=]])
.color(Color.white)
.tag([=10=])
}
}
.pickerStyle(.wheel)
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: geometry.size.height, alignment: .center)
}
}
像这样使用几何形状并固定大小显示两个选择器在每一半中整齐地占据了屏幕宽度的一半。现在你只需要处理来自两个不同状态变量的选择,而不是一个,但我更喜欢这种方式,因为它将所有内容都保存在 swift UI
下面是对上述解决方案的改编,使用了 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 { "\([=10=])" },
Array(20...40).map { "\([=10=])" },
Array(100...200).map { "\([=10=])" }
]
@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()
}
}
即使使用 .clipped()
,底层拾取器也不会收缩并且倾向于与其他拾取器重叠。我设法剪辑甚至底层选择器视图的唯一方法是将 .mask(Rectangle())
添加到父容器。别问为什么,我也不知道。
一个有 2 个选择器的工作示例(小时和分钟):
GeometryReader { geometry in
HStack(spacing: 0) {
Picker("", selection: self.$hoursIndex) {
ForEach(0..<13) {
Text(String([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: 160)
.clipped()
Picker("", selection: self.$minutesIndex) {
ForEach(0..<12) {
Text(String([=10=]*5)).tag([=10=]*5)
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 2, height: 160)
.clipped()
}
}
.frame(height: 160)
.mask(Rectangle())
我很喜欢woko的回答,但最终的结果在视觉上还有一点不尽如人意。这些元素感觉有点间隔,所以我将 geometry.size.width 乘数从 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([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 5, height: 160)
.clipped()
Picker("", selection: self.$selectedMinute) {
ForEach(0..<60) {
Text(String([=10=])).tag([=10=])
}
}
.labelsHidden()
.fixedSize(horizontal: true, vertical: true)
.frame(width: geometry.size.width / 5, height: 160)
.clipped()
Spacer()
}
}
.frame(height: 160)
.mask(Rectangle())
}
}
zto 执行此操作的最简单方法是使用 UIDatePicker
将 datePickerMode
设置为 .countDownTimer
。
将下面的代码粘贴到名为“TimeDurationPicker”的新 SwiftUI 视图文件中。选择器使用 DatePicker
中 countDownDuration
的值更新 duration
。
您可以在 Canvas 上预览选择器。
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))
}
}
在iOS15中,这个解决方案: