Gesture recognizer and button actions - iphone

I have a view hierarchy that looks something like this:
UIView (A)
UIView > UIImageView
UIView > UIView (B)
UIView > UIView (B) > Rounded Rect Button
UIView > UIView (B) > UIImageView
UIView > UIView (B) > UILabel
I've attached gesture recognizer(s) to my UIView (B). The problem that i'm facing is that i don't get any actions for the Rounded Rect Button which is inside the UIView (B). The singleTap gesture recognizer captures/overrides the button's Touch Up Inside event.
How can i make it work? I thought that the responder chain hierarchy will make sure that the button touch event will be given preference, and it WILL get triggered! What am i missing?
Here's some related code:
#pragma mark -
#pragma mark View lifecycle (Gesture recognizer setup)
- (void)viewDidLoad {
[super viewDidLoad];
// double tap gesture recognizer
UITapGestureRecognizer *dtapGestureRecognize = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapGestureRecognizer:)];
dtapGestureRecognize.delegate = self;
dtapGestureRecognize.numberOfTapsRequired = 2;
[self.viewB addGestureRecognizer:dtapGestureRecognize];
// single tap gesture recognizer
UITapGestureRecognizer *tapGestureRecognize = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapGestureRecognizer:)];
tapGestureRecognize.delegate = self;
tapGestureRecognize.numberOfTapsRequired = 1;
[tapGestureRecognize requireGestureRecognizerToFail:dtapGestureRecognize];
[self.viewB addGestureRecognizer:tapGestureRecognize];
// add gesture recodgnizer to the grid view to start the edit mode
UILongPressGestureRecognizer *pahGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressGestureRecognizerStateChanged:)];
pahGestureRecognizer.delegate = self;
pahGestureRecognizer.minimumPressDuration = 0.5;
[self.viewB addGestureRecognizer:pahGestureRecognizer];
[dtapGestureRecognize release];
[tapGestureRecognize release];
[pahGestureRecognizer release];
}
#pragma mark -
#pragma mark Button actions
- (IBAction)buttonTouchUpInside:(id)sender {
NSLog(#"%s, %#", __FUNCTION__, sender);
}
#pragma mark -
#pragma mark Gesture recognizer actions
- (void)singleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%s", __FUNCTION__);
}
- (void)doubleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%s", __FUNCTION__);
}
- (void)longPressGestureRecognizerStateChanged:(UIGestureRecognizer *)gestureRecognizer {
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateEnded: {
NSLog(#"%s", __FUNCTION__);
break;
}
default:
break;
}
}

In the "shouldReceiveTouch" method you should add a condition that will return NO if the touch is in the button.
This is from apple SimpleGestureRecognizers example.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
// Disallow recognition of tap gestures in the segmented control.
if ((touch.view == yourButton)) {//change it to your condition
return NO;
}
return YES;
}
hope it will help
Edit
As Daniel noted you must conform to UIGestureRecognizerDelegate for it to work.
shani

I also had the same problem , then i tried with
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if ([touch.view isKindOfClass:[UIButton class]]) { //change it to your condition
return NO;
}
return YES;
}
It is working now perfectly.........

Generally speaking, we use below delegate method to avoid the touch in all kinds of UIControls:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (([touch.view isKindOfClass:[UIControl class]])) {
return NO;
}
return YES;
}
Note: DO NOT do this check (check the recognizer.view class type) the gestureRecognizerShouldBegin, it won't work.

Here is a Swift 3.0 version:
extension UIViewController: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view is UIButton {
return false
}
return true
}
Don't forget to:
Make your tapper object delegate to self (e.g: tapper.delegate = self)

The best solution is to my mind using code snippet below:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
CGPoint touchLocation = [touch locationInView:self.view];
return !CGRectContainsPoint(self.menuButton.frame, touchLocation);
}

Here is a Swift version:
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if (touch.view!.isKindOfClass(UIButton)) {
return false
}
return true
}
Don't forget to:
Make you class conform to UIGestureRecognizerDelegate
Make your tapper object delegate to self (e.g: tapper.delegate = self)

