SwiftUI – How to create a multi-component picker

Share on facebook
Share on twitter
Share on pinterest

Updated for iOS 15

In many SwiftUI apps there are cases where we want to use multi-component pickers using. For example, they are suitable when we want to know the user’s birthday. However, SwiftUI does not provide us with native multi-component pickers. However, this is not a big problem as we can easily build one ourselves in a few steps!

Let’s get started!

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.

struct ContentView: View {
    
    @State var daySelection = 0
    
    var days = [Int](0..<30)
    
    var body: some View {
        
    }
}

Now we can create the Picker view. To make it appear as a wheel, we use the appropriate .pickerStyle.

struct ContentView: View {
    
    @State var daySelection = 0
    
    var days = [Int](0..<30)
    
    var body: some View {
        Picker(selection: self.$daySelection, label: Text("")) {
            ForEach(0 ..< self.days.count) { index in
                Text("\(self.days[index]) d").tag(index)
            }
        }
            .pickerStyle(.wheel)
    }
}

Step 2: Now we can embed the first Picker view in an HStack with zero spacing and repeat the steps from Step 1 for each additional component. The resulting code looks something like this:

struct ContentView: View {
    
    @State var daySelection = 0
    @State var hourSelection = 0
    @State var minuteSelection = 0
    
    var days = [Int](0..<30)
    var hours = [Int](0..<24)
    var minutes = [Int](0..<60)
    
    var body: some View {
        HStack(spacing: 0) {
            Picker(selection: self.$daySelection, label: Text("")) {
                ForEach(0 ..< self.days.count) { index in
                    Text("\(self.days[index]) d").tag(index)
                }
            }
                .pickerStyle(.wheel)
            Picker(selection: self.$hourSelection, label: Text("")) {
                ForEach(0 ..< self.hours.count) { index in
                    Text("\(self.hours[index]) h").tag(index)
                }
            }
                .pickerStyle(.wheel)
            Picker(selection: self.$minuteSelection, label: Text("")) {
                ForEach(0 ..< self.minutes.count) { index in
                    Text("\(self.minutes[index]) m").tag(index)
                }
            }
                .pickerStyle(.wheel)
        }
    }
}


Step 3: We need to know the superview’s (here: the ContentView) width since we want each component of the Picker to be equally wide. Thus, let’s create a Geometry Reader for getting the ContentView‘s size.

var body: some View {
    GeometryReader { geometry in
        HStack(spacing: 0) {
            //...
        }
    }
}

We can now use the geometry property from the GeometryReader closure to divide each Picker view into thirds. 

HStack(spacing: 0) {
    Picker(selection: self.$daySelection, label: Text("")) {
        ForEach(0 ..< self.days.count) { index in
            Text("\(self.days[index]) d").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
    Picker(selection: self.$hourSelection, label: Text("")) {
        ForEach(0 ..< self.hours.count) { index in
            Text("\(self.hours[index]) h").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
    Picker(selection: self.$minuteSelection, label: Text("")) {
        ForEach(0 ..< self.minutes.count) { index in
            Text("\(self.minutes[index]) m").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
}

This already looks good. However, when we start a Live preview, we notice that the individual Picker views overlap.


Step 4: To prevent overlapping we apply the .compositionGroup and the .clipped modifier to each Picker view.

HStack(spacing: 0) {
    Picker(selection: self.$daySelection, label: Text("")) {
        ForEach(0 ..< self.days.count) { index in
            Text("\(self.days[index]) d").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
    Picker(selection: self.$hourSelection, label: Text("")) {
        ForEach(0 ..< self.hours.count) { index in
            Text("\(self.hours[index]) h").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
    Picker(selection: self.$minuteSelection, label: Text("")) {
        ForEach(0 ..< self.minutes.count) { index in
            Text("\(self.minutes[index]) m").tag(index)
        }
    }
        .pickerStyle(.wheel)
        .frame(width: geometry.size.width/3, height: geometry.size.height, alignment: .center)
        .compositingGroup()
        .clipped()
}


Let’s run a Live preview again to see if this works:


That’s all! You can now use the State properties for subscripting the selected values out of your data sets. For example, like this:

var body: some View {
    GeometryReader { geometry in
        VStack {
            Spacer()
            HStack(spacing: 0) {
                //...
            }
                //You can also limit the width and height of your multi component picker
                .frame(width: 350, height: 150)
            Text("Remind me in \(daySelection) day(s), \(hourSelection) hour(s) and \(minuteSelection) minute(s)")
                .padding(.top, 40)
            Spacer()
        }
    }
}

And that is how you can create multi component Pickers in SwiftUI!

If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics Book and our other tutorials! Also make sure you follow us on Instagram to not miss any updates, tutorials and tips about SwiftUI and more!

3 replies on “SwiftUI – How to create a multi-component picker”

Due to the overlapping of pickers you can’t scroll well in picker . So if you use .clipped( ) after .frame( ), you can cut the overlapping lines. Also add spacing: 0 to HStack after clipped to merge them.

I’ve used something similar on iOS 13/14 but it seems broken on iOS 15. For version 15.1 it works in the simulator but not on device. With iOS 15.2 beta (and Xcode 13.2 beta 2), it is also broken in the simulator. When you try to move the first column, it moves the second column, and the same issue when you try to move second column, it moves third column. I’m guessing this is Apple’s bug but I haven’t been able to figure out a workaround. If you have one, please let us know. Thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount