UISegmentedControl register taps on selected segment
IphoneUisegmentedcontrolIphone 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
}
}
}
}
}