for Swift 5 inside the delegate you can just add:
return (touch.view is UIButton) ? false : true

Swift version of Vivienne answer:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let location = touch.location(in: view)
return !someView.frame.contains(location)
}

My case was the following :
UIView (with tap gesture)
├-> UIView (named "textFieldView")
├-> UIControl
├-> UITextField
├-> UIView (over the textField, with a tap gesture)
╭──────────────────────────────╮
│ ╭──────────────────────────╮ │
│ │ ╭──╮ ┌╴╶╴╶╴╶╴╶╴╶╴╶╴╶╴╶┐ │ │
│ │ ╰──╯ └╴╶╴╶╴╶╴╶╴╶╴╶╴╶╴╶┘ │ │
│ ╰──────────────────────────╯ │
└──────────────────────────────┘
The tapGesture on the upper UIView was causing a conflict with the UIControl target. The target was called if the target was setup with touchDown (but not called for touchUpInside) or if I was changing the inheritance from UIControl to UIButton while keeping the target setup on event touchUpInside.
What I do to solve that is :
upper view is setup as delegate of its tap gesture.
Function func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool is implemented (in upper UIView) like that :
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let touchLocationRelatedToTextFieldView = touch.location(in: textFieldView)
return textFieldView.frame.contains(touchLocationRelatedToTextFieldView) == false
}
With that delegate function implemented I'm now able to keep my UIControl and the target setup on touchUpInside! :)

Related

iPhone iOS how to add a UILongPressGestureRecognizer and UITapGestureRecognizer to the same control and prevent conflict?

