I am trying to add iOS accessibility support/Voice Over to my app. My main screen has three main controls, but the third control is hosted within an embedded view controller.
I am setting accessibility elements in prepareForSegue and have confirmed that the embedded view controller controls are all loaded. Problem is I can still only select the first two controls which are in the enclosing view controller.
self.view.accessibilityElements =
#[
self.cmdMenu, // works
self.collectionView, // works
self.childViewController.peerMenu // doesn't work
];
All three views have isAccessibilityElement = YES.
Am I missing something? I can't imagine that there is a restriction on the accessibility elements being in the same view controller.
I found my bug and now have Voice Over working. In the process I figured out a number of things that I would like to share.
To my original question, you can reference controls in your child view controllers from your main view controller. You can add the controls directly (as I did in my question) or you can add all accessibility elements in the child view controller using self.view.accessibilityElements = #[ _control1, childViewController.view, childViewController2.view].
If you add all of the controls in your child view controller as in (1.) then ensure that childViewController.view.isAccessibilityElement = NO.
You can add any kind of object to accessibilityElements, even elements that have no accessibility information. The API will not assert or warn you. This ended up being my bug.
If your UI changes and you need to change the number or order of items in your accessibilityElements array tell UIKit about it using UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self). The notification argument (where I'm sending self) tells Voice Over where it should position its cursor when the notification completes.
If you want to read aloud some text for a transient notification (imagine when Clash Of Clans tells you how many Gems you found in that Tree Stump) call UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, messageText). One caveat, this won't read aloud the messageText unless there is no other Voice Over in progress. You need to manage the timing yourself. Submitted a bug on this. Apple could make this a lot better.
If you are using UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, messageText) you can listen for UIAccessibilityAnnouncementDidFinishNotification, but unfortunately this notification has almost no value. You only will get notified if your messageText was fully spoken. It doesn't tell you that it was spoken, but interrupted, and it will also not get triggered for any text spoken through the UIKit framework.
The Accessibility Inspector in the iOS Simulator kind of sucks. If your accessibility settings are correct, it can tell you what is there. If you have a problem the Inspector does not provide you any information about what is wrong. This is true of the entire UIAccessibility API. It is so easy to use that it almost always works. But when it doesn't work you need to resort to hunt and peck to figure it out. The API needs some assertions or console messages similar to how Apple handles Constraint warnings. Spoiler alert: the Accessibility Inspector in Xcode 8 is wayyyyy better, but still would not have helped with my issue.
There is a ton of good information in the UIAccessibility.h header. If you are embarking on UIAccessibility support, it is a good read.
Related
I am using iOS UITest for a Swift application. I use something like,
func testAllScreenNavigation() {
let app = XCUIApplication()
app.tabBars.buttons["Home"].tap()
app.navigationBars["Home"].buttons["More"].tap()
app.sheets.buttons["Cancel"].tap()
}
etc. to navigate some of the specific, tabs, buttons, etc. and switch to respective screens. But i want to navigate each and every screens of my Application (It can be BFS style navigation or DFS style navigation, no matter). Is there any way iOS provides so i can get all navigable elements and then explore deeper and deeper automatically for my App?
I also need to keep trace of which xcuoelement in a screen is already processed and which are not yet processed.
The only way I can think of is using Xcode UI test recorder feature.
While you are recording, navigate through all of your screens via the device/simulator and then the XCUIApplication() variable would be recorded with the appropriate references.
If the button/nav bar/any element has text on it, it will show up in the recorded code or else it will be referenced numerically.
Hope that helps.
Kind regards,
Mukund
I like your idea for getting all views and check whether the layouting and localization for example is fine.
I think you need to specify your criteria for "screens" and how they are accessed.
Basically, one could thing of the following structure
- UITabBarController
-- UISplitViewController
--- UINavigationController
---- UIViewController
----- UIBarButtonItems
----- UIView
----- UIButton
----- UISwitch
----- UITableViewCell
You could now go top down from the UITabBarController to the next controlling instance (might also skip one, e.g. SplitViewControllers on iPhones).
You can use the general property:
XCUIApplication().tabBars
Nevertheless that transition is the problem: How would you get from one ViewController to another and are they all position in the ViewController's View or do you have to loop the subviews of a view.
UIButton -> Touch Up Inside
UISwitch -> Value Changed
UITableViewCell -> DidSelectRowAtIndexPath
UIView -> UILongPressGestureRecognizer
This is how I would basically set it up:
For each UIViewController instance, get the related View (and perform the following call recursively).
Check all the subviews of a view.
For UIViews, go even further and check their subviews
For UIButtons, perform TouchUpInside
and so on.
Make sure to have a condition to stop going deeper, as UITableViews got a lot of subviews or your UIWebViews would of course be set up in a different way.
This way you should be able to navigate through a lot Views in your app hierarchy, but you will need some extensions for UIBarButtonItems, custom Gesture Recognizers and of course also for your "special" controls that might listen to value changes and perform a layout-change.
Accessing specific elements
In addition to the above approach where you simply get an array of elements of a specific type, you can access specific elements (e.g. those where you know they are of a very specific type with certain ValueChangeListeners or something)
To access a specific object in particular, like the TabBar example from above, you can use the accessibilityLabel like so. At first you need to declare the accessibilityLabel in your code or in the .xib-file/.storyboard:
// just to illustrate, so you get an idea:
self.tabBarController.isAccessibilityElement = true
self.tabBarController.accessibilityLabel = "tabBar"
And then do:
let tabBar = XCUIApplication().tabBars["tabBar"]
Here is Apple's documentation for setting these accessibilityLabels:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html
A great way to get the related identifier of an element would be to use the Accessibility Inspector from Apple:
https://developer.apple.com/library/content/technotes/TestingAccessibilityOfiOSApps/TestAccessibilityiniOSSimulatorwithAccessibilityInspector/TestAccessibilityiniOSSimulatorwithAccessibilityInspector.html
Accessing elements in general
To access elements in general, you need to make use of the XCUIElementType of these objects, here you will access the objects based on their classes.
E.g. you could call:
"tabBars", "navBars", "tables", "buttons", and so on from the elements in general.
Still you would be facing the issue with "special controls". As the Apple documentation lacks (imho) some detail about properties and attributes, I do recommend the docs here: https://blog.metova.com/guide-xcode-ui-test/ It provides a great overview of what is accessible and may help you getting some better understanding.
An overview of the available XCUIElementTypes can be found here. Basically, the elementType property is an enumerated value that represents the type of an element. XCUIElementType is a very large enumeration and some of its members do not apply to iOS applications (they apply to MacOS X apps). Some of the more commonly used values are:
Alert
Button
NavigationBar
TabBar
ToolBar
ActivityIndicator
SegmentedControl
Picker
Image
StaticText
TextField
DatePicker
TextView
WebView
https://developer.apple.com/reference/xctest/xcuielementtype?language=objc
How do I test whether a view is hidden using XCUITest? A view is sometimes hidden (set in Xcode like this: Hidden view)
How do I test for that in XCUITest using Swift? In my case, the view is just a label. I tried something like this: XCTAssertFalse(app.staticTexts["pushNotificationInstruction"].accessibilityElementsHidden) . But that's not it. accessibilityElementsHidden is not the same as the view is hidden. Thanks.
Unfortunately, this is not currently possible using XCUITest. Here is a developer forum thread where an Apple engineer suggested filing a radar for this exact issue:
https://forums.developer.apple.com/message/46271
I have personally filed a couple radars relating to the limitations imposed by not being able to access certain properties of UIViews from within an XCUITest. I encourage you to do the same and provide details of the scenarios you are blocked from testing because of this deficiency in XCUITest.
You can assert that the view does not exists and use another test to check the scenario when it does exists. Maybe a bit fragile, but that would cover you case.
let viewControllerShown = app.otherElements["view_myviewcontroller"].waitForExistence(timeout: 5)
XCTAssert(viewControllerShown)
let instructionViewExists = app.staticTexts["pushNotificationInstruction"].exists
XCTAssertFalse(instructionViewExists)
An expedient solution is to carry the visibility state of the view in its accessibilityidentifier.
In your view controller:
view.isHidden = hideView
view.accessibilityidentifier = "view1"+(hideView ? "hidden" : "")
In your tests:
XCTAssert(app.otherElements["view1"].exists)
or
XCTAssertFalse(app.otherElements["view1"].exists)
Looking at the documentation of exist:
The fact that an element exists does not imply that it is hittable.
Elements can exist offscreen, or exist onscreen but be hidden by
another element, causing their isHittable property to return false.
It means that you can check:
if uiElement.exists && uiElement.isHittable {
XCTFail()
}
I am sure the answer to this is "no" as the documentation is very clear. But I am a little confused. A standard UIAlertView is pretty dull and I want to improve the look and it seems that other apps do it (see the example below).
Another possibility is that they are not subclassed UIAlertViews. In which case, how is this achieved?
The page UIAlertViews states
Appearance of Alert Views
You cannot customize the appearance of alert views.
So how do we get the something like the example shown here?
No, do not subclass it. From the docs:
Subclassing Notes
The UIAlertView class is intended to be used as-is
and does not support subclassing. The view hierarchy for this class is
private and must not be modified.
What you can do though is create a UIView and have it act similar to a UIAlertView. It's isn't very difficult and seems to be what they are doing in your op.
Apple's docs say that you should not subclass it. That means that there are probably internal reasons that would make it difficult to make it work right.
You might or might not be able to make a subclass of UIAlertView work, but you do so at your own risk, and future iOS releases might break you without warning. If you tried to complain Apple would laugh and tell you "I told you so".
Better to create a view that looks and acts like an alert but is your own custom view/view controller. Beware that even this is dangerous, because Apple has been making sweeping changes to the look and feel of it's UI elements recently. If you implement a view controller that looks and acts like a variant of the current alert view, Apple could change that look and/or behavior in the future and your UI app would end up looking odd and outdated. We've been bitten by this sort of thing before.
Rethink your strategy. Why do you need to use an Alert View? Besides having a modal view displayed top-most on your view stack, there's not much else that it does. Instead, subclass UIView or UIViewController to define your own interface, using images and ui elements to give it the style and input functionality as needed.
I usually subclass UIView, and attach it to the app's window's view so that I'm certain that it will be displayed on top of anything else. And you can use blocks to provide hooks into the various input elements of your new view (did user press OK, or did user enter text?)
For example:
// Instantiate your custom alert
UIView *myCustomAlert = [[UIMyCustomUIViewAlert alloc] initWithFrame:CGRectMake(...)];
// Suppose the new custom alert has a completion block for when user clicks on some button
// Or performs some action...
myCustomAlert.someEventHandler = ^{
// This block should be invoked internally by the custom alert view
// in response to some given user action.
};
// Display your custom alert view
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[window addSubview: myCustomAlert];
// Make sure that your custom alert view is top-most
[window bringSubviewToFront: myCustomAlert];
Using this method, however, will not pause the thread's execution like UIAlertView does. Using this method, everything will continue running as usual. So if you need to pause execution while your custom alert is showing, then it gets much trickier.
But otherwise, creating your own custom alerts is quite straightforward, just as you would customize any other view. You could even use Interface Builder.
Hope this helps.
No. You absolutely should not subclass a UIAlertView for any reason. Apple explicitly states this in their documentation (see "Subclassing Notes"). They even tell you that it relies on private methods - and we all know that meddling in private methods in an AppStore app is immediate grounds for rejection.
HOWEVER, there isn't a need to subclass UIAlertView on iOS 7. Apple introduced a new Custom ViewController Transitions feature in iOS 7.0 that lets you present completely custom ViewControllers with completely custom transitions. In other words, you could very easily make your own UIAlertView or even something better. There's a nice tutorial on the new feature here:
In fact, there are lots of good tutorials on this - a quick Google search on the topic turns up a huge wealth of information.
Now, yes, there are hundreds of questions (and answers) of how to perform custom segues. However, and I'm no exaggerating, ALL of these answers are wrong (all 50+ I've seen)! Sorry, this might sound harsh, but the truth is, NONE of the suggested answers gives the same (correct) result as Apples built in transitions do (vertical cover etc.).
To be more specific, this is the result that's expected (confirmed with logs):
Segue begins (adds view to hierarchy, invokes viewWillAppear on destinationVC and viewWillDisappear on sourceVC and starts animation).
animation is performed for the whole duration
Segue ends (animation finished, sets the destinationVC as the current VC, either on stack or modally presented. Invokes viewDidAppear on destinationVC and viewDidDisappear on sourceVC).
In short: invoke viewWillAppear/Disappear -> animate transition -> invoke viewDidAppear/Disappear
Using apples built-in segues, this is the expected behavior but somehow not a single soul except me have had issues with this. A lot of versions even add the destination-view as subview first, animates it then removes it again and calls
[srcVC presentModalViewController:destVC animated:NO];
or
[srcVC.navigationController pushViewController:destVC animated:NO];
causing the view-events to be sent in all kinds of random order (same issue with CoreAnimations).
In my case, all I really want is the "Vertical Cover"-transition reverted (top to bottom), with all other events sent EXACTLY as expected (shown above).
So, am I just supposed to live with all kinds of ugly workarounds (doing my "tasks" in hard-coded methods called whenever I need them to etc.), or is there some hidden proper way of doing this in a reusable manner?
Funny thing: Even apple suggest that you do it the "wrong" way, making it seem like the right way but with inconsistent outcome compared to their own ways… So my best guess is that apple do this under the hood, and forgot to give enough flexibility for clients to perform the same operations (big flaw in other words), or that I'm just on some bad trip and see some issue that doesn't exist…
Okay, this might not be a true answer of how to solve it for custom segues (subclassing UIStoryboardSegue), but it does solve the general issue!
After some casual reading about new features, I stumbled upon a new way to do custom transitions between ViewControllers introduced in iOS7 called nothing more than "Custom Transitions" i guess!
Read about it here and here, or video from WWDC here.
I've just dipped my toes, but basically it is a new, closer to the system way of doing transitions and allows for better control = better looking transitions. After glancing at the example provided by the blog I referenced (git found here) I can confirm that FINALLY, we are able to do transitions which behave as ONE EXPECTS THEM TO with events fired at the expected occasions!
Since I'm just reading about it I can't give a thorough explanation yet, but check out the links :)
Note: This is maybe not supposed to completely replace custom segues, but can be used in a similar fashion (check examples) so if you need that little extra fancy transition, this is definitely the way to go by the looks of it! Basically you setup segues in the storyboard, and hook up the correct transition-delegates in the prepareForSegue:-method.
I'm interested in doing something similar to this, however the component subviews of MFMessageComposeViewController are a much different than MFMailComposeViewController.
I figured out how to set focus to the input that let's you to type your message text by simply calling setRecipients: with an array containing a blank NSString. However, I'd like to paste non-text from the pasteBoard into the input, so I can't simply use setBody:.
Problem:
What I need to do is get a reference to the actual text field that is the current first responder for my MFMessageComposeViewController. This way, I have a "sender" I can pass to UIPasteboard's paste: method. The problem is, I can't seem to walk the subview hierarchy the same way as MFMailComposeViewController, so I can't find out which view is first responder.
I've even tried this, but the view is always returned as nil if I do a [myMessageVC.view findFirstResponder]
Word of caution, you're not supposed to have your hands inside that view. Apple will refuse your app for doing so. You are only allowed to set the body and recipients.
Important The message composition interface itself is not customizable
and must not be modified by your application. In addition, after
presenting the interface, your application is unable to make further
changes to the SMS content. The user can edit the content using the
interface, but programmatic changes are ignored. Thus, you must set
the values of content fields, if desired, before presenting the
interface
http://developer.apple.com/library/ios/#documentation/MessageUI/Reference/MFMessageComposeViewController_class/Reference/Reference.html