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)
                    }
                }
            }
        }
    }
}
Answered by Jim Dovey in 398576022

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) {
    ...
}
Accepted Answer

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

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)
Simple layout still fails
 
 
Q