Since the UISegmentedControl only sends an action, if an unselected segment is selected, you must subclass the UISegmentedControl to make minor changes to its touch processing. I am using this class:
@implementation MBSegmentedControl // this sends a value changed event even if we reselect the currently selected segment - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSInteger current = self.selectedSegmentIndex; [super touchesBegan:touches withEvent:event]; if (current == self.selectedSegmentIndex) { [self sendActionsForControlEvents:UIControlEventValueChanged]; } } @end
You will now receive UIControlEventValueChanged events, even if a segment is already selected. Just store the current index in a variable and compare it in action. If both indices match, you must cancel the selected segment.
// _selectedSegmentIndex is an instance variable of the view controller - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; _selectedSegmentIndex = self.segment.selectedSegmentIndex; } - (IBAction)segmentChanged:(UISegmentedControl *)sender { if (sender.selectedSegmentIndex == _selectedSegmentIndex) { NSLog(@"Segment %d deselected", sender.selectedSegmentIndex); sender.selectedSegmentIndex = UISegmentedControlNoSegment; _selectedSegmentIndex = UISegmentedControlNoSegment; } else { NSLog(@"Segment %d selected", sender.selectedSegmentIndex); _selectedSegmentIndex = sender.selectedSegmentIndex; } }
iOS 7 has changed how strokes are processed for UISegmentedControl. The selected SegmentIndex parameter now changes during touchesEnded:
So, the updated subclass should look like this:
@implementation MBSegmentedControl + (BOOL)isIOS7 { static BOOL isIOS7 = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSInteger deviceSystemMajorVersion = [[[[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:@"."] objectAtIndex:0] integerValue]; if (deviceSystemMajorVersion >= 7) { isIOS7 = YES; } else { isIOS7 = NO; } }); return isIOS7; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSInteger previousSelectedSegmentIndex = self.selectedSegmentIndex; [super touchesBegan:touches withEvent:event]; if (![[self class] isIOS7]) {
Swift version 2.2, fixed the problem that Grzegorz noticed.
class ReselectableSegmentedControl: UISegmentedControl { @IBInspectable var allowReselection: Bool = true override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { let previousSelectedSegmentIndex = self.selectedSegmentIndex super.touchesEnded(touches, withEvent: event) if allowReselection && previousSelectedSegmentIndex == self.selectedSegmentIndex { if let touch = touches.first { let touchLocation = touch.locationInView(self) if CGRectContainsPoint(bounds, touchLocation) { self.sendActionsForControlEvents(.ValueChanged) } } } } }
Swift 3.0 modifies the fix for this to look like this:
class MyDeselectableSegmentedControl: UISegmentedControl { override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { let previousIndex = selectedSegmentIndex super.touchesEnded(touches, with: event) if previousIndex == selectedSegmentIndex { let touchLocation = touches.first!.location(in: self) if bounds.contains(touchLocation) { sendActions(for: .valueChanged) } } } }