I'm building an iPhone app that would let the user rearrange some of the UI elements on the screen.
How can I add a tap gesture recognizer and a long press gesture recognizer to the same UIView? When I lift up the finger from the long press, the tap gesture recognizer fires. How can I temporarily disable the tap gesture recognizer or prevent it from firing when the user is performing a long press?
Thank you!
To allow both gestures to work together, implement the following delegate method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
To make it so that the long press has first priority, do:
[tapGesture requireGestureRecognizerToFail:longPress];
To combine successfully both you need:
1º Add to interface gesture delegate at header
#interface ViewController : ViewController <UIGestureRecognizerDelegate>
2º Create gesture events and add to a view into source file:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(touch:)];
[tap setNumberOfTapsRequired:1]; // Set your own number here
[tap setDelegate:self]; // Add the <UIGestureRecognizerDelegate> protocol
UILongPressGestureRecognizer *longTap = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longTouch:)];
[longTap setNumberOfTapsRequired:0]; // Set your own number here
[longTap setMinimumPressDuration:1.0];
[longTap setDelegate:self]; // Add the <UIGestureRecognizerDelegate> protocol
[tap requireGestureRecognizerToFail:longTap]; // Priority long
[self.view addGestureRecognizer:tap];
[self.view addGestureRecognizer:longTap];
3º Add callbacks in source file:
- (void) touch: (UITapGestureRecognizer *)recognizer
{
CGPoint location = [recognizer locationInView: self.HUDview];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
NSLog(#"touch UIGestureRecognizerStateBegan");
}
if (recognizer.state == UIGestureRecognizerStateEnded)
{
NSLog(#"touch UIGestureRecognizerStateEnded");
//NSLog(#"Position of touch: %.3f, %.3f", location.x, location.y); // Position landscape
}
}
- (void) longTouch: (UILongPressGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
NSLog(#"longTouch UIGestureRecognizerStateBegan");
}
if (recognizer.state == UIGestureRecognizerStateEnded)
{
NSLog(#"longTouch UIGestureRecognizerStateEnded");
}
}
4º Set gesture recognizer available:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
As an alternative approach, don't have two separate recognisers - just use the LongPress recogniser for both events:
Configure as follows:
UILongPressGestureRecognizer* longPress = [ [ UILongPressGestureRecognizer alloc ] initWithTarget:self.nextResponder action:#selector(longPressEvent:)];
categoryPanelDrag.minimumPressDuration = 0.0;
Then handle as follows:
- (BOOL)longPressEvent:(UILongPressGestureRecognizer *)gesture {
// _dragStarted is a class-level BOOL
if(UIGestureRecognizerStateBegan == gesture.state) {
_dragStarted = NO;
}
if(UIGestureRecognizerStateChanged == gesture.state) {
_dragStarted = YES;
// Do dragging stuff here
}
if(UIGestureRecognizerStateEnded == gesture.state) {
if (_dragStarted == NO)
{
// Do tap stuff here
}
else
{
// Do drag ended stuff here
}
}
return YES;
}
I did try moby and journeyman's approach but somehow they didn't fit my project well, so I solved like below,
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog(#"%# %ld",touch.description, touch.phase);
[self performSelector:#selector(checkTouch:) withObject:touch afterDelay:0.5];
return YES;
}
and
- (void)checkTouch:(UITouch *)touch{
NSLog(#"touch phase = %ld",touch.phase);
if (touch.phase == UITouchPhaseStationary) {
//still holding my hand and this means I wanted longPressTouch
}
if (touch.phase == UITouchPhaseEnded){
//I released my finger so it's obviously tap
}
}
It could be simpler solution but of course it depends to project.
You could take care of it in the code, that during the long press, set a flag, and if the tap gets called while the flag is true or whatever then don't execute the tap code and reset the flag. I don't know a better way

MKMapView hide navigationBar on tap like in photo app without losing functionality of MKMapView

I would like to hide and show the navigation bar on tap like the one in the photos app
BUT without losing the functionality of the MKMapView. the user should still be able to double tap for zoom, pinch and zoom and be able to select annotations.
I tried it with:
UITapGestureRecognizer* tapRec = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(hideBar:)];
[self.myMKMapView addGestureRecognizer:tapRec];
[tapRec release];
But then the user can't select annotations anymore!And it also hides on double taps.
Any ideas ?
I know, it's almost exactly one year too late, but I hope somebody can make a use of it. Here's how I did it, based on #Cocoanetics's answer:
BOOL mapReceivedDoubleTap;
...
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTapMap:)];
[tapGestureRecognizer setDelegate:self];
[_mapView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer release];
...
// ignore annotations
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
return (![[touch view] isKindOfClass:[MKAnnotationView class]]);
}
// take care of double taps for zoom
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if([otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
UITapGestureRecognizer *tr = (UITapGestureRecognizer *)otherGestureRecognizer;
if(tr.numberOfTapsRequired == 2)
mapReceivedDoubleTap = YES;
}
return NO;
}
- (void)didTapMap:(UITapGestureRecognizer *)tapGestureRecognizer {
mapReceivedDoubleTap = NO;
// hide/show on delay
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, .2f * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if(!mapReceivedDoubleTap)
[self.navigationController setNavigationBarHidden:!self.navigationController.navigationBarHidden animated:YES];
});
}
Swift
import MapKit
class MapViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var myMapView: MKMapView!
var mapReceivedDoubleTap = false
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(MapViewController.didTapMap(_:)))
tapGestureRecognizer.delegate = self
myMapView.addGestureRecognizer(tapGestureRecognizer)
}
// ignore annotations
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return !touch.isKindOfClass(MKAnnotationView)
}
// take care of double taps for zoom
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.isKindOfClass(UITapGestureRecognizer) {
let tr = otherGestureRecognizer as! UITapGestureRecognizer
if tr.numberOfTapsRequired == 2 {
mapReceivedDoubleTap = true
}
}
return false
}
func didTapMap(gestureRecognizer: UIGestureRecognizer) {
mapReceivedDoubleTap = false
// hide/show on delay
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.2 * Double(NSEC_PER_SEC)))
dispatch_after(popTime, dispatch_get_main_queue(), {
if !self.mapReceivedDoubleTap {
self.navigationController?.setNavigationBarHidden(!(self.navigationController?.navigationBarHidden)!, animated: true)
}
})
}
}
you probably need to to implement the delegate method for this gesture recognizer to detect simultaneously as the one on the MKMapView. Then you need to perform your hiding/showing on a delay and if an annotation gets selected you need to cancel this.
Alternatively you can do a hitTest in the delegate method that allows you to prevent touches from being delivered to your gesture if the hit view is an MKAnnotationView.
You can tell your gesture recognizer to trigger only if every gesture recognizer from the map fail.
UITapGestureRecognizer* tapRec = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(hideBar:)];
for (UIGestureRecognizer recognizer in self.myMKMapView.gestureRecognizers) {
[tapRec requireGestureRecognizerToFail:recognizer];
}
[self.myMKMapView addGestureRecognizer:tapRec];
[tapRec release];
I don't know it is a gestureRecognizer that handles the annotation though. Guess you'll have to try.
You can prevent a single click gesture recognizer from stealing the double click one with this code:
self.singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
singleTap.numberOfTapsRequired = 1;
for (UIGestureRecognizer* recognizer in self.mapView.gestureRecognizers) {
if([recognizer isKindOfClass: [UITapGestureRecognizer class]] && ((UITapGestureRecognizer*)recognizer).numberOfTapsRequired == 2) {
[singleTap requireGestureRecognizerToFail:recognizer];
}
}
[self.mapView addGestureRecognizer:self.singleTap];
Can can prevent it from stealing other gestures in the same way.

