UIPageControl voiceover issue - ios

I have a UIPageControl in my app's onboarding process. Its purpose is not to change pages manually but as an indication of the user's process through the whole onboarding. (There's no swiping gestures right now)
Everything looks fine, but VoiceOver lets the user increment or decrement the control, and says it can be changed (it seems to keep .adjustable as a trait). I don't want that behaviour. I just want VoiceOver to read "Page 1 of 3". I disabled it, changed its accessibilityTraits and it doesn't affect VoiceOver.
Here is some code.
/// hard coded values for the example:
pageControl.numberOfPages = 3
pageControl.currentPage = 1
pageControl.isEnabled = false
pageControl.isUserInteractionEnabled = false
pageControl.accessibilityTraits = .none
I have created a test project on github for a more complete example.

One way to get your purpose is to subclass UIpageControl and override the accessibiliTraits property as follows:
class MyPageControl: UIPageControl {
override var accessibilityTraits: UIAccessibilityTraits {
get{
return .none
}
set{}
}
}
Define your pageControl element as MyPageControl to get the desired result.

Related

Show or hide items by clicking button

I have four imageview contents in an XIB and a button that covers all my XIB. I want to make when the user tap the button, the first imageview is shown, the next tap is hidden and the second imageview is displayed and so on until all my imageview is shown / hidden. What would be the most efficient way to do it?
Save all your UIImageViews to an array, and current showing imageView to a variable, it may look like this:
var imageViews: [UIImageView] = []
var currentImageViewIndex = 0 {
didSet {
if currentImageViewIndex >= imageViews.count { currentImageViewIndex = 0 }
imageViews[oldValue].isHidden = true
imageViews[currentImageViewIndex].isHidden = false
}
}
func handleTap() {
currentImageViewIndex += 1
}
I suggest you use a state variable that contains an enum listing the various states (firstImageVisible, secondImage.... ) then you can have a function inside the enum that switches to the nextState (being the target of your button action) you can also easily iterate through states of an enum, check the documentation for the CaseIterable protocol. Often having a property observer (didSet) on the state is a handy place to update other parts of the UI which need to change every time the state changes.

Setting UIView.isAccessibleElement to true disables clicking of subview in voiceovermode

So I have a custom view controller that displays a dialog with a couple of buttons.
When the view appears I want voiceover to read out some basic information describing the dialog.
To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
My problems now is that the buttons are not clickable directly.
They must be reached only by swiping right on the screen.
class MyViewController: UIViewController {
let parent = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parent.addSubview(button1)
parent.addSubview(button2)
parent.isAccessibilityElement = true
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
parent.accessibilityLabel = "Message"
self.view.addSubview(parent)
self.view.accessibilityElements = [parent, button1, button2]
}
override func viewDidAppear(_ animated: Bool) {
}
}
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Also, the view needs to be a modal so that focus is trapped on the view.
To achieve this I made the parent view to be an accessible element and the subviews which are two buttons are also accessible elements.
That's definitely the problem: you can't have the parent view and its children accessible all together ⟹ see the example sheet of this explanation.
If a parent view is accessible, its children won't be seen by VoiceOver and conversely.
If there is a better way to get voiceover to give description of the view when the opens, I am open to that too.
Using VoiceOver, you must be as accurate and brief as possible.
The description of a view is provided by its elements when you explore the screen or by its title itself : in my view, you shouldn't read out a description that a perfect title should provide in addition to the correct implementation of the different components of your page.
There's a great presentation made by a blind person who explains how to write labels inside an app to be well understood.
Also, the view needs to be a modal so that focus is trapped on the view.
The best way to reach this purpose is to use the accessibilityViewIsModal property of your view ⟹ take a look at this example introduced during a WWDC session if need be.
You can post a notification with a message as parameter, so you would not need to set the parent view as an accessibility element. This would solve both of your problems.
Example code:
let parentVc = UIView()
let button1 = UIButton()
let button2 = UIButton()
init() {
parentVc.addSubview(button1)
parentVc.addSubview(button2)
button1.setTitle("btn1", for: .normal)
button2.setTitle("btn2", for: .normal)
button1.isAccessibilityElement = true
button2.isAccessibilityElement = true
self.view.addSubview(parentVc)
self.view.accessibilityElements = [button1, button2]
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: "Message here");
}

The caret disappears or won't show up in UITextField

