UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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

8      

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...

3      

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.

3      

@wiidz  

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

3      

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

2      

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

4      

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.

2      

This only limits the views, but not the touching areas. .mask and .compositingGroup doesn't help. At least not for iOS 15 and iOS 16.

2      

I found a new solution to the problem on the Apple Developer Forum. This was posted by TommyL and it works fine for me with Xcode 13 and 14 iOS 15 and above.

extension UIPickerView {

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

Fill in a number if you also want to limit the height of the touch area. You can then skip the entire clipping, compositingGrouping, etc.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.