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
Related
I have a bunch of static objects (UILabel, buttons, views) in multiple Scenes. They are not connected to any IBOutlet. But I'd like to access them at appdelegate (or first VC), and change their properties before it is loaded.
Anyway to do this?
EDIT: Adding my intention:
I actually wanted to make a custom "multi-language" app. I want to be able to change language from within the app. I can get a list of all the objects by applying built in localization of storyboard (Main.strings is autogenerated). Then I disable localization again. Then from this autogenerated file, I want to be able to connect it to a json data based on language that I select.
Of course you can. For example, you can use tags of UIView. Just set tags in Storyboard. It's easy but not so good. Another way to do this is using Accessibilities. Enable and set for it in Storyboard.
And then you can access it by accessibilityIdentifier property.
I will post my choice of "solution". So what I did was make use of accessibilityIdentifier to set the "key" for the multilanguage phrase translation purpose.
And I make use of the UIView+Recursion class (you can find this simple class somewhere in SO), and basically iterate all the objects in a particular Scene and if the text matches, set the key in accessibilityIdentifier property (either in viewDidload or viewWillAppear or viewDidlayoutSubviews).
This way you can have language changes "on-the-fly" within the app, without restarting.
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.
I have created a custom class for my UIBarButtonItem (refreshIndicator.m). This button will be on many different view controllers, all push-segued from my MainViewController/NavigationController.
Instead of dragging an outlet onto every single ViewController.m file for iPhone storyboard THEN iPad storyboard (ugh, still targeting iOS7), I want to know if there is a way to complete my task simply within my UIBarButtonItem custom class. I've looked around everywhere but I haven't quite found an answer to this,
All I need to do is check which UIViewController is present, check the last time the page was refreshed, and then based on that time, set an image for the UIBarButtonItem. (I've got this part figured out though, unless someone has a better suggestion). How can I check for the current UIViewController within a custom button class? Is this possible?
Does it need to know which view controller its on so it can tell that vc it was pressed? If that's the case, then use your button's inherited target and action properties. On every vc that contains an instance of the button, in view did load:
self.myRefreshIndicator.target = self;
self.myRefreshIndicator.action = #selector(myRefreshIndicatorTapped:);
- (void)myRefreshIndicatorTapped:(id)sender {
// do whatever
}
More generally, its better to have knowledge about the model flow to the views from the vc, and knowledge of user actions flow from the views. Under that principal, your custom button could have a method like:
- (void)timeIntervalSinceLastRefresh:(NSTimeInterval)seconds {
// change how I look based on how many seconds are passed
}
And your vcs:
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:self.lastRefreshDate];
[self.myRefreshIndicator timeIntervalSinceLastRefresh:interval];
If you really must go from a subview to a view controller, you could follow the responder chain as suggested in a few of the answers here (but I would go to great lengths to avoid this sort of thing).
It is possible to achieve this, but the solution is everything but elegant. It is one way of getting around the basic principles of iOS and is strongly discouraged.
One of the ways is to walk through the responder chain, posted by Phil M.
Another way is to look through all subviews of view controllers until you find the button.
Both ways are considered a bad practice and should be avoided.
For your particular case, I would rethink the structure of having a separate instance of the bar button. For example, you could rework it into a single UIButton instance that gets displayed over every view controller and it can also act as a singleton.
I have an iOS app developed in Xamarin.iOS (C#, Monotouch) where the primary UI is NOT storyboard-based. (I do this because my app needs structurally different layouts in portrait and landscape orientations, and it is a great deal easier to achieve that programmatically than through IB and Storyboarding.)
My problem is that I'm now trying to use a Storyboard to develop a simple dialog, but when I instantiate the dialog the structure is there but the style elements defined in Interface Builder are not being applied. Everything I've read seems to suggest that this should just happen. This is particularly problematic as most of the style elements cannot be modified after the interface is initialized.
Here's the code where I do the instantiation:
UIStoryboard sb = UIStoryboard.FromName("StoryboardAppSettings", null);
var vc = sb.InstantiateViewController("TableViewControllerAppSettings");
UIViewController settingsVC = vc as UIViewController;
PresentViewController(settingsVC, true, null);
I have defined a UITableView that is to be "grouped", but it isn't. I have two UISwitch elements with colors defined that are not being applied. I have buttons whose tint colors are not being applied.
(If I set a breakpoint and drill down into the view controller data structures, I find that all these parameters ARE set correctly...they just seem to be ignored when the hierarchy is realized.)
What am I doing wrong?
I found that the problem was that I was setting UIView.Appearance.BackgroundColor. It seems that when you do that, the UIView.Appearance.BackgroundColor ALWAYS overrides UISwitch.OnTintColor. (In my mind that is a bug, probably in iOS, but I may misunderstand how the Appearance API is supposed to work. My expectation is that a generalized global setting (like UIView.Appearance) can be overridden at more specific levels (descendent views or specific instantiations), and I believe that is the way it works for most other Appearance properties. UISwitch.OnTintColor (as well as UISwitch.TintColor and UISwitch.BackgroundColor, but NOT UISwitch.ThumbTintColor) seems to be the exception (and hence bug), not the rule.
note: This is an expansion (and clarification) of a question I asked yesterday.
I am conducting a research project where I want to record all of the user's touches in the iPhone app. After the experiment, I will be able to download the data and process it in either Excel or (more likely) Matlab and determine how many times they clicked on certain buttons, when they clicked certain buttons, etc. To do this, I would need to know:
a) When they touched
b) Where they touched
c) Which view they touched
The first two are easy, but the third I am having trouble with. I know I can do this to get the reference to the UIView that was touched:
CGPoint locationPoint = [[touches anyObject] locationInView:self];
UIView* viewYouWishToObtain = [self hitTest:locationPoint withEvent:event];
However, that will just give me a pointer to the view, not the name of the view that was touched. I could assign each view a tag, but then every time I create a new view I would need to remember to tag it (or, alternatively, log the address of each view when initialized and log it when the view is touched). Subclassing UIView and adding an automatic tag isn't really an option since I'm creating other UIButtons and UISliders and would need to subclass those also, which doesn't seem like a very good solution.
Does anyone know of a clean, easy way to do this?
For "Which view they touched", what information do you need?
Perhaps you could use a category to add a method to UIView. This method would generate a string containing information about the view. Such as:
its type e.g. UIButton etc.
its size and position
the title of the view, if it has one (e.g. the button title)
the parent view type and title
other stuff e.g. is the view enabled, what state it is in. anything you like.
For example: "Type:UIButton Title:"Back" Rect:{3,5,40,25}" or some such string.
This is very clean and gives you quite a lot of information to be going with.
You could add a category to UIView which would then be inherited by all UIView descended objects, although I'm not sure its any more efficient than tagging. Since a category can override methods then you could override init methods for automatic tagging I suppose.
http://macdevelopertips.com/objective-c/objective-c-categories.html
I'm not sure what you mean by the "name" of the view. If you mean the view name in Interface Builder, I don't believe it includes that in the instantiated objects. You could use the Tag attribute which is included, but that's just a number and not a name.