Xcode UI testing collection view cells became non-hittable on iOS 11 - ios

We were using this code to simulate a tap on the first cell of a UICollectionView on Xcode UI Testing:
XCUIElementQuery *query = [application descendantsMatchingType:XCUIElementTypeAny];
XCUIElement *collectionView = [query objectForKeyedSubscript:collectionViewAccessibilityIdentifier];
XCUIElement *targetCell = [lensesCollectionView.cells elementBoundByIndex:cellIndex];
if (targetCell.hittable) {
[targetCell tap];
}
this works fine on iOS 10, but stopped working on iOS 11. The targetCell is never hittable no matter what. Adding a sleep(10) before XCUIElement *targetCell = [lensesCollectionView.cells elementBoundByIndex:lensIndex] doesn't help.
I've seen hacky solutions mentioned elsewhere such as
func forceTapElement() {
if self.isHittable {
self.tap()
} else {
var coordinate: XCUICoordinate = self.coordinateWithNormalizedOffset(CGVectorMake(0.0, 0.0))
coordinate.tap()
}
}
but that doesn't look very clean. What's the cleanest way of achieving this?
Update: If I try to tap it without checking for hittable I get this error:
error: Error -25204 performing AXAction 2003 on element pid: 43616, elementOrHash.elementID: 4882574576.240

It turns out that isAccessibilityElement was NO on our custom collection view cells on iOS 11 (strangely, it was YES on iOS 10). Explicitly setting it to YES fixed the issue.

The answer of Ricardo should be the accepted one.
For clarity, we had the same issue and solved it in the Class file of the UICollectionViewCell.
In initWithCoder: we just added property isAccessibilityElement:
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self != nil) {
self.isAccessibilityElement = YES;
}
return self;
}
We could now run the same test script form Xcode 8 in Xcode 9.1 and the cell was tapped properly.
Thanks for this great solution.

Related

"NSInternalInconsistencyException [...] Sigh. Contentview size is zero." crash when using a UINavigationController

