Simple layout still fails

I'm surprised this simple code still doesn't work on iOS 13.3 / Xcode 11.3. On my iPhone SE it's almost all off screen.


It's just two pieces of text, side by side, and two pickers, side by side. Anyone know a workaround?


struct ContentView: View {
    
    @State var choice: Int = 10
    @State var choice2: Int = 10
    
    var body: some View {
        return VStack {
            HStack {
                Text("Some text here")
                Spacer()
                Text("Foo baz")
            }
            HStack {
                Picker(selection: self.$choice, label: Text("C1")) {
                    ForEach(0..<10) { n in
                        Text("\(n) a").tag(n)
                    }
                }
                Picker(selection: self.$choice2, label: Text("C2")) {
                    ForEach(0..<10) { n in
                        Text("\(n) b").tag(n)
                    }
                }
            }
        }
    }
}

Accepted Reply

It looks like the Picker types are set to use the device width by default. That would match with the default behavior of UIPickerView I believe. This then causes the lower HStack to be 2 screens wide, and the upper HStack grows to match, pushing the text either side of the spacer to the far left & right, off the screen.


You can force the pickers to become narrower by using the .frame() modifier to specify a minimum width of 0, thus allowing them to shrink (by default they're fixed-size):


struct ContentView: View {
    @State var choice: Int = 10
    @State var choice2: Int = 10

    var body: some View {
        VStack {
            HStack {
                Text("Some text here")
                Spacer()
                Text("Foo baz")
            }
            HStack {
                Picker(selection: $choice, label: Text("C1")) {
                    ForEach(0..<10) { n in
                        Text("\(n) a").tag(n)
                    }
                }
                .frame(minWidth: 0)
                Picker(selection: $choice2, label: Text("C2")) {
                    ForEach(0..<10) { n in
                        Text("\(n) b").tag(n)
                    }
                }
                .frame(minWidth: 0)
            }
        }
    }
}


There's a slight visual artifact with this approach though, as it seems the semitransparent divider lines used to focus the selected row are still rendering full-width and are overlapping in the center of your view, making a darker line there. You can add a .hidden() modifier to one of the pickers to verify that.


Happily, there's a fix for that, too: tell the pickers to clip their contents with the .clipped() modifier:


Picker(selection: $choice2, label: Text("C2")) {
    ForEach(0..<10) { n in
        Text("\(n) b").tag(n)
    }
}
.frame(minWidth: 0)
.clipped()


All will then be well with the world. Note though that there's a space between the pickers at this point. If you want them to line up perfectly (e.g. to mimic the multi-column appearance of a date/time picker) you'd need to set a spacing of zero on the containing HStack:


HStack(spacing: 0) {
    ...
}
  • This has stopped working in iOS 15. I'm looking for another workaround. The widths still split 50/50 as desired, but the touch target area seems to overlap, so dragging on the first picker causes the second wheel to scroll, unless you touch way towards the edge of the screen.

  • Found an answer: add .compositingGroup() modifier to second picker to fix the touch target issue.

  • Thanks for your help guys however, .compositingGroup() still didn't fix the issue for me.

Replies

It looks like the Picker types are set to use the device width by default. That would match with the default behavior of UIPickerView I believe. This then causes the lower HStack to be 2 screens wide, and the upper HStack grows to match, pushing the text either side of the spacer to the far left & right, off the screen.


You can force the pickers to become narrower by using the .frame() modifier to specify a minimum width of 0, thus allowing them to shrink (by default they're fixed-size):


struct ContentView: View {
    @State var choice: Int = 10
    @State var choice2: Int = 10

    var body: some View {
        VStack {
            HStack {
                Text("Some text here")
                Spacer()
                Text("Foo baz")
            }
            HStack {
                Picker(selection: $choice, label: Text("C1")) {
                    ForEach(0..<10) { n in
                        Text("\(n) a").tag(n)
                    }
                }
                .frame(minWidth: 0)
                Picker(selection: $choice2, label: Text("C2")) {
                    ForEach(0..<10) { n in
                        Text("\(n) b").tag(n)
                    }
                }
                .frame(minWidth: 0)
            }
        }
    }
}


There's a slight visual artifact with this approach though, as it seems the semitransparent divider lines used to focus the selected row are still rendering full-width and are overlapping in the center of your view, making a darker line there. You can add a .hidden() modifier to one of the pickers to verify that.


Happily, there's a fix for that, too: tell the pickers to clip their contents with the .clipped() modifier:


Picker(selection: $choice2, label: Text("C2")) {
    ForEach(0..<10) { n in
        Text("\(n) b").tag(n)
    }
}
.frame(minWidth: 0)
.clipped()


All will then be well with the world. Note though that there's a space between the pickers at this point. If you want them to line up perfectly (e.g. to mimic the multi-column appearance of a date/time picker) you'd need to set a spacing of zero on the containing HStack:


HStack(spacing: 0) {
    ...
}
  • This has stopped working in iOS 15. I'm looking for another workaround. The widths still split 50/50 as desired, but the touch target area seems to overlap, so dragging on the first picker causes the second wheel to scroll, unless you touch way towards the edge of the screen.

  • Found an answer: add .compositingGroup() modifier to second picker to fix the touch target issue.

  • Thanks for your help guys however, .compositingGroup() still didn't fix the issue for me.

Thank you, that's a very helpful answer.


You seem to have a good understanding of SwiftUI so I'm curious how you learned it. The documentation I've seen is often not very helpful. Many functions and properties have empty docs.


I don't understand the layout algorithm. It appears that after years of telling us autolayout was great, they've abandoned it. But I don't know what's happening in its place, and how one might hook into it to observe/debug/modify it. For example, I would have guessed that the `HStack` would constrain it's contents, but it doesn't. If you put `.frame(width: 320)` on it (320 being the device's screen width), the children pickers just overflow, without any error messages about broken constraints.

This is my first post, I think by sheer bumbling I found a way to get multiple pickers in a single view without the touch targets overlapping in XCode 13.2.1, IOS 15.2. I was unable to get it to work with various iterations of .clipped(), .compositingGroup() as discussed in the thread above, but did get it to work with embedding the Picker inside a horizontal ScrollView. In my example below, I also embedded the ScrollView in a Group so I could center the wheel of the ScrollView and keep its vertical dimensions tight, so the parent Starting Census View has a ListView, each child with 3 horizontally adjacent Picker views, all able to be turned independently and without landing on adjacent pickers' target areas. Holding my breath that this is not broken in a future OS 15 update.

struct DigitDialView: View {

    @Binding var currentValue: Int

    var minValue = 0

    var maxValue = 20

    var body: some View {

        Group {

            ScrollView {

                Picker("census",selection: $currentValue){

                    ForEach(minValue..<maxValue){value in

                        Text("\(value)").tag(value)

                    }

                }

                .pickerStyle(.wheel)

                .frame(width: 40)

            }

            .frame(height: 215)

        }

        .frame(height: 45)

       .clipped()

    }

}

    }

}

 

More bumbling, but just to add to my prior answer above, when I tried this in a new project, I was again inadvertently touching the touch targets of the wrong census. The fix was to embed the picker in a List within a NavigationView, with a navigationViewStyle of .stack. I wish I could recall where I found the tip to use the stack style, that was definitely a tip from StackOverflow but I can't find where I pulled it from, hat tip to whoever figured that out and apologies for being unable to specifically find whom to credit.


    @EnvironmentObject var displayedAssignment: DisplayedAssignment

    var body: some View {

        NavigationView{

            List{

                DigitDialView(currentValue: $displayedAssignment.dailyAssignment.count1, maxValue: 99)

                DigitDialView(currentValue: $displayedAssignment.dailyAssignment.count2, maxValue: 99)

            }           

        }.navigationViewStyle(.stack)