How do I set the accessibility label for a particular segment of a UISegmentedControl?
Asked Answered
B

14

22

We use KIF for our functional testing, and it uses the accessibility label of elements to determine where to send events. I'm currently trying to test the behaviour of a UISegmentedControl, but in order to do so I need to set different accessibility labels for the different segments of the control. How do I set the accessibility label for a particular segment?

Bounder answered 12/1, 2012 at 11:1 Comment(0)
H
15

I'm just getting started with KIF myself, so I haven't tested this, but it may be worth a try. I'm sure I'll have the same issue soon, so I'd be interested to hear if it works.

First, UIAccessibility Protocol Reference has a note under accessibilityLabel that says:

"If you supply UIImage objects to display in a UISegmentedControl, you can set this property on each image to ensure that the segments are properly accessible."

So, I'm wondering if you could set the accessibilityLabel on each NSString object as well and be able to use that to access each segment with KIF. As a start, you could try creating a couple of strings, setting their accessibility labels, and using [[UISegmentedControl alloc] initWithItems:myStringArray]; to populate it.

Please update us on your progress. I'd like to hear how this goes

Hopper answered 12/1, 2012 at 21:38 Comment(5)
Nope, this does not work. You have to use the accepted answer to customize these programmatically, and it is ugly.Abrego
This works for me, at least as of iOS 5.1. I haven't tested further back. If I set an image's accessibilityLabel and then use the image in a UISegmentedControl, it works as expected.Tullus
Setting accessibilityLabel on NSString objects... mind = blownFrisbee
developer.apple.com/library/ios/documentation/UserExperience/…Undersell
For example: fooIcon.accessibilityLabel = “Foo” barIcon.accessibilityLabel = “Bar” booIcon.accessibilityLabel = “Boo” segmentedControl.setImage(fooIcon, forSegmentAt: 0) segmentedControl.setImage(barIcon, forSegmentAt: 1) segmentedControl.setImage(booIcon, forSegmentAt: 2)Sasha
V
20

As Vertex said,

obj-c

[[[self.segmentOutlet subviews] objectAtIndex:3] setAccessibilityLabel:@"GENERAL_SEGMENT"];

swift

self.segmentOutlet.subviews[3].accessibilityLabel = "GENERAL_SEGMENT"

some advice so you don't go crazy like I did:

  1. To scroll in accessibility mode swipe three fingers
  2. The indexes of the segments are backwards than you would expect, i.e. the furthest segment to the right is the 0th index and the furthest to the left is the n'th index where n is the number of elements in the UISegmentControl
Vincentvincenta answered 29/3, 2013 at 21:7 Comment(5)
How is this possible in Swift?Derry
I added a swift example. I haven't tried it but the compiler accepted the syntax. Sorry for the late reply @DerryVincentvincenta
Doesn't work for me... setting the accessibilityLabel on the UISegment and right after assignment it's nil againMong
I set the accessibilityIdentifier to the index of the title, but failed: "No matches found for Find: Elements matching predicate '"0" IN identifiers' from input". Then I set accessibilityLabel, and it works.Corby
FAIL: I tested the Swift version of this code. My segmented control only has images. While the code sets the accessibilityLabel, the assigned segment seems random. One run the accessibilityLabel and image are matched. The next run they are not. Use Jmac’s advice and set the accessibilityLabel on the UIImage.Sasha
H
15

I'm just getting started with KIF myself, so I haven't tested this, but it may be worth a try. I'm sure I'll have the same issue soon, so I'd be interested to hear if it works.

First, UIAccessibility Protocol Reference has a note under accessibilityLabel that says:

"If you supply UIImage objects to display in a UISegmentedControl, you can set this property on each image to ensure that the segments are properly accessible."

So, I'm wondering if you could set the accessibilityLabel on each NSString object as well and be able to use that to access each segment with KIF. As a start, you could try creating a couple of strings, setting their accessibility labels, and using [[UISegmentedControl alloc] initWithItems:myStringArray]; to populate it.

Please update us on your progress. I'd like to hear how this goes

