WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Custom Multicomponent Picker - Pure SwiftUI

Forums > SwiftUI

So this isn't a question. This is information I am passing on that may help others now or in the future. Some of the information is basic stuff but I wanted to share a couple of things I have learnt and discovered in creating multi component Pickers in Swift UI. So lets get started.

My examples are basic and I will just use a picker and pickers that select a number. So to create a basic picker with one component in SwiftUI we do the following -

struct BasicPicker: View {
    @State private var selection = 0
    let numbers = [Int](0...10)

    var body: some View {
        VStack {
            Picker("Number", selection: $selection) {
                ForEach(0..<numbers.count) { index in
                    Text("\(self.numbers[index])")
                }
            }
            .labelsHidden()

            Text("\(selection)")
                .foregroundColor(.blue)
        }
    }
}

So here we have a basic picker that will let us select a number. The Text view below will display the number that is picked. Simple, i know right. I have hidden the label so it doesn't get in the way when we create 2 more pickers to go with this one in a HStack. Before we add another 2 pickers we have to change the size of the current one. Now by simply applying a frame modifier and changing the width will not actually achieve what we want. Try it out. Add the following frame modifer directly below the .labelIsHidden() modifier -

.frame(width: 50)

You will see in the preview that the frame for the content of the picker changes but the actual interactive area of the picker does not change, it is still at its original width. To change this as well we have to add the following modifier directly below our new frame modifier -

.clipped()

Now the interactive area of the picker is now clipped to the frame modifier we applied. We now have a bit more room to add 2 more pickers next to this picker in a HStack. So what we will do here is just wrap the entire VStack into a HStack and copy the first picker and past 2 more pickers under this one like the following -

struct BasicPicker: View {
    @State private var selection = 0
    @State private var selection2 = 0
    @State private var selection3 = 0
    let numbers = [Int](0...10)

    var body: some View {
        HStack {
            VStack {
                Picker("Number", selection: $selection) {
                    ForEach(0..<numbers.count) { index in
                        Text("\(self.numbers[index])")
                    }
                }
                .labelsHidden()
                .frame(width: 50)
                .clipped()

                Text("\(selection)")
                    .foregroundColor(.blue)
            }

            VStack {
                Picker("Number", selection: $selection2) {
                    ForEach(0..<numbers.count) { index in
                        Text("\(self.numbers[index])")
                    }
                }
                .labelsHidden()
                .frame(width: 50)
                .clipped()

                Text("\(selection2)")
                    .foregroundColor(.blue)
            }

            VStack {
                Picker("Number", selection: $selection3) {
                    ForEach(0..<numbers.count) { index in
                        Text("\(self.numbers[index])")
                    }
                }
                .labelsHidden()
                .frame(width: 50)
                .clipped()

                Text("\(selection3)")
                    .foregroundColor(.blue)
            }
        }
    }
}

Be sure to add 2 more @State properties so we have a seperate selection for each picker and then also change the Text view to match its corresponding selection value. If you run this we now have 3 pickers side by side each selecting a different value.

Now lets provide a bit of design to what we have to make it look better. First lets add a frame modifier to the outside HStack and change its background color. Add these 2 modifiers to the HStack -

.frame(width: 300, height: 300)
.background(Color.yellow)

Now I am no design guru so bear with me. Run that and check that our pickers still work as expected. Now add the following modifier directly below the background modifier for the HStack -

.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))

Gives us nice rounded corners. Run that and try moving the pickers. You will find that if you try move the one on the left, the picker on the right moves. End result - the pickers now dont do what we expect. Just so you know if you use the modifier .cornerRadius, it has the same effect so that wont solve our problem.

Now how do we solve this. I had a look around, in particular the Apple docs for this modifier which states the following -

'By applying a clipping shape to a view, you preserve the parts of the view covered by the shape, while eliminating other parts of the view. The clipping shape itself isn’t visible.'

Not sure if that has anything to do with it but i am pretty sure there is some underlying code within this view modifier and pickers that create this problem we are encountering. So I thought i would see what other modifiers we had to work with and i found the .mask modifier. Apple docs state the following -

'Masks this view using the alpha channel of the given view.'

This was pretty much all the info on this modifier. But give this a try. Delete the clipShap modifier and add the following modifier in its place -

.mask(RoundedRectangle(cornerRadius: 20, style: .continuous))

Try running that now and there you go, out pickers now are back to normal and we are getting what we need. So now we can create a multi component picker in swiftUI with a bit of customisation.

I know some of this is fairly simple stuff and some of you already know how to do this but i just wanted to share what i encountered just in case there was anyone out there that had run into the same issue or might run into the same issue in the future. So to recap -

  • If you want to change the frame of a picker you have to use the frame modifier in conjunction with the clipped modifier
  • To customise your view your picker (multiple pickers) is contained in (or even the picker itself) you have to use the .mask modifier, not clipShape or cornerRadius

Feel free to add comments or discuss and offer any other insight in relation to this.

Dave

6      

This is a great post. Thank you for this - I experienced very much the same issues you adressed - and I am having another one: I described it at https://www.hackingwithswift.com/forums/swiftui/problems-with-multicomponentpicker/2696. So maybe you get a chance to look at it and give me your opinion...

1      

Thanks for this post! Did change ForEach(0..<numbers.count){...} to just ForEach(numbers){...}. Display to Text wont work unless the options have tags. App crashes if you do something like this though

        VStack {
                    Picker("Number", selection: $selection2) {
                        ForEach(numbers) { index in
                            Text("\(index)").tag(index)
                        }
                    }

hard coding tags works fine however.

1      

@wiidz  

it really really really helpes me !!!!!!!! thx aaaaaaaaaaaaaaaaaaaaaaaaaaaa lot!!!!

1      

This doesn't work on Xcode 13.3, ios simulator iPhone8/ios15.4 Adding .compositingGroup() didn' help either.

   

Put this in your code, just before the "struct ContentView: View { ..."

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

2      

Thank you very much. This fixes the control surface being shifted to the left.
Adding .compositingGroup() ; .clipped(); or .mask(RoundedRectangle(cornerRadius: 20, style: .continuous)) didn't correct this issue. However .clip() or .mask() confine the displayed numbers to the .frame(width: 300, height: 300). They both appear to work equally well for that.

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.