How to detect touch on UIWebView

On UIWebview, how can I detect a touch?
But not when user clicks some URL or touching a control.
Is it possible to handle it?
Use UIGestureRecognizerDelegate method:
Add UIGestureRecognizerDelegate in declaration file (i.e. your .h file)
Step 1: Just set the delegate of gestureRecognizer: (in .m file viewDidLoad)
UITapGestureRecognizer *webViewTapped = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(tapAction:)];
webViewTapped.numberOfTapsRequired = 1;
webViewTapped.delegate = self;
[offScreenWebView addGestureRecognizer:webViewTapped];
[webViewTapped release];
Step 2: Override this function: (in .m file)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
Step 3: Now implement the tapAction function:
- (void)tapAction:(UITapGestureRecognizer *)sender
{
NSLog(#"touched");
// Get the specific point that was touched
CGPoint point = [sender locationInView:self.view];
}
The accepted answer is great if you only need to detect taps. If you need to detect all touches, the best way is to create a new UIView subclass and place it over the webview. In the subclass you can detect touches using hitTest:
TouchOverlay.h
#class TouchOverlay;
#protocol TouchOverlayDelegate <NSObject>
#optional
- (void)touchOverlayTouched:(TV4TouchOverlay *)touchOverlay;
#end
#interface TouchOverlay : UIView
#property (nonatomic, unsafe_unretained) id <TouchOverlayDelegate> delegate;
#end
Touchoverlay.m
#implementation TouchOverlay
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
if (self.delegate && [self.delegate respondsToSelector:#selector(touchOverlayTouched:)]) {
[self.delegate touchOverlayTouched:self];
}
return nil; // Tell the OS to keep looking for a responder
}
return hitView;
}
#end
Note that the accepted answer above will only capture tap gestures (touchDown and touchUp without a drag in between), and that swipe gestures will be ignored.
For my purposes I needed to be informed of both, and so I added swipe gesture recognizers appropriately. (Note that despite being a bit field, you can't OR together swipe gesture recognizers' direction property, so 4 gesture recognizers are required to detect any swipe).
// Note that despite being a bit field, you can't `OR` together swipe gesture
// recognizers' `direction` property, so 4 gesture recognizers are required
// to detect any swipe
for (NSNumber * swipeDirection in #[#(UISwipeGestureRecognizerDirectionUp), #(UISwipeGestureRecognizerDirectionDown), #(UISwipeGestureRecognizerDirectionLeft), #(UISwipeGestureRecognizerDirectionRight)]) {
UISwipeGestureRecognizer * swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(timerReset:)];
swipe.direction = [swipeDirection integerValue];
swipe.delegate = self;
[rootWebView addGestureRecognizer:swipe];
}
Everything that inherits from UIResponder can handle touches (so does UIWebView). Read the doc:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIResponder_Class/Reference/Reference.html
You'll have to use:
touchesBegan:withEvent:
Edit: Adding the comment here for clarity-
I believe then there's no clean way of doing it, you can either override the hittest withEvent method like this or do a hack like this: overriding UIView
Do you mean you want to override the options that popup when they hold down on a link? I managed to get one to work with this tutorial/guide but the one posted here is still slightly buggy and needs you to do some fine tuning:
http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/

