Clickable area of SwiftUI Picker overlapping
Asked Answered
S

3

5

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.

Semivitreous answered 12/12, 2021 at 21:50 Comment(3)
There's no way to have the native picker work correctly in iOS 15.1, see my UIViewRepresentable solution idea at this question: #69122669Enough
Does this answer your question https://mcmap.net/q/911664/-how-to-change-swiftui-picker-view-size-with-no-additional-space?Bluma
@Bluma I just tested using .contentShape(Rectangle()) and it did not work. I answered the original post with an adjusted solution from Steve MSemivitreous
F
12

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

Fanchette answered 26/3, 2022 at 21:43 Comment(0)
C
5

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

Cruel answered 8/5, 2022 at 17:29 Comment(2)
Awesome idea. Only issue is, that it messes up the rounded corners of the selection area. But for me it's no problem, because I clip them anywayHanoverian
It works for me. Simple but effective! Thank you. However, it seems like to be only suitable for pickers in the same hstack. Is there any way to solve the overlapping issue of pickers in the same vstack?Fellowship
S
4

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)
        }
    }
}
Semivitreous answered 13/12, 2021 at 6:31 Comment(2)
This is the only solution that works for me and that avoid overlapping gestures between multiple Pickers. Thanks!Pecuniary
Thank you for this excellent answer. Anyone know how to make this more generic in case each picker has a different Type. I'm using enums to back my picker and am trying to convert this.Fanchette

© 2022 - 2024 — McMap. All rights reserved.