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.
I am working on a photography app that allow photos to be taken in portrait or landscape. Due to the requirements of the project, I cannot let the device orientation autorotate, but rotation does need to be supported.
When using the following orientation methods:
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if self.orientation == .Landscape {
return UIInterfaceOrientationMask.LandscapeRight
} else {
return UIInterfaceOrientationMask.Portrait
}
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
if self.orientation == .Landscape {
return UIInterfaceOrientation.LandscapeRight
} else {
return UIInterfaceOrientation.Portrait
}
}
I am able to set rotation correctly at launch. By changing the orientation value and calling UIViewController.attemptRotationToDeviceOrientation() I am able to support rotation to the new desired interface. However, this rotation only occurs when the user actually moves their device. I need it to happen automatically.
I am able to call: UIDevice.currentDevice().setValue(targetOrientation.rawValue, forKey: "orientation") to force the change, but that causes other side effects because UIDevice.currentDevice().orientation only returns the setValue from that point on. (and it's extremely dirty)
Is there something I'm missing? I've looked into closing and launching a new view controller, but that has other issues such as a constant UI glitch when dismissing and immediately presenting a new view controller.
EDIT:
The following methods did not work for me:
Trick preferredInterfaceOrientationForPresentation to fire on viewController change
Forcing UIInterfaceOrientation changes on iPhone
EDIT 2:
Thoughts on potential solutions:
set orientation directly (with setValue) and deal with all the side effects this presents on iOS 9 (not acceptable)
I can use the current solution and indicate that the user needs to rotate the device. Once the device has been physically rotated, the UI rotates and then locks in place correctly. (poor UI)
I can find a solution that forces the refresh of orientation and rotates without physical action. (what I'm asking about, and looking for)
Do it all by hand. I can lock the interface in portrait or landscape, and manually rotate and resize the container view. This is 'dirty' because it forgoes all of the size class autolayout features and causes much heavier code. I am trying to avoid this.
I was able to find a solution with the assistance of this answer: Programmatic interface orientation change not working for iOS
My base orientation logic is as follows:
// Local variable to tracking allowed orientation. I have specific landscape and
// portrait targets and did not want to remember which I was supporting
enum MyOrientations {
case Landscape
case Portrait
}
var orientation: MyOrientations = .Landscape
// MARK: - Orientation Methods
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if self.orientation == .Landscape {
return UIInterfaceOrientationMask.LandscapeRight
} else {
return UIInterfaceOrientationMask.Portrait
}
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
if self.orientation == .Landscape {
return UIInterfaceOrientation.LandscapeRight
} else {
return UIInterfaceOrientation.Portrait
}
}
// Called on region and delegate setters
func refreshOrientation() {
if let newOrientation = self.delegate?.getOrientation() {
self.orientation = newOrientation
}
}
Then when I want to refresh the orientation, I do the following:
// Correct Orientation
let oldOrientation = self.orientation
self.refreshOrientation()
if self.orientation != oldOrientation {
dispatch_async(dispatch_get_main_queue(), {
self.orientationRefreshing = true
let vc = UIViewController()
UIViewController.attemptRotationToDeviceOrientation()
self.presentViewController(vc, animated: false, completion: nil)
UIView.animateWithDuration(0.3, animations: {
vc.dismissViewControllerAnimated(false, completion: nil)
})
})
}
This solution has the side effect of causing view[Will/Did]Appear and view[Will/Did]Disappear to fire all at once. I'm using the local orientationRefreshing variable to manage what aspects of those methods are called again.
I've encountered this exact problem in the past, myself. I was able to solve it using a simple work around (and GPUImage). My code is in Objective-C but i'm sure you'll have no problem translating it to Swift.
I began by setting the project's supported rotations to all that I hoped to support and then overriding the same UIViewController methods:
-(BOOL)shouldAutorotate {
return TRUE;
}
-(NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
Which allows the device to rotate but will persist in Portrait mode. Then began observing for Device rotations:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(adjustForRotation:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
Then updated the UI if the device was in Portrait mode or landscape:
-(void)adjustForRotation:(NSNotification*)notification
{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
switch (orientation) {
case UIDeviceOrientationLandscapeLeft:
{
// UPDATE UI
}
break;
case UIDeviceOrientationLandscapeRight:
{
// UPDATE UI
}
break;
default: // All other orientations - Portrait, Upside Down, Unknown
{
// UPDATE UI
}
break;
}
}
And finally, GPUImage rendered the image based on the device's orientation.
[_gpuImageStillCamera capturePhotoAsImageProcessedUpToFilter:last_f
withCompletionHandler:^(UIImage *processedImage, NSError *error) {
// Process the processedImage
}];
So I looked at the private headers of UIDevice, and it appears that there are two setters, and two property definitions, for orientation, which is currently baffling me. This is what I saw...
#property (nonatomic) int orientation;
#property (nonatomic, readonly) int orientation;
- (void)setOrientation:(int)arg1;
- (void)setOrientation:(int)arg1 animated:(BOOL)arg2;
So when I saw that you used setValue:forKey:, I wanted to see there was a synthesized setter and getter, and am honestly not 100% sure as to which one is being set, and which one is being acknowledged by the device... I attempted in a demo app to use setValue:forKey: to no avail, but used this trick from one of my past applications, and it did the trick right away :) I hope this helps
UIDevice.currentDevice().performSelector(Selector("setOrientation:"), withObject: UIInterfaceOrientation.Portrait.rawValue)
I've ran into quite an unfortunate bug in iOS 9. It seems that when you set a UITextField.inputAccessoryView, that view's viewWillDisappear: and viewDidDisappear: methods are called prematurely (right when the keyboard finishes animating up).
I've included a gif to demonstrate the issue. When the view turns red is when its viewWillDisappear: method has been called. Oddly when you dismiss the keyboard, viewWillDisappear: and viewDidDisappear: are called again. However, viewWillAppear: is only called once.
Has anyone run into a similar issue? I use viewWillDisappear: and viewDidDisappear: to wind down the controller, and obviously an early call is causing unwanted behaviour.
Note: Below is how I create and set the accessory view. Nothing notable in AccessoryViewController.m. Reproduced the issue in a clean project. And it is not present on iOS 8.
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (self.textField.inputAccessoryView == nil) {
self.textField.inputAccessoryView = self.vc.view;
}
return YES;
}
- (UIViewController *)vc {
if (!_vc) _vc = [[AccessoryViewController alloc] init];
return _vc;
}
The AccessoryViewController is not stored strongly on the ViewController. Store it in an instance variable so it does not get cleaned up.
My solution (Swift):
var accessoryView: AccessoryViewController! // works
vs
weak var accessoryView: AccessoryViewController!
I came across a strange bug in my app:
The setup
A simple Master-Detail app, iPhone style (ie. no split view, no popover, just a navigation controller, a table view controller, and a view controller).
The bug
Touch a "background" part of the table view (the darker grey parts on my screenshot) like a section header or footer.
While keeping your finger on the screen, touch a cell multiple times.
Release all fingers. The "detail" view will pushed normally, but when touching the back button, you will find that the detail view was stacked as many times as you touched the cell at step 2.
You can also touch multiple cells at step 2 and their destination views will be stacked in the correct order :)
Reproduce it
I was able to reproduce the bug with a clean, freshly created app, and on the last release of the Twitter app for iPhone (by touching the "Loading" label with finger #1 and touching a tweet multiple times).
However, I could not trigger the same behaviour in the Settings app, under the "General" tab (which is a grouped table view).
The bug was reproduced on iOS 6.0 and 6.1. I don't have devices with older versions to test.
Question
Is this a known trick when creating navigation/table view based apps and if so is there a solution to prevent this (weird) behavior ? Or is this an iOS bug (and if so, is it already known from Apple) ?
A possible stop-gap measure you could use is to implement
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
And use a boolean flag or something to indicate that you are currently trying to execute that segue. ex:
BOOL doingSegue = NO;
-(void) viewWillAppear
{
doingSegue = NO;
}
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
if ( [identifier isEqualToString:#"MySegueIdentifier"] )
{
if ( doingSegue )
{
return NO;
}
else
{
doingSegue = YES;
return YES;
}
}
return YES;
}
Swift Version
var doingSegue = false
override func viewWillAppear(_ animated: Bool) {
doingSegue = false
}
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "MySegueIdentifier" {
if doingSegue {
return false
}
else {
doingSegue = true
return true
}
}
return true
}
This is fixed by Apple in iOS 7.
For prior versions of the OS, Dan F's answer should do the trick.
Let's say we have a view controller with one sub view. the subview takes up the center of the screen with 100 px margins on all sides. We then add a bunch of little stuff to click on inside that subview. We are only using the subview to take advantage of the new frame ( x=0, y=0 inside the subview is actually 100,100 in the parent view).
Then, imagine that we have something behind the subview, like a menu. I want the user to be able to select any of the "little stuff" in the subview, but if there is nothing there, I want touches to pass through it (since the background is clear anyway) to the buttons behind it.
How can I do this? It looks like touchesBegan goes through, but buttons don't work.
Create a custom view for your container and override the pointInside: message to return false when the point isn't within an eligible child view, like this:
Swift:
class PassThroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
return true
}
}
return false
}
}
Objective C:
#interface PassthroughView : UIView
#end
#implementation PassthroughView
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *view in self.subviews) {
if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
return YES;
}
return NO;
}
#end
Using this view as a container will allow any of its children to receive touches but the view itself will be transparent to events.
I also use
myView.userInteractionEnabled = NO;
No need to subclass. Works fine.
From Apple:
Event forwarding is a technique used by some applications. You forward touch events by invoking the event-handling methods of another responder object. Although this can be an effective technique, you should use it with caution. The classes of the UIKit framework are not designed to receive touches that are not bound to them .... If you want to conditionally forward touches to other responders in your application, all of these responders should be instances of your own subclasses of UIView.
Apples Best Practise:
Do not explicitly send events up the responder chain (via nextResponder); instead, invoke the superclass implementation and let the UIKit handle responder-chain traversal.
instead you can override:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
in your UIView subclass and return NO if you want that touch to be sent up the responder chain (I.E. to views behind your view with nothing in it).
A far simpler way is to "Un-Check" User Interaction Enabled in the interface builder. "If you are using a storyboard"
Lately I wrote a class that will help me with just that. Using it as a custom class for a UIButton or UIView will pass touch events that were executed on a transparent pixel.
This solution is a somewhat better than the accepted answer because you can still click a UIButton that is under a semi transparent UIView while the non transparent part of the UIView will still respond to touch events.
As you can see in the GIF, the Giraffe button is a simple rectangle but touch events on transparent areas are passed on to the yellow UIButton underneath.
Link to class
Top voted solution was not fully working for me, I guess it was because I had a TabBarController into the hierarchy (as one of the comments points out) it was in fact passing along touches to some parts of the UI but it was messing with my tableView's ability to intercept touch events, what finally did it was overriding hitTest in the view I want to ignore touches and let the subviews of that view handle them
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view == self) {
return nil; //avoid delivering touch events to the container view (self)
}
else{
return view; //the subviews will still receive touch events
}
}
Building on what John posted, here is an example that will allow touch events to pass through all subviews of a view except for buttons:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// Allow buttons to receive press events. All other views will get ignored
for( id foundView in self.subviews )
{
if( [foundView isKindOfClass:[UIButton class]] )
{
UIButton *foundButton = foundView;
if( foundButton.isEnabled && !foundButton.hidden && [foundButton pointInside:[self convertPoint:point toView:foundButton] withEvent:event] )
return YES;
}
}
return NO;
}
Swift 3
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if subview.frame.contains(point) {
return true
}
}
return false
}
According to the 'iPhone Application Programming Guide':
Turning off delivery of touch events.
By default, a view receives touch
events, but you can set its userInteractionEnabled property to NO
to turn off delivery of events. A view also does not receive events if it’s hidden
or if it’s transparent.
http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html
Updated: Removed example - reread the question...
Do you have any gesture processing on the views that may be processing the taps before the button gets it? Does the button work when you don't have the transparent view over it?
Any code samples of non-working code?
As far as I know, you are supposed to be able to do this by overriding the hitTest: method. I did try it but could not get it to work properly.
In the end I created a series of transparent views around the touchable object so that they did not cover it. Bit of a hack for my issue this worked fine.
Taking tips from the other answers and reading up on Apple's documentation, I created this simple library for solving your problem:
https://github.com/natrosoft/NATouchThroughView
It makes it easy to draw views in Interface Builder that should pass touches through to an underlying view.
I think method swizzling is overkill and very dangerous to do in production code because you are directly messing with Apple's base implementation and making an application-wide change that could cause unintended consequences.
There is a demo project and hopefully the README does a good job explaining what to do. To address the OP, you would change the clear UIView that contains the buttons to class NATouchThroughView in Interface Builder. Then find the clear UIView that overlays the menu that you want to be tap-able. Change that UIView to class NARootTouchThroughView in Interface Builder. It can even be the root UIView of your view controller if you intend those touches to pass through to the underlying view controller. Check out the demo project to see how it works. It's really quite simple, safe, and non-invasive
I created a category to do this.
a little method swizzling and the view is golden.
The header
//UIView+PassthroughParent.h
#interface UIView (PassthroughParent)
- (BOOL) passthroughParent;
- (void) setPassthroughParent:(BOOL) passthroughParent;
#end
The implementation file
#import "UIView+PassthroughParent.h"
#implementation UIView (PassthroughParent)
+ (void)load{
Swizz([UIView class], #selector(pointInside:withEvent:), #selector(passthroughPointInside:withEvent:));
}
- (BOOL)passthroughParent{
NSNumber *passthrough = [self propertyValueForKey:#"passthroughParent"];
if (passthrough) return passthrough.boolValue;
return NO;
}
- (void)setPassthroughParent:(BOOL)passthroughParent{
[self setPropertyValue:[NSNumber numberWithBool:passthroughParent] forKey:#"passthroughParent"];
}
- (BOOL)passthroughPointInside:(CGPoint)point withEvent:(UIEvent *)event{
// Allow buttons to receive press events. All other views will get ignored
if (self.passthroughParent){
if (self.alpha != 0 && !self.isHidden){
for( id foundView in self.subviews )
{
if ([foundView alpha] != 0 && ![foundView isHidden] && [foundView pointInside:[self convertPoint:point toView:foundView] withEvent:event])
return YES;
}
}
return NO;
}
else {
return [self passthroughPointInside:point withEvent:event];// Swizzled
}
}
#end
You will need to add my Swizz.h and Swizz.m
located Here
After that, you just Import the UIView+PassthroughParent.h in your {Project}-Prefix.pch file, and every view will have this ability.
every view will take points, but none of the blank space will.
I also recommend using a clear background.
myView.passthroughParent = YES;
myView.backgroundColor = [UIColor clearColor];
EDIT
I created my own property bag, and that was not included previously.
Header file
// NSObject+PropertyBag.h
#import <Foundation/Foundation.h>
#interface NSObject (PropertyBag)
- (id) propertyValueForKey:(NSString*) key;
- (void) setPropertyValue:(id) value forKey:(NSString*) key;
#end
Implementation File
// NSObject+PropertyBag.m
#import "NSObject+PropertyBag.h"
#implementation NSObject (PropertyBag)
+ (void) load{
[self loadPropertyBag];
}
+ (void) loadPropertyBag{
#autoreleasepool {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Swizz([NSObject class], NSSelectorFromString(#"dealloc"), #selector(propertyBagDealloc));
});
}
}
__strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag
- (id) propertyValueForKey:(NSString*) key{
return [[self propertyBag] valueForKey:key];
}
- (void) setPropertyValue:(id) value forKey:(NSString*) key{
[[self propertyBag] setValue:value forKey:key];
}
- (NSMutableDictionary*) propertyBag{
if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:#"%p",self]];
if (propBag == nil){
propBag = [NSMutableDictionary dictionary];
[self setPropertyBag:propBag];
}
return propBag;
}
- (void) setPropertyBag:(NSDictionary*) propertyBag{
if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
[_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:#"%p",self]];
}
- (void)propertyBagDealloc{
[self setPropertyBag:nil];
[self propertyBagDealloc];//Swizzled
}
#end
Try set a backgroundColor of your transparentView as UIColor(white:0.000, alpha:0.020). Then you can get touch events in touchesBegan/touchesMoved methods. Place the code below somewhere your view is inited:
self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true
Try this
class PassthroughToWindowView: UIView {
override func test(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var view = super.hitTest(point, with: event)
if view != self {
return view
}
while !(view is PassthroughWindow) {
view = view?.superview
}
return view
}
}
I use that instead of override method point(inside: CGPoint, with: UIEvent)
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.point(inside: point, with: event) else { return nil }
return self
}
If you can't bother to use a category or subclass UIView, you could also just bring the button forward so that it is in front of the transparent view. This won't always be possible depending on your application, but it worked for me. You can always bring the button back again or hide it.