I'm having trouble with the caret of UITextfields.
Whenever I tap on the textfield the caret doesn't show, but otherwise the textfield is working properly. The problem is similar to the problem described here (already tried the solution written there as well).
I noticed that whenever I load a viewcontroller and tap on a textfield (or programatically make it the first responder) it does show a caret, but as soon as I tap another textfield or make a textfield resign its first responder status no textfield will show its caret anymore (unless I go back and reload the viewcontroller again).
Got no clue as of why this is happening. I am using a custom font throughout the app. But I'm not sure if that's what making the caret disappear, as I've used custom fonts before with no problem whatsoever.
EDIT:
I think I've at least found why the problem occurs. It is related to me overriding the becomeFirstResponder and resignFirstResponder.
My code looks like:
extension UITextField {
open override func becomeFirstResponder() -> Bool {
let willBecomeResponder = super.becomeFirstResponder()
if willBecomeResponder {
backgroundColor = .red
layer.borderColor = .blue
}
return willBecomeResponder
}
open override func resignFirstResponder() -> Bool {
let willResignResponder = super.resignFirstResponder()
if willResignResponder {
backgroundColor = .blue
layer.borderColor = .red
}
return willResignResponder
}
}
Overriding those methods in the extension makes the caret disappear.
I'm thinking this most likely happens because UITextField itself does some...'caret-management' in it's own implementation of those methods.
More so because even return super.becomeFirstResponder() and no custom code in the overridden method makes the caret disappear.
My question therefore is; how can one solve this problem without making a custom UITextField subclass?
Calling super obviously just calls the UIResponder's implementation, but the docs specifically mention:
becomeFirstResponder()
You can override this method in your custom responders to update your object's state or perform some action such as highlighting the selection. If you override this method, you must call super at some point in your implementation.
So I need to call super I guess.
Alright I solved my problem by overriding isFirstResponder and altering the textfield based on the super.isFirstResponder there.
Looks like:
open override var isFirstResponder: Bool {
get {
let responder = super.isFirstResponder
backgroundColor = (responder) ? .red : .blue
layer.borderColor = (responder) ? .blue : .red
return responder
}
}
I did noticed a lot of calls being sent to isFirstResponder, so maybe this isn't the most efficient way (if altering the textfield is a heavy op).

What's the simplest way to receive tap events on a disabled UIButton?

I have a UIButton on a form, and want to put it in a disabled state when the form is incomplete. However, I still want to be able to detect if a user attempts to press the button even in its disabled state so that the interface can let the user know that certain required fields on the form are not filled-in yet (and perhaps scroll to that field and point it out, etc.).
There doesn't seem to be any straightforward way to do this. I tried simply attaching a UITapGestureRecognizer to the UIButton but it doesn't respond when the button is in a disabled state.
I'd like to avoid subclassing UIButton if possible, unless it's the only way.
Create a fallback button. Put it behind the main button. Set its background and text colors to [UIColor clearColor] to ensure it won't show up. (You can't just set its alpha to 0 because that makes it ignore touches.) In Interface Builder, the fallback button should be above the main button in the list of subviews, like this:
Give it the same frame as the main button. If you're using autolayout, select both the main and fallback buttons and create constraints to keep all four edges equal.
When the main button is disabled, touches will pass through to the fallback button. When the main button is enabled, it will catch all the touches and the fallback button won't receive any.
Connect the fallback button to an action so you can detect when it's tapped.
Based on #rob idea, I sub-class a UIButton, and add a transparent button before someone addSubview on this button.
This custom UIButton will save many time about adjusting the UI components on the storyboard.
Update 2018/08
It works well, and add some enhanced detail to this sub-class. I have used it for 2 years.
class TTButton : UIButton {
// MARK: -
private lazy var fakeButton : UIButton! = self.initFakeButton()
private func initFakeButton() -> UIButton {
let btn = UIButton(frame: self.frame)
btn.backgroundColor = UIColor.clearColor()
btn.addTarget(self, action: #selector(self.handleDisabledTouchEvent), forControlEvents: UIControlEvents.TouchUpInside)
return btn
}
// Respect this property for `fakeButton` and `self` buttons
override var isUserInteractionEnabled: Bool {
didSet {
self.fakeButton.isUserInteractionEnabled = isUserInteractionEnabled
}
}
override func layoutSubviews() {
super.layoutSubviews()
// NOTE: `fakeButton` and `self` has the same `superView`.
self.fakeButton.frame = self.frame
}
override func willMoveToSuperview(newSuperview: UIView?) {
//1. newSuperView add `fakeButton` first.
if (newSuperview != nil) {
newSuperview!.addSubview(self.fakeButton)
} else {
self.fakeButton.removeFromSuperview()
}
//2. Then, newSuperView add `self` second.
super.willMoveToSuperview(newSuperview)
}
#objc private func handleDisabledTouchEvent() {
//NSLog("handle disabled touch event. Enabled: \(self.enabled)")
self.sendActionsForControlEvents(.TouchUpInside)
}
}
You have a great misunderstanding of user experience.
If a button is disabled, it is meant to be non-interactable.
You can not click on a disabled button, that is why it is disabled.
If you want to warn users about something when that button is clicked (e.g. form not filled correctly or completely), you need to make that button enabled. And just warn users when they click on it, instead of proceeding further with app logic.
Or you can keep that button disabled until form criteria are met, but show what is wrong with the form using another way, like putting exclamation marks near text fields, changing text field colors to red, or something like that...
But never try to add gesture recognizers, or hidden fallback buttons to a disabled button.
Check those and let me know if you see a disabled button:
https://airbnb.com/signup_login
https://spotify.com/us/signup/
https://netflix.com/signup/regform
https://reddit.com/register/
https://twitter.com/signup
https://facebook.com/r.php
https://appleid.apple.com/account
https://accounts.google.com/SignUp
https://login.yahoo.com/account/create
https://signup.live.com/signup
All the proceed buttons on these websites are always enabled, and you get feedback about what is wrong when you try to continue.
And here is really good answer: https://ux.stackexchange.com/a/76306
Long story short: disabled UI elements meant to be not-interactable.
Trying to make them interactable while they are disabled is the same to making them enabled in the first place.
So, for your question's case, it is just a styling issue. Just try styling your button, instead of making it disabled/enabled.

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

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?
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:
To scroll in accessibility mode swipe three fingers
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
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
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.
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"
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.
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.
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.
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.
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.
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
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
}
}
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)
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
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."
}

Resources