My code runs perfectly fine on iOS 10.3.3, whereas when I run the same code on iOS 11.2.1, it causes a crash at launch time with the following error:
Assertion failure in -[_UINavigationBarVisualProviderModernIOS _contentViewFittingHeight], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3698.33.7/_UINavigationBarVisualProviderModernIOS.m:569
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Sigh. Contentview size is zero.'
I cleaned the code, cleared the derived data, but those did not solve the issue.
There was an issue in the library I was using for Slide menu "iOS-Slide-Menu". So, I simply changed this two very methods in the library which then worked fine.
- (void)setup
{
[[NSUserDefaults standardUserDefaults] setObject:nil forKey:#"ssidName"];
[[NSUserDefaults standardUserDefaults] synchronize];
if (singletonInstance)
NSLog(#"Singleton instance already exists. You can only instantiate one instance of SlideNavigationController. This could cause major issues");
singletonInstance = self;
self.menuRevealAnimationDuration = MENU_SLIDE_ANIMATION_DURATION;
self.menuRevealAnimationOption = MENU_SLIDE_ANIMATION_OPTION;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Update shadow size of enabled
if (self.enableShadow)
self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath;
self.landscapeSlideOffset = self.view.frame.size.width/6;
self.portraitSlideOffset = self.view.frame.size.width/6;
self.panGestureSideOffset = 0;
self.avoidSwitchingToSameClassViewController = YES;
self.enableShadow = YES;
self.enableSwipeGesture = NO;
self.delegate = self;
// When menu open we disable user interaction
// When rotates we want to make sure that userInteraction is enabled again
[self enableTapGestureToCloseMenu:NO];
if (self.menuNeedsLayout)
{
[self updateMenuFrameAndTransformAccordingToOrientation];
// Handle different horizontal/vertical slideOffset during rotation
// On iOS below 8 we just close the menu, iOS8 handles rotation better so we support keepiong the menu open
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0") && [self isMenuOpen])
{
Menu menu = (self.horizontalLocation > 0) ? MenuLeft : MenuRight;
[self openMenu:menu withDuration:0 andCompletion:nil];
}
self.menuNeedsLayout = NO;
}
}
Fixed out, that two lines must be in viewWillLayoutSubviews instead of setup method. And like #Ishika said, this is the problem of iOS-Slide-Menu.
self.enableShadow = YES;
self.enableSwipeGesture = YES;
This error occured to me with Xcode 10.2.1 and SideMenu 6.0.4. I ended up configuring the navigation controller programmatically, which solved the issue.
let sideMenuVc = UISideMenuNavigationController(rootViewController: <view controller>)

SlideMenu not detecting touches for dismiss menu on iOS 11. Why?

I'm using the pod iOS-Slide-Menu repo in one of my personal projects.
If I run my project on iOS < 11 everything works as expected: When the side menu (blue view controller), touching outside it, i.e. the green part. Automatically closes it
But running on a device with iOS 11 the menu does't get closed when tapping outside.
Another curious situation is that this only happen with recently compiled versions (currently using Xcode 9.0), running the App Store version on a iOS11 device also works correctly.
So my questions are:
Why is this happening?
How can I avoid this to happen without replacing the whole library?
I created a SAMPLE PROJECT in github to reproduce the problem.
In your SlideNavigationController.m file go to viewWillLayoutSubviews method and remove or comment below line
[self enableTapGestureToCloseMenu:NO];
It is because viewWillLayoutSubviews method gets called in ios 11 initially!
so, from viewWillLayoutSubviews, enableTapGestureToCloseMenu gets called and it is removing gesture recognizer from right menu!
Hello #Adrime i have downloaded your code and tested in Xcode 9.0
found same issue like you have. outside tapped not closed view.
after seeing library i have found one solution.
In SlideNavigationController.m file,
one method is already created which is - (void)enableTapGestureToCloseMenu:(BOOL)enable
in that method, just comment this one line [self.view removeGestureRecognizer:self.tapRecognizer];
and your problem is solved.
this line removedGesture of tapping outside.
Updated:
I got it what you want, just change your viewWillLayoutSubviews method
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Update shadow size of enabled
if (self.enableShadow)
self.view.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.view.bounds].CGPath;
// When menu open we disable user interaction
// When rotates we want to make sure that userInteraction is enabled again
//[self enableTapGestureToCloseMenu:NO];
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0"))
self.interactivePopGestureRecognizer.enabled = YES;
self.topViewController.view.userInteractionEnabled = YES;
if (self.menuNeedsLayout)
{
[self updateMenuFrameAndTransformAccordingToOrientation];
// Handle different horizontal/vertical slideOffset during rotation
// On iOS below 8 we just close the menu, iOS8 handles rotation better so we support keepiong the menu open
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0") && [self isMenuOpen])
{
Menu menu = (self.horizontalLocation > 0) ? MenuLeft : MenuRight;
[self openMenu:menu withDuration:0 andCompletion:nil];
}
self.menuNeedsLayout = NO;
}
}
What I Did : Do comment //[self enableTapGestureToCloseMenu:NO]; code and put below code
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0"))
self.interactivePopGestureRecognizer.enabled = YES;
self.topViewController.view.userInteractionEnabled = YES;
OLD:
It's Because in SlideNavigationController.m file you are calling [self enableTapGestureToCloseMenu:NO];
Just remove it or make condition for iOS 11 to pass YES for all
For EX.
if (IOS_VERSION == 11) {
[self enableTapGestureToCloseMenu:YES];
}
else {
[self enableTapGestureToCloseMenu:NO];
}
Because when you open your slide menu or click on green area alway call
[self enableTapGestureToCloseMenu:NO];
So as per method code
- (void)enableTapGestureToCloseMenu:(BOOL)enable
{
if (enable)
{
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0"))
self.interactivePopGestureRecognizer.enabled = NO;
self.topViewController.view.userInteractionEnabled = NO;
[self.view addGestureRecognizer:self.tapRecognizer];
}
else
{
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0"))
self.interactivePopGestureRecognizer.enabled = YES;
self.topViewController.view.userInteractionEnabled = YES;
[self.view removeGestureRecognizer:self.tapRecognizer];
}
}
It's removeGestureRecognizer for view.