dismissing the NumberPad keyboard from a UIScrollView

I have an application with UIScrollView added as a subview of UIView. This Scroll view has a textfield with keyboard type set to numberPad.
Now the problem is , i want to dismiss the keyboard when i tap anywhere else in the scroll view. how can i do this ... ?
Just call the textField's resignFirstResponder in the touch handler.
(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[myTextField resignFirstResponder];
}
When I added the gesture to a subclass of UIScrollView, I was having problems with the various gestures in my view tree interfering with each other, such as being able to click on subviews, scroll the view, and have the keyboard dismiss in all cases. I came up with this solution, which can be setup from a superclass of UIScrollView or from a UIViewController.
The DismissKeyboardTapGesture class uses ARC, works with any text fields under the view, and doesn't take over any clicks from subviews like buttons. Also takes advantage of iOS7 scrolling effect to dismiss keyboard.
Setting up from UISScrollView superclass:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self];
or from UIViewController:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self.view];
Here is the class:
#interface DismissKeyboardTapGesture : NSObject <UIGestureRecognizerDelegate>
#end
#implementation DismissKeyboardTapGesture
- (id)initWithView:(UIView *)view
{
self = [super init];
if (self) {
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTap:)];
singleTap.cancelsTouchesInView = NO;
singleTap.delegate = self;
[view addGestureRecognizer:singleTap];
if ([view respondsToSelector:#selector(setKeyboardDismissMode:)]) {
// Bonus effect to dismiss keyboard by scrolling
((UIScrollView *)view).keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
}
}
return self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// Don't stop any existing gestures in our view from working
if (otherGestureRecognizer.view == gestureRecognizer.view) {
return YES;
}
return NO;
}
- (void)singleTap:(UIGestureRecognizer*)gestureRecognizer
{
// Close keyboard for any text edit views that are children of the main view
[gestureRecognizer.view endEditing:YES];
}
#end

UISegmentedControl register taps on selected segment

