17

My goal is to have two pickers placed side-by-side horizontally with each picker taking up half the width of the screen. Imagine a UIPickerView that fits the width of the screen and has two components of equal width - that's what I'm attempting to recreate in SwiftUI.

Since pickers in SwiftUI do not currently allow for multiple components, the obvious alternative to me was just to place two pickers inside an HStack.

Here's some example code from a test project:

struct ContentView: View {
    @State var selection1: Int = 0
    @State var selection2: Int = 0

    @State var integers: [Int] = [0, 1, 2, 3, 4, 5]

    var body: some View {
        HStack {
            Picker(selection: self.$selection1, label: Text("Numbers")) {
                ForEach(self.integers) { integer in
                    Text("\(integer)")
                }
            }
            Picker(selection: self.$selection2, label: Text("Numbers")) {
                ForEach(self.integers) { integer in
                    Text("\(integer)")
                }
            }
        }
    }
}

And here is the canvas:

SwiftUI - Pickers in HStack

The pickers do not resize to be half the width of the screen like I would expect. They retain their size and instead stretch the width of the content view, distorting the widths of other UI elements in the process (as I found out when I tried to do this in my other project).

I know that I can use UIViewRepresentable to get the effect that I want, but SwiftUI would be much easier to use given the complexity of what I'm trying to use this for.

Is it a bug that placing two pickers inside an HStack does not properly resize them, or do pickers in SwiftUI just have a fixed width that cannot be changed?


Update

Using GeometryReader, I've managed to get closer to resizing the pickers how I want, but not all the way.

Side note: you can also achieve this same imperfect result without using GeometryReader by simply setting the frame on each picker to .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).

Here's the example code:

struct ContentView: View {
    @State var selection1: Int = 0
    @State var selection2: Int = 0

    @State var integers: [Int] = [0, 1, 2, 3, 4, 5]

    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                Picker(selection: self.$selection1, label: Text("Numbers")) {
                    ForEach(self.integers) { integer in
                        Text("\(integer)")
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
                Picker(selection: self.$selection2, label: Text("Numbers")) {
                    ForEach(self.integers) { integer in
                        Text("\(integer)")
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
            }
        }
    }
}

And here is the canvas:

Pickers in HStack with GeometryReader

The pickers are now closer to having the appearance that I want, but the sizing is still slightly off, and they're now overlapping each other in the middle.

graycampbell
  • 7,430
  • 3
  • 24
  • 30

2 Answers2

27

The overlapping in the middle you can fix by adding a clipped() modifier. As for the width, I see them both exactly the same:

enter image description here

struct ContentView: View {
    @State var selection1: Int = 0
    @State var selection2: Int = 0

    @State var integers: [Int] = [0, 1, 2, 3, 4, 5]

    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 0) {
                Picker(selection: self.$selection1, label: Text("Numbers")) {
                    ForEach(self.integers) { integer in
                        Text("\(integer)")
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
                .clipped()
                .border(Color.red)

                Picker(selection: self.$selection2, label: Text("Numbers")) {
                    ForEach(self.integers) { integer in
                        Text("\(integer)")
                    }
                }
                .frame(maxWidth: geometry.size.width / 2)
                .clipped()
                .border(Color.blue)
            }
        }
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Thanks so much! I can't believe it was as simple as adding `clipped()`. You're right, the width was the same. I was thrown off by the fact that the picker on the right was overlapping the picker on the left by so much. Your tutorials are great, by the way. I was using them the other day and learned a lot, so thanks for that! – graycampbell Jul 10 '19 at 08:58
  • 18
    the UI is showing correctly but the gesture still overlaping on Xcode13. do you facing with this problem? – eric long Sep 22 '21 at 15:14
  • Any working solutions? –  Mar 10 '22 at 00:50
9

As of iOS 15.5 (tested on simulator), Xcode 13.4 additionally to adding .clipped() you also need to add the following extension to prevent the touch area overlap issue mentioned in the comments from the other answers:

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

Just place it before the struct of the View where you're using the Picker.

Source: TommyL on the Apple forum: https://developer.apple.com/forums/thread/687986?answerId=706782022#706782022

justinsAccount
  • 742
  • 1
  • 10
  • 10