forceTouchCapability returning nil

I am trying to incorporate some 3D touch into an application and I've run into a weird issue where the forceTouchCapability check is returning nil on viewDidLoad but not in viewWillAppear/viewDidAppear.
I'm aware that this is only available on iOS 9+ so I've added checks to verify that the traitCollection property on the view controller responds to forceTouchCapability as in the following:
- (void)loadView {
self.view = [[MyView alloc] init];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Checking the force touch availability here
if ([self.traitCollection respondsToSelector:#selector(forceTouchCapability)] &&
self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// This won't get called because forceTouchCapability is returning nil
// which corresponds to UIForceTouchCapabilityUnknown
[self registerForPreviewingWithDelegate:self sourceView:self.view];
}
}
In LLDB with a breakpoint at the if statement, entering po [self.traitCollection forceTouchCapability] returns nil which corresponds to UIForceTouchCapabilityUnknown. However, the traitCollection itself is not nil.
According to the documentation for UIForceTouchCapabilityUnknown:
UIForceTouchCapabilityUnknown: The availability of 3D Touch is unknown. For example, if you create a view but have not yet added it to your app’s view hierarchy, the view’s trait collection has this value.
Has the view not been added to the hierarchy by this point?
I'm curious if anyone has run into this issue before and how to work around this? I would like to avoid adding this in the viewDidAppear as this can get called quite a bit.
If it helps, I'm running this on a 6S on iOS 9.1 with Xcode 7.2
The view hasn't been added to the View Hierarchy yet. You can see this easily by checking for a superview in the debug console
(lldb) po self.view.superview
nil
If that's what you're seeing, the view hasn't been added to a hierarchy yet: so you have to put your check elsewhere.
This is kind of confusing because in Apple's ViewControllerPreview sample app it's in viewDidLoad. But it really should in traitCollectionDidChange:, because then you're sure that the view has been added to the app's hierarchy.
This is the code I use (works on iOS 8, if you don't need to support that feel free to move the outer conditional).
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection respondsToSelector:#selector(forceTouchCapability)]) {
if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) {
// retain the context to avoid registering more than once
if (!self.previewingContext) {
self.previewingContext = [self registerForPreviewingWithDelegate:self sourceView:self.view];
}
} else {
[self unregisterForPreviewingWithContext:self.previewingContext];
self.previewingContext = nil;
}
}
}
The added benefit to this is that your view will be registered/unregistered if the user changes their 3D Touch settings while the app is running.
I've also seen this issue, and found that the easiest way to check whether the device can support force touch or not is doing it via the screen instance. This kinda makes sense because the capability is a property of the screen. Doing it this way means you don't have to worry about the lifecycle of a viewcontroller or a view.
func canForceTouch() -> Bool
{
if iOS9OrHigher // pseudocode, a function that makes sure u only do this check on ios9 or higher
{
return UIScreen.mainScreen().traitCollection.forceTouchCapability == .Available
}
return false
}
Like what #bpapa said, Your view hasn't added to view hierarchy yet, But my solution is different little bit:
var token:dispatch_once_t = 0
override func viewDidAppear(animated: Bool) {
dispatch_once(&token) {
// Force Touch Checking
if #available(iOS 9.0, *) {
if self.traitCollection.forceTouchCapability == .Available {
self.registerForPreviewingWithDelegate(self, sourceView: self.view)
}
}
}
}

When assigning focus via becomeFirstResponder to UISearchController's UISearchBar, the keyboard does not appear

