UISegmentedControl register taps on selected segment

IphoneUisegmentedcontrol

Iphone Problem Overview


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!

Iphone Solutions


Solution 1 - Iphone

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]);
    }
}

Solution 2 - Iphone

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];
}

Solution 3 - Iphone

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

Solution 4 - Iphone

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];
}

Solution 5 - Iphone

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!

Solution 6 - Iphone

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

Solution 7 - Iphone

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.

Solution 8 - Iphone

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][1] [1]: https://stackoverflow.com/questions/30237667/detect-event-when-tapped-on-already-selected-segment/32082881

Solution 9 - Iphone

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];
    }
}

Solution 10 - Iphone

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)")
    }
}

Solution 11 - Iphone

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.

Solution 12 - Iphone

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.

Solution 13 - Iphone

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

Solution 14 - Iphone

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:.

Solution 15 - Iphone

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

Solution 16 - Iphone

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

Solution 17 - Iphone

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
}

Solution 18 - Iphone

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

Solution 19 - Iphone

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
}

Solution 20 - Iphone

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPieter JongsmaView Question on Stackoverflow
Solution 1 - IphoneJulianView Answer on Stackoverflow
Solution 2 - IphonedavidavView Answer on Stackoverflow
Solution 3 - IphoneHenrik NView Answer on Stackoverflow
Solution 4 - IphoneEricSView Answer on Stackoverflow
Solution 5 - IphoneBob de GraafView Answer on Stackoverflow
Solution 6 - IphoneSteve EView Answer on Stackoverflow
Solution 7 - IphoneyershuachuView Answer on Stackoverflow
Solution 8 - IphoneIvanPavliukView Answer on Stackoverflow
Solution 9 - IphoneJason ShehaneView Answer on Stackoverflow
Solution 10 - IphonehhammView Answer on Stackoverflow
Solution 11 - IphoneRichard StellingView Answer on Stackoverflow
Solution 12 - IphoneDavid HView Answer on Stackoverflow
Solution 13 - IphoneClement MView Answer on Stackoverflow
Solution 14 - IphoneAndyView Answer on Stackoverflow
Solution 15 - IphonekelinView Answer on Stackoverflow
Solution 16 - IphoneGleb TarasovView Answer on Stackoverflow
Solution 17 - IphoneJeffView Answer on Stackoverflow
Solution 18 - IphoneBogdanView Answer on Stackoverflow
Solution 19 - Iphoneuser523234View Answer on Stackoverflow
Solution 20 - IphonevvolfView Answer on Stackoverflow