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