How to specify accessibility identifiers for multiple UIPickerViews during XCUITest - objective-c

When developing Xcode UI testcases for a view controller with multiple UIPickerViews I ran into several bugs preventing success all relating to being able to uniquely identify the pickers within XCUITest.
What "should" work is to simply set the accessibility identifier, or accessibility label, from within storyboard like so:
But this does not work at all for a UIPickerView, though I verified the accessibilityLabel and accessibilityIdentifier properties are set for the UIPickerView. And yes, I tried it with one or the other or both set. I even tried programmatically setting one or the other or both. The below lines within an XCUITest case fail to locate the picker regardless:
XCUIElement *shippingMethodPicker = app.pickerWheels[#"Shipping method"];
[shippingMethodPicker adjustToPickerWheelValue:#"USPS Media Mail"];
It would seem that this is a known issue, and that the solution would be to make the view controller also a UIPickerViewAccessibilityDelegate, and implement the - (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component delegate method.
The Apple API Documentation would seem to describe exactly what we need to uniquely apply an accessibility label to each pickerWheels component.
But this is also bugged, the pickerView parameter is not actually a UIPickerView *, as referenced in this stackoverflow link Unable to get pickerView.tag in -pickerView:accessibilityLabelForComponent: method
Due to the implementation defect with the delegate method, you cannot determine which UIPickerView the delegate is being called for rendering it useless for a view with more than one picker.
With the storyboard approach bugged, and the accessibility delegate also bugged, I could not locate a way to uniquely identify two or more UIPickerViews in a view controller from within a XCUITest testcase.
Anyone have a solution?

Focusing on the previous stackoverflow link comments, I determined a solution that worked for my requirements.
From the debugger we can confirm the previous links comment observation:
The accessibility delegate method,
- (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component;
is quite bugged and does not have a UIPickerView *, but rather a private class UIAccessibilityPickerComponent *. As we can note in the debugger, there is an actual UIPickerView * at the private _picker property of this private class.
Radar opened.
Well this is an internal test problem, it's not something we would ship in the app for the App Store. So we CAN use private interfaces to get around this problem. We will only compile this when we are performing UI testing.
First, create a new build configuration in Xcode that you would only use for Testing, duplicated from Debug. Within that create a new preprocessor define -DXCUITEST and be sure to set this new build config in your scheme for Test.
Then implement the accessibility delegate as follows:
#pragma mark - UIPickerViewAccessibilityDelegate
#ifdef XCUITEST
- (NSString *)pickerView:(UIPickerView *)pickerView accessibilityLabelForComponent:(NSInteger)component {
NSString *label;
UIPickerView *realPickerView;
Ivar picker;
// we are going to work around a bug where the pickerView on this delegate is the wrong class by
// pulling the UIPickerView * that we need from the private property of the UIAccessibilityPickerComponent class
picker = class_getInstanceVariable([NSClassFromString(#"UIAccessibilityPickerComponent") class], "_picker");
// check if the bug still exists and apply workaround only if necessary
if (![pickerView isKindOfClass:[UIPickerView class]])
realPickerView = object_getIvar(pickerView, picker);
else
realPickerView = pickerView;
if (realPickerView == self.shippingMethod)
label = #"Shipping method";
else if (realPickerView == self.someOtherPicker)
label = #"SomeOtherPicker";
return label;
}
#endif
With this workaround the XCUITest testcases finally executed as expected, successfully testing situations of two and even three UIPickerViews on a single view all uniquely identified. Note in my case these were single wheel pickers, if you wanted to solve the problem for multi-wheel pickers, then implement the component logic in the delegate, which is not bugged and works as expected.
Also, don't forget to add this header to the top of your view controller class file:
#ifdef XCUITEST
#import <objc/runtime.h>
#endif

Related

XCUITest using adjustToPickerWheelValue: && viewForRow: delegate results in assertion

I ran into a situation where I could not get an XCUITest for a view controller containing a UIPickerView to work without an assertion being generated. The problem causing the assertion is clearly from a spurious "1 of n" being appended to each of the picker values by the accessibility functionality used by XCUITest.
Something as simple as this one line would assert during the test
[app.pickers.pickerWheels.element adjustToPickerWheelValue:#"ANYVALUE"];
This turns out to be a known bug with Xcode, and there is an open radar Cannot use UI Testing to adjust pickerView which uses a view-based delegate
This was my problem exactly as I use the viewForRow: delegate in order to use attributed strings in the picker values. The alternative, using the titleForRow: delegate, does NOT exhibit the problem.
I have tried to work around this by manually setting accessibility information, but to no avail.
And I cannot consider using the titleForRow: delegate, due to my clients requirements for the attributed text.
Yet the dilemma, to also deliver complete XCUITests for all views as another client requirement.
What viable workarounds are there?
This is a known bug in Xcode, and it has been opened for quite some time now, going back to at least this 2015 Apple Developer Forum Post
No fix coming obviously.
My solution to meet all client requirements was to create an additional build configuration called "Testing", which was duplicated from "Debug", so that I could define a new preprocessor macro XCUITEST. Then editing the scheme under Test to use this new build configuration, instead of either DEBUG or RELEASE.
With this change I could incorporate the delegate that works for XC testing and yet keep the one that meets the client app requirements like so:
#ifndef XCUITEST // for release or debug we use this method as we want attributed text strings
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
// blah blah
}
#endif
#ifdef XCUITEST // for testing we have to use this method so XCUITests will work with adjustToPickerWheelValue:
- (UIView *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
// blah blah
}
#endif

Category declaration in *.m file affects whole app

I implemented the following category for UINavigationController inside of one *.m file:
#interface UINavigationController (ConfirmPop) <UINavigationBarDelegate>
#end
#implementation UINavigationController (ConfirmPop)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
...
return YES;
}
#end
I was trying to check some conditions before popping the current view controller, and it worked all right but I short noticed that that category affected all UINavigationControllers in my app. Why does that happen? I thought this would only happen if I declared it on a header file and if I imported somewhere - which is not the case.
Categories apply globally. There isn't a way to apply them selectively.
Categories are used to add new functionality to all instances of that class, especially when your code isn't responsible for creating instances of that class -- otherwise subclassing might be a better choice.
You could create your own instance of a UINavigationController with the desired behavior and only use it where you want that behavior. Or if that's somehow not possible, you could add property-like methods to the category that toggle the desired behavior on and off.

How to use NSWindowController class

I am sorry if this seems trivial, but I am sure its a reasonable question to ask here.
I worked a lot around the NSWindowController class, and it seems the only way to get it
to work fully (for my purpose), is by creating a new xib-file along with it.
My question is, would it be somehow feasible to work with MainMenu.xib and the NSWindowController class and an instantiated object controller, to get interaction with the windows' content. So far without xib the only code segments getting executed are within awakeFromNib. The purpose being, I want to save xib-file space, complexity and have it easily integrate with a bigger project. Just fyi this is not a document-based project.
Should I choose a different subclass of NSObject other than NSWindowController? Or is it not possible?
The code required to run for the class to be working fully is as follows:
- (void) tableViewSelectionDidChange:(NSNotification *)notification
{
NSInteger selectedRow = [logsTableView selectedRow];
if ([directoryList containsObject:[directoryList objectAtIndex:selectedRow]])
{
NSString *logContent = [NSString stringWithContentsOfFile:[directoryList objectAtIndex:selectedRow]
encoding:NSUTF8StringEncoding
error:NULL];
if (logContent != NULL)
{
[logsTextView setString:logContent];
} else
{
[logsTextView setString:#"No permission to read log"];
}
}
}
NSWindowController usually wants to create the window it controls, which means you either need to give it a XIB file that contains the window to create or override the various window creation methods to customize the window in code. So it's probably not feasible to use an already-instantiated window from a different XIB with your NSWindowController.
That said, I almost always create a a XIB and an NSWindowController subclass for every window in my apps. Even the preferences window gets its own window controller class. The only exception would be extremely simple windows, but even now I'm struggling to think of a good example.
Your method isn't being called because window controller instance isn't set as the table view's delegate. The typical pattern here is to create your window in a XIB, set your window controller as the custom class of the File's Owner object, and then hook up the table view's delegate and dataSource outlets to File's Owner. This makes your window controller the table view's data source and delegate, and the connections will be established automatically when the XIB is loaded.

Trouble accessing NSTextField from method called by a Custom View

I'm almost done writing an image editing program, but ran into a problem that should have a simple solution.
Basically, I've built a set of buttons and NSTextFields and a Custom View into the main xib, just dropping them straight off the Library into the default window (and then, of course, linking them up with IBOutlets and IBActions). One of the buttons is an "Open" button that calls a function. The function does several things: it runs a NSOpenPanel, and then changes some of the NSTextFields (used for changing image name and path). That code is called as follows:
- (IBAction)openButtonPressed: (id)sender {
[self runOpenPanel];
}
Now I also happen to be running my keyDown handler from the Custom View, as I've told it to acceptFirstResponder. (I know, it's probably bad practice to not write a separate Controller class, but that's best left for another time.) So my keyDown event looks like this (simplified, as in the actual code I have has if statements to handle certain keys separately that don't pertain to this question):
- (void)keyDown: (NSEvent *)theEvent {
[self runOpenPanel];
}
So they both use [self runOpenPanel] but from different contexts. The problem I'm having is that "runOpenPanel" makes a few calls to change IBOutlets, like this:
-(void)runOpenPanel {
// Omitting some of the trivial NSOpenPanel code
[myTextField setStringValue: #"The file name NSString from the aforementioned omitted code."];
}
So myTextField updates when the function is run by an Interface Builder button, but not when run by the Custom View's keyDown handler. Is there some way to call runOpenPanel that will allow it to access myTextField? I've tried using [[super self] runOpenPanel] (don't laugh) and a number of other things. Thanks in advance!

How to activate a custom screensaver preview in Cocoa/Obj-C?

I have created a fairly simple screensaver that runs on Mac OS 10.6.5 without issue.
The configuration screen has accumulated quite a few different options and I'm trying to implement my own preview on the configureSheet window so the user (just me, currently) can immediately see the effect of a change without having to OK and Test each change.
I've added an NSView to the configureSheet and set the custom class in Interface Builder to my ScreenSaverView subclass. I know that drawRect: is firing, because I can remove the condition for clearing the view to black, and my custom preview no longer appears with the black background.
Here is that function (based on several fine tutorials on the Internet):
- (void)drawRect:(NSRect)rect
{
if ( shouldDrawBackground )
{
[super drawRect:rect];
shouldDrawBackground = NO;
}
if (pausing == NO)
[spiroForm drawForm];
}
The spiroForm class simply draws itself into the ScreenSaverView frame using NSBezierPath and, as mentioned, is not problematical for the actual screensaver or the built-in System Preferences preview. The custom preview (configureView) frame is passed into the init method for, um, itself (since its custom class is my ScreenSaverView subclass.) The -initWithFrame method is called in configureSheet before returning the configureSheet object to the OS:
[configureView initWithFrame:[configureView bounds] isPreview:YES];
Maybe I don't have to do that? It was just something I tried to see if it was required for drawing.
I eventually added a delegate to the configureSheet to try triggering the startAnimation and stopAnimation functions of my preview via windowWillBeginSheet and windowWillEndSheet notifications, but those don't appear to be getting called for some reason. The delegate is declared as NSObject <NSWindowDelegate> and I set the delegate in the configureSheet method before returning the configureSheet object.
I've been working on this for days, but haven't been able to find anything about how the OS manages the ScreenSaverView objects (which I think is what I'm trying to emulate by running my own copy.)
Does anybody have any suggestions on how to manage this or if Apple documents it somewhere that I haven't found? This isn't really required for the screensaver to work, I just think it would be fun (I also looked for a way to use the OS preview, but it's blocked while the configureSheet is activated.)
OK, there are a couple of 'duh' moments involved with the solution:
First of all, I was setting the delegate for the sheet notifications to the sheet itself. The window that the sheet belongs to gets the notifications.
Secondly, that very window that the sheet belongs to is owned by System Preferences, I don't see any way to set my delegate class as a delegate to that window, so the whole delegate thing doesn't appear to be a viable solution.
I ended up subclassing NSWindow for the configureSheet so that I could start and stop animation on my preview by over-riding the makeKeyWindow and close methods.
- (void) makeKeyWindow
{
if (myPreview != nil)
if ( ! [myPreview isAnimating])
{
[myPreview startAnimation];
}
[super makeKeyWindow];
}
I also had to add an IBOutlet for my preview object itself and connect it in Interface Builder.
Still working out a couple of issues, but now when I click on my screensaver Options button, my configureSheet drops down and displays its own preview while you set options. Sheesh. The hoops I jump through for these little niceties. Anyway, I like it. Onward and upward.