skip to Main Content

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.

3

Answers


  1. Chosen as BEST ANSWER

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

  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

    Login or Signup to reply.
  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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search