Categories
iOS Swift SwiftUI UIKit

Using a multi component picker in a SwiftUI form

SwiftUI has a Picker view available with multiple different styles. One example of when it falls short is when we want to use a multi component picker with wheel style. One way how to try to achieve this is using a HStack with two Picker views, but it does not work very well, especially when trying to show it inside a Form view. So what else we can do? If something can’t be done in SwiftUI then we can use UIKit instead.

In my case, I wanted to create a picker which allows picking a date duration. It would have one wheel for selecting a number and the other wheel for selecting either days, weeks or months.

Screenshot of a SwiftUI form with a two component wheel picker where left wheel selects a number and right wheel selects days, weeks or months.

Firstly, let’s create a tiny struct which is going to hold the state of this picker. It needs to store a numeric value and the unit: days, weeks, months. Let’s name it as DateDuration. Since we want to iterate over the DateDuration.Unit, we’ll conform it to CaseIterable protocol.

struct DateDuration {
let value: Int
let unit: Unit
enum Unit: String, CaseIterable {
case days, weeks, months
}
}

UIPickerView in UIKit can do everything we want, therefore we’ll need to wrap it into a SwiftUI view. This can be done by creating a new type which conforms to UIViewRepresentable protocol. Also, we need a binding which holds the value of the current selection: when the user changes it, the binding communicates the changes back and vice-versa. Additionally, we’ll add properties for configuring values and units. UIPickerView us created and configured in the makeUIView(context:) function. UIPickerView is driven by a data source and a delegate, which means we require a coordinator object as well. Coordinator is part of the UIViewRepresentable protocol.

struct DateDurationPicker: UIViewRepresentable {
let selection: Binding<DateDuration>
let values: [Int]
let units: [DateDuration.Unit]
func makeUIView(context: Context) -> UIPickerView {
let pickerView = UIPickerView(frame: .zero)
pickerView.translatesAutoresizingMaskIntoConstraints = false
pickerView.delegate = context.coordinator
pickerView.dataSource = context.coordinator
return pickerView
}
// …
}

Coordinator is created in the makeCoordinator() function. It is going to do most of the work by providing data to the UIPickerView and handling the current selection. Therefore, we’ll store the selection binding, values, and units in the Coordinator class as well.

struct DateDurationPicker: UIViewRepresentable {
// …
func makeCoordinator() -> Coordinator {
return Coordinator(selection: selection, values: values, units: units)
}
final class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
let selection: Binding<DateDuration>
let values: [Int]
let units: [DateDuration.Unit]
init(selection: Binding<DateDuration>, values: [Int], units: [DateDuration.Unit]) {
self.selection = selection
self.values = values
self.units = units
}
// …
}
}

The last missing piece is implementing UIPickerViewDataSource and UIPickerViewDelegate methods in the Coordinator class. This is pretty straight-forward to do. We’ll need to display two components where the first component is the list of values and the second component is the unit: days, weeks, months. When the user selects a new value, we’ll change the DateDuration value of the binding.

func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return component == 0 ? values.count : units.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return "\(values[row])"
}
else {
return units[row].rawValue
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let valueIndex = pickerView.selectedRow(inComponent: 0)
let unitIndex = pickerView.selectedRow(inComponent: 1)
selection.wrappedValue = DateDuration(value: values[valueIndex], unit: units[unitIndex])
}

Finally, let’s hook it up in an example view.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
Form {
Section {
// …
DateDurationPicker(
selection: $viewModel.selection,
values: Array(1..<100),
units: DateDuration.Unit.allCases
)
// …
}
}
.navigationTitle("Reminders")
}
}
}
extension ContentView {
final class ViewModel: ObservableObject {
@Published var selection = DateDuration(value: 1, unit: .days)
// …
}
}

Example Project

SwiftUIDateDurationPicker (GitHub, Xcode 13.2.1)

If this was helpful, please let me know on Mastodon@toomasvahter orĀ Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift SwiftUI UIKit

Using SwiftUI previews for UIKit views

SwiftUI provides wrappers for UIViewController and UIView on iOS. Same wrappers are also available for AppKit views on macOS. Let’s see how to use those wrappers for rendering UIKit views in SwiftUI previews and therefore benefiting from seeing changes immediately. Note that even when a project can’t support SwiftUI views because of the minimum deployment target, then this is still something what can be used when compiling the project with debug settings. Preview related code should only be compiled in debug builds and is never meant to be compiled in release builds. Before we jump in, there are two very useful shortcuts for keeping in mind: option+command+return for toggling previews and option+command+p for refreshing previews.

UIViewControllerRepresentable for wrapping UIViewControllers

UIViewControllerRepresentable is a protocol which can be used for wrapping UIViewController and representing it in SwiftUI. We can add a struct which conforms to that protocol and then creating an instance of the view controller in the makeUIViewController method. Second step is to add another struct which implements PreviewProvider protocol and which is used by Xcode for rendering previews. In simple cases we can get away only with such implementation but in more complex view controllers we would need to set up dependencies and generate example data for the preview. If need to do this, then all that code can be added to the makeUIViewController method.

import UIKit
import SwiftUI
final class ContentViewController: UIViewController {
override func loadView() {
self.view = UIView()
self.view.backgroundColor = .systemBackground
}
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 16)
])
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.textColor = .systemRed
label.text = "Red text"
}
}
// MARK: SwiftUI Preview
#if DEBUG
struct ContentViewControllerContainerView: UIViewControllerRepresentable {
typealias UIViewControllerType = ContentViewController
func makeUIViewController(context: Context) -> UIViewControllerType {
return ContentViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentViewController_Previews: PreviewProvider {
static var previews: some View {
ContentViewControllerContainerView().colorScheme(.light) // or .dark
}
}
#endif
Wrapping UIViewController with UIViewControllerRepresentable.
UIViewController shown using SwiftUI

UIViewRepresentable for wrapping UIViews

UIViewRepresentable follows the same flow. In the example below, we use Group for showing two views with fixed size and different appearances at the same time.

import SwiftUI
import UIKit
final class BackgroundView: UIView {
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .systemBackground
layer.cornerRadius = 32
layer.borderColor = UIColor.systemBlue.cgColor
layer.borderWidth = 14
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: SwiftUI Preview
#if DEBUG
struct BackgroundViewContainer: UIViewRepresentable {
typealias UIViewType = BackgroundView
func makeUIView(context: Context) -> UIViewType {
return BackgroundView(frame: .zero)
}
func updateUIView(_ uiView: BackgroundView, context: Context) {}
}
struct BackgroundViewContainer_Previews: PreviewProvider {
static var previews: some View {
Group {
BackgroundViewContainer().colorScheme(.light)
BackgroundViewContainer().colorScheme(.dark)
}.previewLayout(.fixed(width: 200, height: 200))
}
}
#endif
Wrapping UIView subclass with UIViewRepresentable.
Multiple UIViews shown in SwiftUI preview at the same time.

Summary

We looked into how to wrap view controllers and views for SwiftUI previews. Previews only required a little bit of code and therefore it is something what we can use for improving our workflows when working with UIKit views.

If this was helpful, please let me know on Mastodon@toomasvahter orĀ Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

UIKitInSwiftUIPreview (Xcode 11.5)