UIPicker detect tap on currently selected row
Asked Answered
J

8

47

I have a UIPickerView and The method didSelectRow is not called when tapping on a selected row. I need to handle this case. Any ideas?

Jarboe answered 18/8, 2010 at 23:4 Comment(2)
Honestly, slightly surprised no one knows what to do here. One thing I can think of is that you could subclass the pickerview and add your own touches methods, but that seems kinda overkill and messy. didSelectRow is called all the time when a row becomes the selected row, but not if the user taps (selects) the row which is already selected. There must be a method that I can hook into to catch this case without subclassing...Jarboe
similar question: #568305Cynthy
A
30

First, conform the class to the UIGestureRecognizerDelegate protocol

Then, in the view setup:

UITapGestureRecognizer *tapToSelect = [[UITapGestureRecognizer alloc]initWithTarget:self
                                                                                 action:@selector(tappedToSelectRow:)];
tapToSelect.delegate = self;
[self.pickerView addGestureRecognizer:tapToSelect];

And elsewhere:

#pragma mark - Actions

- (IBAction)tappedToSelectRow:(UITapGestureRecognizer *)tapRecognizer
{
    if (tapRecognizer.state == UIGestureRecognizerStateEnded) {
        CGFloat rowHeight = [self.pickerView rowSizeForComponent:0].height;
        CGRect selectedRowFrame = CGRectInset(self.pickerView.bounds, 0.0, (CGRectGetHeight(self.pickerView.frame) - rowHeight) / 2.0 );
        BOOL userTappedOnSelectedRow = (CGRectContainsPoint(selectedRowFrame, [tapRecognizer locationInView:self.pickerView]));
        if (userTappedOnSelectedRow) {
            NSInteger selectedRow = [self.pickerView selectedRowInComponent:0];
            [self pickerView:self.pickerView didSelectRow:selectedRow inComponent:0];
        }
    }
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return true;
}
Apocarpous answered 8/9, 2014 at 7:31 Comment(3)
To make this code work, I had add -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ // return return true; } as specified here: #22319927Chishima
This code will also fire if you scroll a micrometer then decide to stay on the same object. To deal with this issue I added a pan gesture recognizer to determine of the user was scrolling. In the scrolling state I didn't do anything in the tap recognizer callback.Raulrausch
@AdamJohns I tried to add PanGesture to my picker. But PanGesture selector is not calling. I have a UITapGestureRecognizer as well with this picker view. Do you have any idea or suggestion for that?Apostrophize
N
24

Nikolay's answer in Swift 4:

First, add a UITapGestureRecognizer to your UIPickerView in viewDidLoad() and let your UIViewController conform to the UIGestureRecognizerDelegate.

let tap = UITapGestureRecognizer(target: self, action: #selector(pickerTapped))
tap.delegate = self
self.pickerView.addGestureRecognizer(tap)

Add this function which calls your UIPickerViewDelegate when a tap on a row has been detected:

@objc func pickerTapped(tapRecognizer: UITapGestureRecognizer) {
    if tapRecognizer.state == .ended {
        let rowHeight = self.pickerView.rowSize(forComponent: 0).height
        let selectedRowFrame = self.pickerView.bounds.insetBy(dx: 0, dy: (self.pickerView.frame.height - rowHeight) / 2)
        let userTappedOnSelectedRow = selectedRowFrame.contains(tapRecognizer.location(in: self.pickerView))
        if userTappedOnSelectedRow {
            let selectedRow = self.pickerView.selectedRow(inComponent: 0)
            pickerView(self.pickerView, didSelectRow: selectedRow, inComponent: 0)
        }
    }
}

Add the shouldRecognizeSimultaneouslyWith method from UIGestureRecognizerDelegate:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}
Nereus answered 7/2, 2018 at 8:37 Comment(1)
Instead of writing "return true" inside func gestureRecognizer(...shouldRecognizeSimultaneouslyWith otherGestureRecognizer...), you can have it check the view first with something like, "if gestureRecognizer.view == self.pickerView {return true} return false". This is needed if you are using gestureRecognizer on other views in the ViewController.Matthewmatthews
M
10

Nikolay's Answer with Swift:

let tap = UITapGestureRecognizer(target: self, action: #selector(OCAccountSettingsViewController.pickerTapped(_:)))
tap.cancelsTouchesInView = false
tap.delegate = self
pickerView.addGestureRecognizer(tap)

important let your viewController confirm the UIGestureRecognizerDelegate otherwise handler won't fire:

func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true}

the last step will be the handler for tap event:

