Home > Software design >  How to add clickable images to UIPickerView rows?
How to add clickable images to UIPickerView rows?

Time:12-30

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
}

CodePudding user response:

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?

CodePudding user response:

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
    }
    
}
  • Related