I have a segmented control where the user can select how to order a list. Works fine.
However, I would like that when an already selected segment is tapped, the order gets inverted. I have all the code in place, but I don't know how to register the taps on those segments. It seems the only control event you can use is UIControlEventValueChanged, but that isn't working (since the selected segment isn't actually changing).
Is there a solution for this? And if so, what is it?
Thanks in advance!
You can subclass UISegmentedControl, and then override setSelectedSegmentIndex:
- (void) setSelectedSegmentIndex:(NSInteger)toValue {
if (self.selectedSegmentIndex == toValue) {
[super setSelectedSegmentIndex:UISegmentedControlNoSegment];
} else {
[super setSelectedSegmentIndex:toValue];
}
}
If using IB, make sure you set the class of your UISegmentedControl to your subclass.
Now you can listen for the same UIControlEventValueChanged as you would normally, except if the user deselected the segment, you will see a selectedSegmentIndex equal to UISegmentedControlNoSegment:
-(IBAction) valueChanged: (id) sender {
UISegmentedControl *segmentedControl = (UISegmentedControl*) sender;
switch ([segmentedControl selectedSegmentIndex]) {
case 0:
// do something
break;
case 1:
// do something
break;
case UISegmentedControlNoSegment:
// do something
break;
default:
NSLog(#"No option for: %d", [segmentedControl selectedSegmentIndex]);
}
}
I think it is even a little better if you use -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event---as this is the behaviour of UISegmentedControl. Further, it seems you don't need to overload the -(void)setSelectedSegmentIndex:(NSInteger)toValue
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSInteger current = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if (current == self.selectedSegmentIndex)
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
Wanted this myself. Took Julian's solution (thanks!) and modified slightly.
The following UISegmentedControl subclass simply triggers the UIControlEventValueChanged event even when the value didn't change, which obviously reads a bit weird, but works fine in my case and keeps things simple.
AKSegmentedControl.h
#import <UIKit/UIKit.h>
#interface AKSegmentedControl : UISegmentedControl {
}
#end
AKSegmentedControl.m
#import "AKSegmentedControl.h"
#implementation AKSegmentedControl
- (void)setSelectedSegmentIndex:(NSInteger)toValue {
// Trigger UIControlEventValueChanged even when re-tapping the selected segment.
if (toValue==self.selectedSegmentIndex) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
[super setSelectedSegmentIndex:toValue];
}
#end
This works on both iOS 4 and 5:
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
int oldValue = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if ( oldValue == self.selectedSegmentIndex )
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
The current solution presented still does not work, because setSelectedSegmentIndex is never called unless really a new segment is tapped. At least in my case this never worked, I do use iOS5 though, perhaps this solution did work in iOS4. Anyway, this is my solution.
It needs one extra subclass method, which is the following:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self setSelectedSegmentIndex:self.selectedSegmentIndex];
[super touchesEnded:touches withEvent:event];
}
Good luck!
This was a pretty old question so I thought I'd add the rather simple solution I used when I ran into this in an iOS 5+ app.
All I did was drag a Tap Gesture Recognizer onto the UISegmentedControl in my .xib file. Check the connections for your new gesture recognizer to make sure the segmented control is the only gestureRecognizer outlet and then just wire up it's action to the method you want to fire in your controller.
This method should fire any time you touch the segmented control, so just check the selected index with perhaps an instance variable to see if the user touched the already selected segment.
#implementation MyViewController
#synthesize selectedSegment;
#synthesize segmentedControl;
// connected to Tap Gesture Recognizer's action
- (IBAction)segmentedControlTouched:(id)sender
{
NSLog(#"Segmented Control Touched");
if (selectedSegment == [segmentedControl selectedSegmentIndex]) {
// Do some cool stuff
}
selectedSegment = [segmentedControl selectedSegmentIndex];
}
#end
On iOS8 every answer here seem to either not work or trigger change twice. I came up with very simple solution - so I'd like to share it.
In subclass I only have:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self setSelectedSegmentIndex:UISegmentedControlNoSegment];
}
What it does is to reset selected segment to -1 before changing segmented control segment. It will happen in touchesEnded method.
Swift 5
class ReselectableSegmentedControl: UISegmentedControl {
// Captures existing selected segment on touchesBegan.
var oldValue: Int!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.oldValue = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if self.oldValue == self.selectedSegmentIndex {
self.sendActions(for: .valueChanged)
}
}
}
From here
This works:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
int oldValue = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
if (oldValue == self.selectedSegmentIndex)
{
[super setSelectedSegmentIndex:UISegmentedControlNoSegment];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
In Swift 3 I could imagine at least 2 solutions as follows. For a special case I posted also a third solution, where the selected segment works as toggle button.
Solution 1:
Hint: This solution only works on momentary controlSegments! If you need a solution for stationary controls, choose Solution 2
The idea is to register a second event that fires up on touch-up-inside:
// this solution works only for momentary segment control:
class Solution1ViewController : UIViewController {
var current:Int = UISegmentedControlNoSegment
#IBOutlet weak var mySegmentedControl: UISegmentedControl! {
didSet {
guard mySegmentedControl != nil else { return }
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .valueChanged)
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .touchUpInside)
}
}
func changeSelection(sender: UISegmentedControl) {
if current == sender.selectedSegmentIndex {
// user hit the same button again!
print("user hit the same button again!")
}
else {
current = sender.selectedSegmentIndex
// user selected a new index
print("user selected a new index")
}
}
}
Solution 2:
The other way is to override the touch functions in UISegmentedControl and to fire the valueChanged even if the segment index has not changed. Therefore you could override the UISegmentedControl as follows:
// override UISegmentControl to fire event on second hit:
class Solution2SegmentControl : UISegmentedControl
{
private var current:Int = UISegmentedControlNoSegment
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
current = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if current == self.selectedSegmentIndex {
self.sendActions(for: .valueChanged)
}
}
}
// solution2 works with stationary (default) segment controls
class Solution2ViewController : UIViewController {
var current:Int = UISegmentedControlNoSegment
#IBOutlet weak var mySegmentedControl: UISegmentedControl! {
didSet {
guard mySegmentedControl != nil else { return }
self.mySegmentedControl!.addTarget(
self,
action:#selector(changeSelection(sender:)),
for: .valueChanged)
}
}
func changeSelection(sender: UISegmentedControl) {
if current == sender.selectedSegmentIndex {
// user hit the same button again!
print("user hit the same button again!")
}
else {
current = sender.selectedSegmentIndex
// user selected a new index
print("user selected a new index")
}
}
}
Solution 3:
If your approach was to have the selected segment be a toggle button than Solution 2 could be changed to clean up the code like this:
class MyToggleSegmentControl : UISegmentedControl {
/// you could enable or disable the toggle behaviour here
var shouldToggle:Bool = true
private var current:Int = UISegmentedControlNoSegment
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
current = self.selectedSegmentIndex
super.touchesBegan(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
if shouldToggle, current == self.selectedSegmentIndex {
self.selectedSegmentIndex = UISegmentedControlNoSegment
}
}
}
Now we could clean up the changeSelection function as follows:
func changeSelection(sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case UISegmentedControlNoSegment:
print("none")
default:
print("selected: \(sender.selectedSegmentIndex)")
}
}
The first idea I had was wire up the Touch Up Inside or Touch Down actions to your sort method, but this doesn't seem to work.
The second idea is more of a work around, set the Momentary property on the segmented control. This will then fire a Value Did Change action every time it is tapped.
Big help! What I want to do is have the option of one or no buttons set - and when a button is set, a second tap unsets it. This is my modification:
- (void)setSelectedSegmentIndex:(NSInteger)toValue
{
// Trigger UIControlEventValueChanged even when re-tapping the selected segment.
if (toValue==self.selectedSegmentIndex) {
[super setSelectedSegmentIndex:UISegmentedControlNoSegment]; // notify first
[self sendActionsForControlEvents:UIControlEventValueChanged]; // then unset
} else {
[super setSelectedSegmentIndex:toValue];
}
}
Notify first lets you read the control to find the current setting before it's reset.
Here is my solution. The most elegant I think if you want the ValueChanged event to fire on every touches...
.h
#interface UISegmentedControlAllChanges : UISegmentedControl
#end
.m
#implementation UISegmentedControlAllChanges
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventValueChanged];
[super touchesEnded:touches withEvent:event];
}
#end
Based on Bob de Graaf's answer:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventAllTouchEvents];
[super touchesEnded:touches withEvent:event];
}
Note the use of UIControlEventAllTouchEvents instead of UIControlEventValueChanged.
Also there's no need to call -(void)setSelectedSegmentIndex:.
Below the piece of code that did work for me. Repeated tap on the same segment will deselect it.
#implementation WUUnselectableSegmentedControl
{
NSUInteger _selectedIndexBeforeTouches;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
_selectedIndexBeforeTouches = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (_selectedIndexBeforeTouches == self.selectedSegmentIndex)
{
// Selection didn't change after touch - deselect
self.selectedSegmentIndex = UISegmentedControlNoSegment;
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
#end
iOS 9 solution. Override UISegmentedControl with such class:
#interface SegmentedControl()
#property (nonatomic) NSInteger previousCurrent;
#end
#implementation SegmentedControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.previousCurrent = self.selectedSegmentIndex;
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if (self.selectedSegmentIndex == self.previousCurrent) {
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
self.previousCurrent = NSNotFound;
}
#end
Seems like a fun question to answer. This scheme expands upon Steve E's and yershuachu's solutions. This version uses a UITapGestureRecognizer to capture all touches and which sets the selectedSegmentIndex to -1; but it also passes on all touches to the UISegmentedControl so it can handle any normal touches. No subclassing is required.
UISegmentedControl *mySegControl;
- (void)viewDidLoad
{
[super viewDidLoad];
[mySegControl addTarget:self action:#selector(segmentAction:)
forControlEvents:UIControlEventValueChanged];
// allow the a seg button tap to be seen even if already selected
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(unitsSegTap:)];
tapGesture.cancelsTouchesInView = NO; // pass touches through for normal taps
[mySegControl addGestureRecognizer:tapGesture];
// continue setup ...
}
- (void)segTap:(UITapGestureRecognizer *)sender
{
mySegControl.selectedSegmentIndex = -1;
}
// called for UIControlEventValueChanged
- (void)segmentAction:(UISegmentedControl *)sender
{
NSInteger index = sender.selectedSegmentIndex;
NSLog(#"unitsSegmentAction %d",(int)index);
// process the segment
}
I'm using KVO to invert already selected segment for iOS8.
#import "QCSegmentedControl.h"
static void *QCSegmentedControlContext = &QCSegmentedControlContext;
#implementation QCSegmentedControl
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self registerKVO];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"selectedSegmentIndex"];
}
#pragma mark -
- (void)registerKVO {
[self addObserver:self
forKeyPath:#"selectedSegmentIndex"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:QCSegmentedControlContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == QCSegmentedControlContext) {
NSNumber *new = change[NSKeyValueChangeNewKey];
NSNumber *old = change[NSKeyValueChangeOldKey];
if (new.integerValue == old.integerValue) {
self.selectedSegmentIndex = UISegmentedControlNoSegment;
}
}
}
#end
The selected answer's solution did not work for me as of current iOS v8.x. But #Andy offer a solution and I will complete:
Subclass the UISegmentedControl, and overwrite touchesEnded method in the subclass:
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self sendActionsForControlEvents:UIControlEventAllTouchEvents];
[super touchesEnded:touches withEvent:event];
}
In the class where you intend to use the UISegmentedControl, create an iVar currentSelectedSegIndex. Add the UIControlEventAllTouchEvents beside your UIControlEventValueChanged action methods:
NSInteger currentSelectedSegIndex;
[aSegmentedController addTarget:self action:#selector(aSegmentedControllerSelection:) forControlEvents:UIControlEventValueChanged];
[aSegmentedController addTarget:self action:#selector(aSegmentedControllerAllTouchEvent:) forControlEvents:UIControlEventAllTouchEvents];
Implement the two action methods:
-(void)aSegmentedControllerAllTouchEvent:(MyUISegmentedControl *)seg
{
seg.selectedSegmentIndex = UISegmentedControlNoSegment;
}
-(void)aSegmentedControllerSelection:(MyUISegmentedControl *)seg
{
if (currentSelectedSegIndex == seg.selectedSegmentIndex)
{
seg.selectedSegmentIndex = UISegmentedControlNoSegment;
currentSelectedSegIndex = NSIntegerMax;
NSLog(#"%s Deselected.", __PRETTY_FUNCTION__);
// do your stuff if deselected
return;
}
NSLog(#"%s Selected index:%u", __PRETTY_FUNCTION__, seg.selectedSegmentIndex);
currentSelectedSegIndex = seg.selectedSegmentIndex;
//do your stuff if selected index
}
Because the UISegmentedControl is in charge of setting the segment, it should only reset the state. I modify a little the suggestions of other guys for swift 5:
//MARK: Custom segmentedControl
/// Every element works like a flipflop [see](http://stackoverflow.com/questions/17652773/how-to-deselect-a-segment-in-segmented-control-button-permanently-till-its-click)
#IBDesignable class GRSegmentedControl: UISegmentedControl {
private let url = Bundle.main.url(forResource: "PlopChoiceCntrl", withExtension: "aiff")!
private var soundID:SystemSoundID = 0
override init(items: [Any]?) {
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
super.init(items: items)
}
required init?(coder: NSCoder) {
AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
super.init(coder: coder)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
AudioServicesPlaySystemSound(soundID)
let previousSelectedSegmentIndex = self.selectedSegmentIndex
super.touchesEnded(touches, with: event)
if previousSelectedSegmentIndex == self.selectedSegmentIndex {
if let touch = touches.first{
let touchLocation = touch.location(in: self)
if bounds.contains(touchLocation) {
self.selectedSegmentIndex = UISegmentedControl.noSegment
}
}
}
}
}