skip to Main Content

I have a UIPickerView that is used to select musical instruments, and I have a play icon next to each instrument name that I want to play the appropriate sound when the user clicks it, so that they can hear what the instrument sounds like before they select it in the picker.

I have no problem creating images that response to taps in general.

However, within a UIPickerView, an image that is created as part of the row doesn’t seem to receive clicks (I would guess the UIPickerView somehow takes priority?)

What do I need to do to ensure that my images get tap events?

Many thanks!

func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
    let parentView = UIView()
    let label = UILabel(frame: CGRect(x: 60, y: 0, width: 80, height: 50))
    let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height:50))
    if #available(iOS 13.0, *) {
        imageView.image = UIImage.add
        
        let tapGR = UITapGestureRecognizer(target: self, action: #selector(self.imageTapped))
        imageView.addGestureRecognizer(tapGR)
        imageView.isUserInteractionEnabled = true
    } else {
        // Fallback on earlier versions
    }
    label.text = "red"
    parentView.addSubview(label)
    parentView.addSubview(imageView)
    
    return parentView
}

2

Answers


  1. Good question. I haven’t done a lot with this specific scenario but I’ve done similar stuff with UITableView. Some ideas:

    1. Double check the following: isUserEnabled is true for the whole set of subviews from parentView down to imageView? Is imageView toward the front? Maybe make imageView.layer.zPosition closer to 1 and others farther back? You can double check this in simulator with the debug view hierarchy stack icon:
      Debug View Hierarchy Icon in XCode
    2. Work around: Does it need to happen on a click or can you just play a small sample when you hit the didSelectRow delegate method?
    Login or Signup to reply.
  2. You won’t be able to add a gesture recognizer to the viewForRow view — all touches get eaten by the picker view.

    So, you could write your own picker view which, depending on how closely you want to replicate the built-in picker view, could be fairly simple or super complex.

    Or… you can add a tap recognizer to the picker view … then check if the tap is inside the center row’s image view.

    Here’s a quick example…

    We’ll use this as our custom row view:

    class MyPickerRowView: UIView {
    
        let label = UILabel(frame: CGRect(x: 60, y: 0, width: 80, height: 50))
        let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 50, height:50))
    
        var selected: Bool = false
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            if #available(iOS 13.0, *) {
                imageView.image = UIImage.add
            } else {
                // Fallback on earlier versions
                imageView.backgroundColor = .red
            }
            addSubview(label)
            addSubview(imageView)
        }
        func tappedImageView(_ p: CGPoint) -> Bool {
            // let's toggle the image if the tap hits
            if imageView.frame.contains(p) {
                selected.toggle()
                if #available(iOS 13.0, *) {
                    imageView.image = selected ? UIImage.checkmark : UIImage.add
                } else {
                    imageView.backgroundColor = selected ? .green : .red
                }
                return true
            }
            return false
        }
    
    }
    

    The only "non-usual" thing is the func tappedImageView(_ p: CGPoint) -> Bool… it will check the passed point and return true if the point is inside the image view frame, or false if it’s not.

    We’re also going to toggle the image between UIImage.add and UIImage.checkmark so we have some visual feedback.

    Our tap gesture handler (in the controller class) will:

    • get the view for the selected (center) row
    • make sure it’s our custom MyPickerRowView class
    • translate the tap point to that view
    • ask that view if the point is inside the image view frame

    So, here’s the controller class:

    class PickerTestViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate, UIGestureRecognizerDelegate {
        
        let picker = UIPickerView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            picker.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(picker)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                picker.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                picker.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                picker.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
            // create a tap recognizer
            let tap = UITapGestureRecognizer(target: self, action: #selector(pickerTapped(_:)))
            // don't prevent all the normal behaviors of the picker view
            tap.cancelsTouchesInView = false
            // tap delegate needs to be set so we can use shouldRecognizeSimultaneouslyWith
            tap.delegate = self
            // add the tap recognizer to the PICKER view itself
            picker.addGestureRecognizer(tap)
            
            picker.delegate = self
            picker.dataSource = self
    
            // just so we can easily see the frame of the picker view
            picker.layer.borderColor = UIColor.red.cgColor
            picker.layer.borderWidth = 1
            
        }
        
        @objc func pickerTapped(_ tapRecognizer:UITapGestureRecognizer) {
            
            // get the index of the selected row (the "center" row)
            let n = picker.selectedRow(inComponent: 0)
            
            // make sure the view for that row is a MyPickerRowView
            if let v = picker.view(forRow: n, forComponent: 0) as? MyPickerRowView {
                // convert the tap location to that view
                let p = tapRecognizer.location(in: v)
                // ask the view if the tap is in the image view frame
                if v.tappedImageView(p) {
                    print("tapped in image view in center row")
                    // do something
                }
            }
        }
        
        func numberOfComponents(in pickerView: UIPickerView) -> Int {
            return 1
        }
        
        func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return 100
        }
        func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
            return 50
        }
        
        func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
            let parentView = MyPickerRowView()
            parentView.label.text = "(row)"
            return parentView
        }
        
        func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search