Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi-Component Picker (UIPickerView) in SwiftUI

I'm trying to add a three-component Picker (UIPickerView) to a SwiftUI app (in a traditional UIKit app, the data source would return 3 from the numberOfComponents method), but I can't find an example of this anywhere.

I've tried adding an HStack of three single-component Pickers, but the perspective is off from what it would be if they were all part of a single Picker.

like image 373
Frank Schmitt Avatar asked Jun 12 '19 17:06

Frank Schmitt


People also ask

How do I create a multi-component picker in SwiftUI?

Step 1: Our SwiftUI multi-component Picker basically consists of several individual Picker views arranged horizontally. Therefore, we start by creating an ordinary Picker view for our first component. To do this, we declare an appropriate State property and initialize an array that contains the set for the Picker view.

What is picker in SwiftUI?

Figure 1. SwiftUI’s Picker is the standard way of working with options. It allows developers to have the capability of UIKit’s UIPickerView and UISegmentedControl without the working knowledge of how those view works. This article is part of my SwiftUI Tutorial series. In this tutorial, we’re going to tackle different ways of working with Picker.

How to store options in Swift picker?

Another way of storing options in Picker is by customizing the data types using enum. Although its complicated to understand for novice coders, it’s the best practice for sophisticated programs. Note: Swift is a type-safe programming language. This means that it encourage clarity when declaring types of values.

What are the different types of Picker styles in swifui?

The options are: . inline, . menu, . segmented, and . wheel. By default, it is set to . automatic where SwifUI’s Picker automatically adapts based on the environment of Views. In our above example, it used .menu style to present the choices.


3 Answers

Here's an adaptation of the solutions above, using the UIKit picker:

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()
    }
}
like image 73
protasm Avatar answered Oct 21 '22 22:10

protasm


Updated answer in pure SwiftUI- in this example the data is of type String.

Tested on Xcode 11.1 - may not work on previous versions.

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

}

Result:

enter image description here

like image 25
Matteo Pacini Avatar answered Oct 21 '22 21:10

Matteo Pacini


The easiest way zto do this is creating a wrapped UI View using a UIDatePicker with datePickerMode set to .countDownTimer.

Paste the code below into a new SwiftUI view file called "TimeDurationPicker". The picker updates duration with the value of countDownDuration in DatePicker.

You can preview the picker on the 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))
    }
}
like image 38
Maurice Avatar answered Oct 21 '22 21:10

Maurice