UISegmentedControl register taps on selected segment

Viewed 38392

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!

20 Answers

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

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
}

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