I've spent quite a bit of time searching online and talking to other developers about this issue to no avail. The exact issue is described in this SO post (Focus on the UISearchBar but the keyboard not appear), although it's many years old.
I recently switched from using the deprecated UISearchDisplayController and UISearchBar in IB, and switched over to UISearchController via the code for iOS8.
The problem I'm getting however, is that focus is assigned correctly (you can tell because the cancel button animates to the right of the search bar after the view loads), however the keyboard does not show up.
Here's the code that I have.
.h
#property (nonatomic, strong) UISearchController *searchController;
.m
- (void)viewDidLoad {
[super viewDidLoad];
...
[self initializeSearchController];
....
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.searchController setActive:YES];
[self.searchController.searchBar becomeFirstResponder];
}
- (void)initializeSearchController {
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.delegate = self;
self.searchController.searchBar.delegate = self;
[self.searchController.searchBar sizeToFit];
[self.tableView setTableHeaderView:self.searchController.searchBar];
self.definesPresentationContext = YES;
}
The things I've tried so far.
I've tried calling becomeFirstResponder on a 0.2 second delay, as suggested in another SO post.
I've set a breakpoint in viewDidAppear, and verified that both self.searchController and self.searchController.searchBar are both valid objects, neither nil.
I've tried conforming to the UISearchControllerDelegate and using the following snippet of code
here:
- (void)didPresentSearchController:(UISearchController *)searchController {
//no matter what code I put in here to becomeFirstResponder, it doesn't
//matter because this is never called, despite setting the
//self.searchController.delegate = self AND
//self.searchController.searchBar.delegate = self.
}
I've created a new view from scratch in storyboards, and segued to that one instead, to make sure I didn't have some old searchBar remnant in my view. This did not work either.
I've only tested this on a real device (iPhone 6), and it's not a simulator issue of not showing the keyboard.
I'm out of ideas, and I've seen every question and answer related to this one the web. Nothing is working.
To clarify again what's going on, the searchBar correctly becomes the first responder, the cancel button to the right of it animates onscreen proving this, but the keyboard does not appear and the cursor does not blink in the searchBar.
Your code looks ok. What you are describing isn't normal behaviour.
The first thing you can do is to create a new project with just the UISearchController functionality and see how it goes. You can edit your question with it so we'll have a better view.
There's a good example on how to implement UISearchController here: Sample-UISearchController
Adding:
-(void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.searchController.searchBar becomeFirstResponder];
}
to MasterViewController_TableResults.m gave the expected results and the keyboard popped up on launch on an iPad & iPhone with iOS 8.3.
You can go over that project and see what you did differently,
Edit:
Apparently if [self.searchController setActive:YES] is called before becomeFirstResponder the keyboard won't show. I wonder if that's a bug or not.
Had the same annoying issue.
You would think that by setting the SearchController as active would both present the the search controller and the keyboard. Unfortunately, it only does the first part.
My solution
in viewDidAppear make the Search Controller active:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
resultSearchController.active = true
}
once it is active, in didPresentSearchController make as first responder
func didPresentSearchController(searchController: UISearchController) {
searchController.searchBar.becomeFirstResponder()
}
Swift 3.0 (iOS 10) working solution:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
DispatchQueue.main.async { [unowned self] in
self.searchController.searchBar.becomeFirstResponder()
}
}
On iOS 9 I've found its sufficient to delay becomeFirstResponder() to the next run loop:
func focusSearchField() {
searchController?.active = true
// skipping to the next run loop is required, otherwise the keyboard does not appear
dispatch_async(dispatch_get_main_queue(), { [weak self] in
self?.searchController?.searchBar.becomeFirstResponder()
})
}
Working Solution:-
Don't use [self.searchController setActive:YES] before becomeFirstResponder.
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
// [self.searchController setActive:YES];
[self.searchController.searchBar becomeFirstResponder];
});
});
}
In iOS 10, I had to run the code in delegate method on main thread. First I set the active to YES in viewDidAppear,
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self.searchController setActive:YES];
}
and then in the delegate method:
- (void)didPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
[searchController.searchBar becomeFirstResponder];
});
}
The solution that will work is as follows :
1.Override ViewDidLayoutSubviews in the view controller in which you are showing UISearchController
2.Override ViewDidLayoutSubviews and inside it make search bar first responder.
Tested it on iOS > 9.0
Caution : Put a null check before making it First responder as follows
if((searchController != null)&&(searchController.SearchBar != null))
searchController.SearchBar.BecomeFirstResponder();
This is because ViewDidLayoutSubviews also gets called when cancel button is pressed.
This worked for me in Xamarin.
I had trouble with an UISearchBar not displaying the keyboard when doing
[searchBar becomeFirstResponder];
By searching on the net, i found this thread on the Apple developer website
that helped me to discover that the keyboard won't open if you don't have a keyWindow.
The application i work on do something like this :
Window A (KeyWindow)
do some things
open Window B (KeyWindow)
do some things
close Window B (resign KeyWindow)
I just had to do
[[[[UIApplication sharedApplication] windows] firstObject] makeKeyWindow];
after the resigning of window B and no more trouble with the keyboard.
This might also be related to Simulator Settings. Just disable Hardware -> Keyboard -> "Connect Hardware Keyboard" .
For further details: UISearchBar not showing keyboard when tapped
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.searchController setActive:YES];
}
//and then in the delegate method:
- (void)didPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
[searchController.searchBar becomeFirstResponder];
});
}
//The above works for me in addition to this I had to add:
-(void)viewWillDisappear:(BOOL)animated {
[searchController setActive:NO];
}