Hopper answered 12/1, 2012 at 21:38 Comment(5)
Nope, this does not work. You have to use the accepted answer to customize these programmatically, and it is ugly.Abrego
This works for me, at least as of iOS 5.1. I haven't tested further back. If I set an image's accessibilityLabel and then use the image in a UISegmentedControl, it works as expected.Tullus
Setting accessibilityLabel on NSString objects... mind = blownFrisbee
developer.apple.com/library/ios/documentation/UserExperience/…Undersell
For example: fooIcon.accessibilityLabel = “Foo” barIcon.accessibilityLabel = “Bar” booIcon.accessibilityLabel = “Boo” segmentedControl.setImage(fooIcon, forSegmentAt: 0) segmentedControl.setImage(barIcon, forSegmentAt: 1) segmentedControl.setImage(booIcon, forSegmentAt: 2)Sasha
R
10

You can't rely on the index in the subviewsarray for the position. For customisation of the individual subviews I sort the subviews on their X Position before setting any propery.What would also be valid for accesibilityLbel.

let sortedViews = self.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )

sortedViews[0].accessibilityLabel = "segment_full"
sortedViews[1].accessibilityLabel = "segment_not_full"
Reprehensible answered 20/9, 2018 at 8:59 Comment(2)
This. THIS. Thank you! This got it working for me! I can't believe that Apple hasn't made it easy to add strings to segments. The main segment does not play (I added strings there). Only placeholders for each segment (the title of the image).Kreitman
This didn't quite work for me in XCode 12 and Swift 5, BUT it was a necessary component of the code that's worked for me so far.Lucianaluciano
C
9

Each segment of UISegmentedControl is UISegment class instance which subclass from UIImageView. You can access those instances by subviews property of UISegmentedControl and try to add accessibility for them programmatically.

Cleopatra answered 12/1, 2012 at 21:43 Comment(2)
I tried it, it did not work for me. I tried another way, it works for me now. [[segmentedControl imageForSegmentAtIndex:0] setAccessibilityLabel:@"Weapons"];Bothersome
For some reason, the subviews property is returning too many values for me. It's returning one UISegment AND one UIImageView for each segment. Any thoughts on why that could be?Tollhouse
T
8

This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text. So if two options were added of Option 1 and Option 2. A call to

[tester tapViewWithAccessibilityLabel:@"Option 2"];

successfully selected the segment.

Telltale answered 26/6, 2014 at 13:2 Comment(3)
It don't work and I checked with the Accessibility Inspector and there's no label on themTurbulence
OMG this should be the accepted answer, so simple! Works for me on iOS 8.Gradation
Works, though I have a problem: one of my tabs has the exact same text as a segment. Consequently they both have the same accessibility label.Icj
D
7

The solutions with using an indexed subview is not working since you cannot rely on a correct order and it will be difficult to change the number of segments. And sorting by origin does not work, since the frame (at least for current versions) seems to be always at x: 0.

My solution:

