Handle tap on same segmented button?

5k views Asked by At

I'm trying to handle a tap event on a segmented control, but when the selected button is clicked again. For example, for the screenshot below where "Second" is already selected, how do I handle the action when the "Second" button is clicked again?

I tried an IBOutlet, but it only triggers when the value has changed. Then I tried the code below, but the same thing where it triggers only when the value changes. In both cases while "Second" is selected, clicking "Second" again does not fire anything. Is there a way to do this?

segmentedControl.addTarget(self, action: "segementedAnyTap:", forControlEvents: .AllEvents)

enter image description here

10

There are 10 answers

2
Callum Boddy On BEST ANSWER

This works for me, adding a gesture recogniser to the UISegmentedControl

- (void)viewDidLoad
{
  [super viewDidLoad];

  [self.segmentedControl addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged];
  UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(touched:)];
  [self.segmentedControl addGestureRecognizer:tapGesture];
}

- (void)valueChanged:(id)sender
{
  // value change code
}

- (void)touched:(id)sender
{
  // code to check if the segmented controls index has not changed.
  // execute desired functionality
}
0
K Bakalov On

I'm not quite sure why are you trying to achieve this but I'd like to suggest subclassing UISegmentedControl and overriding touchesEnded:withEvent:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];

    [self sendActionsForControlEvents:UIControlEventTouchUpInside];
}

Now your scheduled selector for UIControlEventTouchUpInside will get called every time you press each of the segments and still keep the default functionality of UISegmentedControl.

NOTE: You'll need to handle yourself if that's the first selection of the segment (e.g. keep a private property of the previous value). If you add selector for UIControlEventValueChanged it will also trigger the selector for UIControlEventTouchUpInside and it might cause a bit of confusion or bugs.

Good luck and hope that helps.

2
BevTheDev On

In your comment on your question, you mentioned you're trying to allow users to select 'unread' to display all unread messages, then let them click again to mark all as unread. Instead of using the segment control for that, I'd recommend adding a "Mark all unread" button that appears when the 'unread' segment is selected. That will accomplish the feature you're trying to add while also making it clear to the user that they have a way to mark everything as unread.

0
AdminXVII On

You could set your app to display a popover coming from your unread segment with a button to display all as unread. To place the popover, use :

CGRect frame = [segmentControl frame];
frame =CGRectMake((frame.size.width/2*butIndex), 0, frame.size.width/2, segmentControl.bounds.size.height);

[popOver presentPopoverFromRect:frame inView:segmentControl permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
0
Sgorbyo On

I think this could solve the problem:

- (void)viewDidLoad {
    [super viewDidLoad];

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(touched:)];
    [self.segmentedControl addGestureRecognizer:tapGesture];
}

- (void) valueChanged:(id) sender {
    // Your segment changed selector
}

- (void) touched:(UITapGestureRecognizer *) tapGesture {
    CGPoint point = [tapGesture locationInView:self.segmentedControl];
    NSUInteger segmentSize = self.segmentedControl.bounds.size.width / self.segmentedControl.numberOfSegments;
    // Warning: If you are using segments not equally sized, you have to adapt the code in the next line
    NSUInteger touchedSegment = point.x / segmentSize;
    if (self.segmentedControl.selectedSegmentIndex != touchedSegment) {
        // Normal behaviour the segment changes
        self.segmentedControl.selectedSegmentIndex = touchedSegment;
    } else {
        // Tap on the already selected segment, I'm switching to No segment selected just to show the effect
        self.segmentedControl.selectedSegmentIndex = UISegmentedControlNoSegment;
    }
    // You have to call your selector because the UIControlEventValueChanged can't work together with UITapGestureRecognizer
    [self valueChanged:self.segmentedControl];
}
0
LLIAJLbHOu On

Add KVO observing.

Exaple:

#pragma mark - 

- (void)viewDidLoad {
    [_segmentControl addObserver:self forKeyPath:@"selectedSegmentIndex" options:NSKeyValueObservingOptionInitial context:nil];
}


#pragma mark - KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"segment index: %ld", (long)_segmentControl.selectedSegmentIndex);
}

Result:

2015-06-22 12:31:54.155 Location test[27082:13176230] segment index: 0
2015-06-22 12:31:54.740 Location test[27082:13176230] segment index: 0
2015-06-22 12:31:55.821 Location test[27082:13176230] segment index: 1
2015-06-22 12:31:56.529 Location test[27082:13176230] segment index: 1
1
Drmorgan On

I do the following for a segmented control that has 3 options ("categories"). _selectedCategory is a property NSInteger that keeps track of segmented controls currently selected index. On tap, if I find that the _selectedCategory is the same as the one pressed, they're pressing the selected segmented control and I flip it off.

 -(IBAction)categorySelected:(id)sender {
        if (_selectedCategory == [sender selectedSegmentIndex]) {
            sender.selectedSegmentIndex = UISegmentedControlNoSegment;
            // update my model, etc...
        } else {
            _selectedCategory = [sender selectedSegmentIndex];
            switch (_selectedCategory) {
                case 0:
                // do logic...
            }
        }
}
0
lifjoy On

A little late I know, but another technique that worked well for me...

Add a UIButton with a clear background over each segment of the UISegmentedControl. Each UIButton can have its own UIControlEventTouchUpInside event handler-- which can change the UISegmentedControl's selectedSegmentIndex.

Then set the UISegmentedControl.userInteractionEnabled to NO, and remove its UIControlEventValueChanged event handler.

0
Sivda On

I encounter a case which I need the segment index before the selection, and I do not want the .valueChanged event stops firing, therefore I come up with this.

Create a subclass for UISegmentedControl and override touchesEnded

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

    // Previous selected segment index
    let oldIdx = selectedSegmentIndex

    super.touchesEnded(touches, with: event)

    // New selected segment index
    let newIdx = selectedSegmentIndex

    // If the previously selected segment index is equal to the new one, 
    // then you are tapping on the same segment button.
    // Call a block, delegate method or whatever to notify this
}
0
Thomas Besnehard On

Great Answer @Sgorbyo, here is a Swift 3 version of it:

override func viewDidLoad() {
    super.viewDidLoad()
    let segmentedTapGesture = UITapGestureRecognizer(target: self, action: #selector(onTapGestureSegment(_:)))
    segmentedControl.addGestureRecognizer(segmentedTapGesture)
}

@IBAction func onTapGestureSegment(_ tapGesture: UITapGestureRecognizer) {
    let point = tapGesture.location(in: segmentedControl)
    let segmentSize = tipSegmentedControl.bounds.size.width / CGFloat(segmentedControl.numberOfSegments)
    let touchedSegment = Int(point.x / segmentSize)

    if segmentedControl.selectedSegmentIndex != touchedSegment {
        // Normal behaviour the segment changes
        segmentedControl.selectedSegmentIndex = touchedSegment
    } else {
        // Tap on the already selected segment
        segmentedControl.selectedSegmentIndex = touchedSegment
    }
    onSegment(segmentedControl)
}

@IBAction func onSegment(_ sender: Any) {
// Your segment changed selector
}