func pickerTapped(tapRecognizer:UITapGestureRecognizer)
{
    if (tapRecognizer.state == UIGestureRecognizerState.Ended)
    {
      let rowHeight : CGFloat  = self.pickerView.rowSizeForComponent(0).height
      let selectedRowFrame: CGRect = CGRectInset(self.pickerView.bounds, 0.0, (CGRectGetHeight(self.pickerView.frame) - rowHeight) / 2.0 )
      let userTappedOnSelectedRow = (CGRectContainsPoint(selectedRowFrame, tapRecognizer.locationInView(pickerView)))
      if (userTappedOnSelectedRow)
      {
        let selectedRow = self.pickerView.selectedRowInComponent(0)
        //do whatever you want here
      }
    }
  }
Meyers answered 2/1, 2017 at 13:9 Comment(0)
E
5

Solution touchesBegan: / touchesEnded: worked fine for me when using iOS 4.2/4.3, but they stopped working with iOS. Finally I got this solution which may be helpful: using tap gesture recognition.

    IBOutlet UIPickerView *picker;

    [picker addGestureRecognizer:
        [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(pickerTapped:)] autorelease]
    ];

In this case when a user taps on picker view, the selector

    -(void)pickerTapped:(UIGestureRecognizer *)gestureRecognizer

is invoked. Worked for me on both iOS 4.2+ / 5.0

Eulogy answered 24/1, 2012 at 10:39 Comment(0)
D
1

Here's swift code for figuring out if user tapped inside the selected row or not. I just converted obj-c code from Nikolay's answer.

func pickerTapped(sender: UITapGestureRecognizer){
    let rowHeight = self.rowSizeForComponent(0).height
    let selectedRowFrame = CGRectInset(self.bounds, 0.0, (CGRectGetHeight(self.frame) - rowHeight) / 2.0)
    let userTappedOnSelectedRow = CGRectContainsPoint(selectedRowFrame, sender.locationInView(self))
    if( userTappedOnSelectedRow ){
        // .. your action here ..
    }
}
Divaricate answered 14/8, 2015 at 7:38 Comment(0)
J
0

I solved this by subclassing a common superview and intercepting the touch events before sending them forward. In interface builder, I also added a button where the selector area is, attached an action to the touch up event, and sent the button to "the back" (that's very important so it doesn't return itself in hitTest). The most relevant code is below. It could be improved for more complicated, like multitouch, cases.

    @implementation InterceptorView


-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 UITouch *t=[touches anyObject];
 CGPoint p=[t locationInView:button];
 if ([button hitTest:p withEvent:event]) 
  started_in_button=YES;
 [hitView touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 [hitView touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 UITouch *t=[touches anyObject];
 CGPoint p=[t locationInView:button];
 if ([button hitTest:p withEvent:event] && started_in_button) {
  started_in_button=NO;
  [button sendActionsForControlEvents:UIControlEventTouchUpInside];
 }
 [hitView touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
 [hitView touchesCancelled:touches withEvent:event];
}

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
 hitView = [super hitTest:point withEvent:event];
 return self;
}
Jeffereyjefferies answered 28/10, 2010 at 19:50 Comment(0)
M
0

Here's Nikolay's trick in Swift v3:

First, make your UIViewController implement protocol UIGestureRecognizerDelegate, and add this method to it:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

Then, apply a UITapGestureRecognizer on the picker, and when your target is called, call the following extension method:

extension UIPickerView {
    func pickerTapped(nizer: UITapGestureRecognizer, onPick: @escaping (Int) -> ()) {
        if nizer.state == .ended {
            let rowHeight = self.rowSize(forComponent: 0).height
            let selectedRowFrame = self.bounds.insetBy(dx: 0, dy: (self.frame.height - rowHeight) / 2)

            // check if begin and end tap locations both fall into the row frame:
            if selectedRowFrame.contains(nizer.location(in: self)) &&
                   selectedRowFrame.contains(nizer.location(ofTouch: 0, in: self)) {
                onPick(self.selectedRow(inComponent: 0))
            }
        }
    }
}
Marceau answered 5/1, 2017 at 9:12 Comment(0)
B
0
  1. Add a tapGestureRecognizer to the pickerView as already suggested

  2. Provide views for the picker rows, not the title, via

    -(UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view

  3. In the gesture's callback method do this

-(void)pickerTapped:(UIGestureRecognizer *)gestureRecognizer {

UIPickerView * pv = (id)gestureRecognizer.view;
UIView * selView = [pv viewForRow:[pv selectedRowInComponent:0]
                     forComponent:0];
CGPoint touchPoint = [gestureRecognizer locationInView:selView];
BOOL tapOnSelection = CGRectContainsPoint(selView.bounds, touchPoint);

if (tapOnSelection) ...

}

I think is more elegant then doing pixel math

Bombazine answered 9/9, 2020 at 13:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.