Search code examples
iosobjective-cuibuttonuisegmentedcontrol

How to deselect a segment in Segmented control button permanently till its clicked again


I have a UISegmentedControl with 4 segments. When it is selected, it should maintain the selected state. When the same segment is clicked again, it should deselect itself. How to achieve this?


Solution

  • Since UISegmentedControl only sends an action if a not selected segment is selected, you have to subclass UISegmentedControl to make a tiny change in its touch handling. I use 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
    

    Now you will get UIControlEventValueChanged events even if the segment is already selected. Simply save the current index in a variable and compare it in the action. If the two indexes match you have to unselect the touched 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 changed how touches are handled for UISegmentedControl. The selectedSegmentIndex is now changed 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]) {
            // before iOS7 the segment is selected in touchesBegan
            if (previousSelectedSegmentIndex == self.selectedSegmentIndex) {
                // if the selectedSegmentIndex before the selection process is equal to the selectedSegmentIndex
                // after the selection process the superclass won't send a UIControlEventValueChanged event.
                // So we have to do this ourselves.
                [self sendActionsForControlEvents:UIControlEventValueChanged];
            }
        }
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        NSInteger previousSelectedSegmentIndex = self.selectedSegmentIndex;
        [super touchesEnded:touches withEvent:event];
        if ([[self class] isIOS7]) {
            // on iOS7 the segment is selected in touchesEnded
            if (previousSelectedSegmentIndex == self.selectedSegmentIndex) {
                [self sendActionsForControlEvents:UIControlEventValueChanged];
            }
        }
    }
    
    @end
    

    Swift 2.2 version, fixed the problem 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 changes the fix for this to look like the following:

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