(segmentedControl.accessibilityElement(at: 0) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 1"
(segmentedControl.accessibilityElement(at: 1) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 2"
(segmentedControl.accessibilityElement(at: 2) as? UIView)?.accessibilityLabel = "Custom VoiceOver Label 3"

Seems to work for me and has the correct order. You also do not rely on an image. Not that pretty either but maybe more reliable than other solutions.

Devitrify answered 24/1, 2020 at 14:29 Comment(2)
From what I'm seeing In XCode 12 / iOS 14 a year after your reply, the frame origin is x = 0 in viewDidLoad() and viewWillAppear(), but not in viewDidAppear(). But then in viewDidAppear() the UISegmentedControl subviews include UIImageViews. At least from what I'm seeing in my app. Making changes in viewDidAppear() seems to be "late," but that's the only function where the frames are set as one would expect.Lucianaluciano
Using iOS15 currently. This seems to be the best. Just would use (segmentedControl.accessibilityElement(at: 0) as? UIAccessibilityElement) insteadMoulden
N
4

This is an old question but just in case anyone else runs up against this I found that the segments automatically had an accessibility label specified as their text.

Further to Stuart's answer, I found it really useful when writing test cases to turn on 'Accessibility Inspector' on the Simulator (Settings -> General -> Accessibility -> Accessibility Inspector). You'd be surprised how many elements already have accessibility labels included, like in the standard iOS UI elements or even third party frameworks.

Facebook log in button with accessibility label

Note: Gestures will now be different - Tap to view accessibility information, double tap to select. Minimizing the Accessibility Inspector window (by tapping the X button) will return the gestures back to normal.

Nettles answered 23/9, 2014 at 3:52 Comment(0)
K
2

You guys want to see how Apple recommends it be done?

It's FUGLY.

This is from this example:

func configureCustomSegmentsSegmentedControl() {
    let imageToAccessibilityLabelMappings = [
        "checkmark_icon": NSLocalizedString("Done", comment: ""),
        "search_icon": NSLocalizedString("Search", comment: ""),
        "tools_icon": NSLocalizedString("Settings", comment: "")
    ]

    // Guarantee that the segments show up in the same order.
    var sortedSegmentImageNames = Array(imageToAccessibilityLabelMappings.keys)
    sortedSegmentImageNames.sort { lhs, rhs in
        return lhs.localizedStandardCompare(rhs) == ComparisonResult.orderedAscending
    }

    for (idx, segmentImageName) in sortedSegmentImageNames.enumerated() {
        let image = UIImage(named: segmentImageName)!

        image.accessibilityLabel = imageToAccessibilityLabelMappings[segmentImageName]

        customSegmentsSegmentedControl.setImage(image, forSegmentAt: idx)
    }

    customSegmentsSegmentedControl.selectedSegmentIndex = 0

    customSegmentsSegmentedControl.addTarget(self,
                                             action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)),
                                             for: .valueChanged)
}

They apply the accessibility labels to images, and then attach the images. Not too different from the above answer.

Kreitman answered 19/12, 2018 at 1:15 Comment(1)
See my solution, which also involves a sort, but a much easier sort, which takes advantage of bringSubviewToFront doing the sorting for you.Fillagree
L
1

another option if not willing to set accesibility label might be calculating the poistion of each segment part and use

[tester tapScreenAtPoint:segementPosition];

to trigger the actions

Lacedaemon answered 23/7, 2014 at 4:46 Comment(0)
B
1

If you look at the segmented control thru the accessibility inspector, you find that the segments are UISegment objects. Moreover, they turn out to be direct subviews of the UISegmentedControl. That fact suggests the following insanely crazy but perfectly safe Swift 4 code to set the accessibility labels of the segments of a UISegmentedControl:

    let seg = // the UISegmentedControl
    if let segclass = NSClassFromString("UISegment") {
        let segments = seg.subviews.filter {type(of:$0) == segclass}
            .sorted {$0.frame.minX < $1.frame.minX}
        let labels = ["Previous article", "Next article"] // or whatever
        for pair in zip(segments,labels) {
            pair.0.accessibilityLabel = pair.1
        }
    }
Bogtrotter answered 4/10, 2018 at 15:9 Comment(1)
But if you access a private class, which UISegment appears to be, isn't there a chance that you app will be rejected from the App Store? Or would using NSClassFromString not trigger any alarm bells during app submission? I tried something similar, and less elegant.Lucianaluciano
A
1

As mentioned in the accepted answer, adding accessibilityLabel to the text should do the trick:

let title0 = "Button1" as NSString
title0.accessibilityLabel = "MyButtonIdentifier1"
segmentedControl.setTitle("\(title0)", forSegmentAt: 0)
            
let title1 ="Button2" as NSString
title1.accessibilityLabel = "MyButtonIdentifier2"
segmentedControl.setTitle("\(title1)", forSegmentAt: 1)
Anzovin answered 2/2, 2021 at 11:48 Comment(0)
L
0

XCode 12 / iOS 14.3 / Swift 5

This is an old post but I encountered the same problem trying to set accessibility hints for individual segments in a UISegmentedControl. I also had problems with some of the older solutions. The code that's currently working for my app borrows from replies such as those from matt and Ilker Baltaci and then mixes in my own hack using UIView.description.

