4

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

Brian
  • 14,610
  • 7
  • 35
  • 43
TruMan1
  • 33,665
  • 59
  • 184
  • 335
  • What are you trying to accomplish? A segmented control is for toggling between different options, hence why you only get a callback when the selected option changes. – duncanc4 Jun 14 '15 at 21:24
  • My segmented control is: All and Unread. When the user taps on Unread it displays unread records, but I want it so when they tap Unread again, it will pop up an alert asking if they are sure if they want to mark all as Unread. I don't want to clutter my app by adding an extra button in the navigation bar just to mark all as read since there are other buttons in the nav bar already. – TruMan1 Jun 14 '15 at 22:24
  • I think you might be able to use the `momentary` property to accomplish the functionality you are looking for. https://developer.apple.com/library/ios/documentation/UIKit/Reference/UISegmentedControl_Class/index.html#//apple_ref/occ/instp/UISegmentedControl/momentary – duncanc4 Jun 14 '15 at 22:46
  • It seems this will drastically change the behavior of the segmented control since selected index is no longer being tracked which will break existing functionality. I was hoping to get the subview that stores the button for each segment and attach an action to it. – TruMan1 Jun 14 '15 at 23:36
  • Yeah, I looked through the documentation as well, you can get at them on OS X, but it doesn't look like it on iOS. – duncanc4 Jun 14 '15 at 23:37
  • 4
    sorry to say but your design need to change. Segment control should never have such action. If you need more action in your app do add more buttons for it , there is no harm in it, but just don't try to change the default behavior in such way. – Bishal Ghimire Jun 20 '15 at 15:43

10 Answers10

7

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
}
Callum Boddy
  • 407
  • 2
  • 5
5

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
}
Thomas Besnehard
  • 2,106
  • 3
  • 25
  • 47
3

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
LLIAJLbHOu
  • 1,313
  • 12
  • 17
2

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.

K Bakalov
  • 254
  • 1
  • 7
2

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];
}
Sgorbyo
  • 43
  • 7
1

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...
            }
        }
}
Drmorgan
  • 529
  • 4
  • 10
  • I didn't actually use your code, but the [sender selectedSegmentIndex] bit gave me the clue I had been looking for for days! Thank you!! – user3079872 Mar 22 '16 at 22:44
1

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
}
Sivda
  • 101
  • 6
0

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.

BevTheDev
  • 563
  • 2
  • 9
  • I don't have a place to put a "Mark all read" button in the navigation header. The left and right side buttons are occupied with other functionality (menu slider and search). So I was trying to save screen real estate and get creative with the with the existing space. – TruMan1 Jun 17 '15 at 16:47
  • If space is an issue, consider using two buttons instead of the segment control, i.e. 'button1', 'button2'. When button2 is tapped, change it's color and text to make it clear that it serves another purpose now, and you will be able to pick up the second tap while also letting your user see the options they have available. – BevTheDev Jun 17 '15 at 16:53
0

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];
AdminXVII
  • 1,319
  • 11
  • 22
0

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.

lifjoy
  • 2,158
  • 21
  • 19