'Clickable area of SwiftUI Picker overlapping

I am currently trying to create a page with three adjacent Picker views inside of an HStack as seen below:

enter image description here

I made a CustomPicker view where I limit the frame to 90 x 240, and then use .compositingGroup() and .clipped() to make the selectable area of each picker not overlap.

CustomPicker.swift

import SwiftUI

struct CustomPicker: View {
    @Binding var selection: Int
    let pickerColor: Color
    
    var numbers: some View {
        ForEach(0...100, id: \.self) { num in
            Text("\(num)")
                .bold()
        }
    }
    
    var stroke: some View {
        RoundedRectangle(cornerRadius: 16)
            .stroke(lineWidth: 2)
    }
    
    var backgroundColor: some View {
        pickerColor
            .opacity(0.25)
    }
    
    var body: some View {
        Picker("Numbers", selection: $selection) {
            numbers
        }
        .frame(width: 90, height: 240)
        .compositingGroup()
        .clipped()
        .pickerStyle(.wheel)
        .overlay(stroke)
        .background(backgroundColor)
        .cornerRadius(16)
    }
}

ChoicePage.swift

struct ChoicePage: View {
    @State var choiceA: Int = 0
    @State var choiceB: Int = 0
    @State var choiceC: Int = 0
    
    var body: some View {
        HStack(spacing: 18) {
            CustomPicker(selection: $choiceA, pickerColor: .red)
            CustomPicker(selection: $choiceB, pickerColor: .green)
            CustomPicker(selection: $choiceC, pickerColor: .blue)
        }
    }
}

When testing both CustomPicker and ChoicePage in the preview canvas and simulator, it had worked perfectly fine, but when I tried to use it on my physical devices (iPhone 8 and iPhone 13, both on iOS 15.1) the clickable areas overlap. I have tried solutions from this post and this post, as well as many others, but nothing seems to be working for me.



Solution 1:[1]

I solved this issue by modifying the solution from Steve M, so all the credit for this goes to him.

He uses a UIViewRepresentable, but in his implementation, it's for three different selections inside of one. I slightly adjusted his implementation, to be used for just one value to select from in a given picker.

I start with BasePicker, which acts as the UIViewRepresentable:

BasePicker.swift

struct BasePicker: UIViewRepresentable {
    var selection: Binding<Int>
    let data: [Int]
    
    init(selecting: Binding<Int>, data: [Int]) {
        self.selection = selecting
        self.data = data
    }
    
    func makeCoordinator() -> BasePicker.Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: UIViewRepresentableContext<BasePicker>) -> UIPickerView {
        let picker = UIPickerView()
        picker.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        picker.dataSource = context.coordinator
        picker.delegate = context.coordinator
        return picker
    }
    
    func updateUIView(_ view: UIPickerView, context: UIViewRepresentableContext<BasePicker>) {
        guard let row = data.firstIndex(of: selection.wrappedValue) else { return }
        view.selectRow(row, inComponent: 0, animated: false)
    }
    
    class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
        var parent: BasePicker
        
        init(_ pickerView: BasePicker) {
            parent = pickerView
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
        
        func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
            return 90
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return parent.data.count
        }
        
        func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
            return parent.data[row].formatted()
        }
        
        func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
            parent.selection.wrappedValue = parent.data[row]
        }
    }
}

I then use the BasePicker Representable inside of CustomPicker, which is a SwiftUI View. I did this to make it a bit easier to keep my previous styling/structure in the original code.

CustomPicker.swift

struct CustomPicker: View {
    @Binding var selection: Int
    let pickerColor: Color

    let numbers: [Int] = Array(stride(from: 0, through: 100, by: 1))
    
    var stroke: some View {
        RoundedRectangle(cornerRadius: 16)
            .stroke(lineWidth: 2)
    }
    
    var backgroundColor: some View {
        pickerColor
            .opacity(0.25)
    }
    
    var body: some View {
        BasePicker(selecting: $selection, data: numbers)
            .frame(width: 90, height: 240)
            .overlay(stroke)
            .background(backgroundColor)
            .cornerRadius(16)
    }
}

I then just need to slightly change ChoicePage and it's fixed. Also, take note that I moved the numbers array into my CustomPicker view, but you adust it so that you can pass it in from ChoicePage if you wanted.

ChoicePage.swift

struct ChoicePage: View {
    @State var choiceA: Int = 0
    @State var choiceB: Int = 0
    @State var choiceC: Int = 0
    
    var body: some View {
        HStack(spacing: 18) {
            CustomPicker(selection: $choiceA, pickerColor: .red)
            CustomPicker(selection: $choiceB, pickerColor: .green)
            CustomPicker(selection: $choiceC, pickerColor: .blue)
        }
    }
}

Solution 2:[2]

adding this extension is working for me in 15.4

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

found at https://developer.apple.com/forums/thread/687986?answerId=706782022#706782022

Solution 3:[3]

I have a workaround for iOS 15+.

Use .scaleEffect(x: 0.5) to half the touchable area, of the Inline picker.

This will however also squish the text inside it, to fix this, apply .scaleEffect(x: 2), ONLY to the text inside the ForEach.

  var body: some View {
      Picker(selection: $number, label: Text(""), content: {
            ForEach(0..<21) {value in
            Text("\(value)").tag(number)
                .scaleEffect(x: 3)
            }
        }
    )
    .pickerStyle(InlinePickerStyle())
    .scaleEffect(x: 0.333)
}

Screenshot of result, where the touchable area of the picker is smaller in width

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Prasanth
Solution 3 Cedan Misquith