First, some comments:

  • For my UISegmentedControl with 3 segments, the subview count is 3 in the viewDidLoad() and viewWillAppear() of the parent UIVIewController. But the subview count is 7 in viewDidAppear().
  • In viewDidLoad() or viewWillAppear() the subview frames aren't set, so ordering the subviews didn't work for me. Apparently Benjamin B encountered the same problem with frame origins.
  • In viewDidAppear(), the 7 subviews include 4 views of type UIImageView and 3 views of type UISegment.
  • UISegment is a private type. Working directly with the private API might flag your app for rejection. (see comment below)
  • type(of:) didn't yield anything useful for the UISegment subviews
  • (HACK!) UIView.description can be used to check the type without accessing the private API.
  • Setting accessibility hints based on X order tightly couples UI segment titles and hints to their current positions. If user testing suggests a change in segment order, then changes must be made both in the UI and in the code to set accessibility hints. It's easy to miss that.

Using an enum to set segment titles is an alternative to relying on X ordering set manually in the UI. If your enum inherits from String and adopts the protocols CaseIterable and RawRepresentable, then it's straightforward to create titles from the enum cases, and to determine the enum case from a segment title.

There's no guarantee the following will work in a future release of the framework, given the reliance on description.contains("UISegment") but it's working for me. Gotta move on.

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

// get only the UISegment items; ignore UIImageView
let segments = mySegmentedControl.subviews.compactMap(
    { $0.description.contains("UISegment") ? $0 : nil }
)
let sortedSegments = segments.sorted(
    by: { $0.frame.origin.x < $1.frame.origin.x }
)

for i in 0 ..< sortedSegments.count {
    let segment = sortedSegments[i]
    // set .accessibilityHint or .accessibilityLabel by index
    // or check for a segment title matching an enum case
    // ...
}
}

On Private APIs and Rejection

I'm referring to the April 2016 comment from @dan in Test if object is an instance of class UISegment:

It's a private class. You can check it with [... isKindOfClass:NSClassFromString(@"UISegment")] but that may get your app rejected for using private api or stop working in the future if apple changes the internal class name or structure.

Also: What exactly is a Private API, and why will Apple reject an iOS App if one is used?

"App rejected due to non-public api's": https://discussions.apple.com/thread/3838251

Lucianaluciano answered 24/2, 2021 at 15:41 Comment(0)
F
0

My solution is actually quite simple (shown assuming you want the accessibilityIdentifier to match the title). Just make the indices of the subviews match those of the segments:

In Objective-C:

//Sort the subviews to match the segments.

for (int i=0; i<control.numberOfSegments; i++) {
    UIView *v = [[control subviews] objectAtIndex:i];
    [v.superview bringSubviewToFront:v];
}

// Now set the accessibilityIdentifiers.

for (int i=0; i<control.numberOfSegments; i++) {
    NSString *s = [[control titleForSegmentAtIndex:i];
    [[[control subviews] objectAtIndex:i] setAccessibilityIdentifier:s];
}

In Swift

//Sort the subviews to match the segments.

    for v in control.subviews {
        v.superview?.bringSubviewToFront(v)
    }

// Now set the accessibilityIdentifiers.

    for i in 0..<control.numberOfSegments {
        let s = control.titleForSegment(at: i)
        control.subviews[i].accessibilityIdentifier = s
    }
Fillagree answered 8/5 at 23:55 Comment(0)
A
-1

As Vortex said, the array is right to left with [0] starting on the right. You can set every single accessibility option by accessing the subviews. Since the subviews are optional, it is good to pull out the subview first, and then assign the accessibility traits that you want. Swift 4 example for a simple two option segment control:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    guard let rightSegment = segmentControl.subviews.first, let leftSegment = segmentControl.subviews.last else { return }
    rightSegment.accessibilityLabel = "A label for the right segment."
    rightSegment.accessibilityHint = "A hint for the right segment."
    leftSegment.accessibilityLabel = "A label for the left segment."
    leftSegment.accessibilityHint = "A hint for the left segment."
}
Alfaro answered 23/8, 2018 at 12:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.