UITextView selectAll method not working as expected

I'm creating an iOS 8 app with Xcode 6.0.1 for my iPhone 5 (which has iOS 8.0.2 on it). I want to make it so that when a user clicks on my UITextView, all the text gets selected so he can easily start typing and erase what was there (but I don't want the text to be automatically erased because the user may want to keep it or append to it). To do this, I have the following code:
- (void)textViewDidBeginEditing:(UITextView *)textView {
if ([textView hasText]) {
NSLog(#"selectedRange before: %d", textView.selectedRange.length);
[textView selectAll:self];
NSLog(#"selectedRange after: %d", textView.selectedRange.length);
}
}
When this method gets called, the console output is what I expect (i.e. the selectedRange length is the same as the number of characters in the textView's text). However, nothing shows up as selected in the UITextView and it doesn't act selected (i.e. no selection menu pops up).
I have seen multiple questions like this on the internet, but none of the provided solutions worked for me (and some of them wrote it off as a bug without providing any solution). Changing the sender id to something other than self (such as nil) did not help, and neither did it help to call [textView select:self] as one person suggested. I have also tried this code:
- (void)textViewDidBeginEditing:(UITextView *)textView {
if ([textView hasText]) {
UITextRange *range = [textView textRangeFromPosition:textView.beginningOfDocument toPosition:textView.endOfDocument];
[textView setSelectedTextRange:range];
}
}
But, it has the same problem.
Any suggestions?
This solution works too and does not require subclassing UITextView, just put this function on your delegate:
OBJECTIVE C -
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
dispatch_async(dispatch_get_main_queue(), ^{
[textView selectAll:nil];
});
return YES;
}
SWIFT 3 -
func textViewDidBeginEditing(_ textView: UITextView) {
DispatchQueue.main.async {
textView.selectAll(nil)
}
}
#brentvatne 's solution worked for me. Posting the Swift syntax so people can copy and paste in the future.
func textViewShouldBeginEditing(textView: UITextView) -> Bool {
dispatch_async(dispatch_get_main_queue()) {
textView.selectAll(nil)
}
return true
}
The best solution I've found for this issue so far is to create a custom UITextView (i.e. create a new class that extends UITextView) and then implement the selectAll method like this:
- (void)selectAll:(id)sender {
[super selectAll:sender];
UITextRange *selectionRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument];
[self performSelector:#selector(setSelectedTextRange:) withObject:selectionRange afterDelay:0.0];
}
Then when you use a text view, set its type to your custom text view type (in your code and in the storyboard). Now you can successfully call the selectAll method whenever you need to. I suppose this should work with UITextField too, but I haven't tried it yet.

Resources