UITapGestureRecognizer Programs a trigger in my view

Edit: Updated to make the question more obvious.

Edit 2: Made the question more accurate for my real problem. I'm actually looking for actions if they click EXCEPT anywhere in the text box on the screen. That way, I can't just listen for events in the text box, I need to know if they will be used anywhere in the view.

I am writing unit tests to claim that a certain action is taken when the gesture recognizer recognizes a touch in certain coordinates of my view. I want to know if I can programmatically create a touch (at certain coordinates) that will be processed by UITapGestureRecognizer. I am trying to simulate user interaction during a unit test.

UITapGestureRecognizer configured in Interface Builder

//MYUIViewControllerSubclass.m -(IBAction)viewTapped:(UITapGestureRecognizer*)gesture { CGPoint tapPoint = [gesture locationInView:self.view]; if (!CGRectContainsPoint(self.textField, tapPoint)) { // Do stuff if they tapped anywhere outside the text field } } //MYUIViewControllerSubclassTests.m //What I'm trying to accomplish in my unit test: -(void)testThatTappingInNoteworthyAreaTriggersStuff { // Create fake gesture recognizer and ViewController MYUIViewControllerSubclass *vc = [[MYUIViewControllersSubclass alloc] init]; UITapGestureRecognizer *tgr = [[UITapGestureRecognizer initWithView: vc.view]; // What I want to do: [[ Simulate A Tap anywhere outside vc.textField ]] [[ Assert that "Stuff" occured ]] } 
+13
ios uigesturerecognizer
Dec 30
source share
6 answers

I think you have several options:

  • It might be easiest to send a push event action to your view, but I don’t think you really want to, since you want to choose where the action of the tap occurs.

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

  • You can use the UI automation tool , which is equipped with Xcode tools. This blog explains how to automate your user interface tests with a script.

  • There is also a solution that explains how to synthesize touch events on the iPhone, but make sure that you use them only for unit tests. It sounds more like hacking me, and I will consider this solution as a last resort if the two previous points do not match your needs.

+9
Dec 30 '12 at 22:44
source share

When used in tests, you can use either a test library called SpecTools , which helps with all this and more, or use it directly in the code

 // Return type alias public typealias TargetActionInfo = [(target: AnyObject, action: Selector)] // UIGestureRecognizer extension extension UIGestureRecognizer { // MARK: Retrieving targets from gesture recognizers /// Returns all actions and selectors for a gesture recognizer /// This method uses private API and will most likely cause your app to be rejected if used outside of your test target /// - Returns: [(target: AnyObject, action: Selector)] Array of action/selector tuples public func getTargetInfo() -> TargetActionInfo { var targetsInfo: TargetActionInfo = [] if let targets = self.value(forKeyPath: "_targets") as? [NSObject] { for target in targets { // Getting selector by parsing the description string of a UIGestureRecognizerTarget let selectorString = String.init(describing: target).components(separatedBy: ", ").first!.replacingOccurrences(of: "(action=", with: "") let selector = NSSelectorFromString(selectorString) // Getting target from iVars let targetActionPairClass: AnyClass = NSClassFromString("UIGestureRecognizerTarget")! let targetIvar: Ivar = class_getInstanceVariable(targetActionPairClass, "_target") let targetObject: AnyObject = object_getIvar(target, targetIvar) as! AnyObject targetsInfo.append((target: targetObject, action: selector)) } } return targetsInfo } /// Executes all targets on a gesture recognizer public func execute() { let targetsInfo = self.getTargetInfo() for info in targetsInfo { info.target.performSelector(onMainThread: info.action, with: nil, waitUntilDone: true) } } } 

Both libraries, as well as the fragment, use a private API and are likely to cause a rejection if they are used outside of your test suite ...

+3
Sep 11 '17 at 21:56 on
source share

Ok, I turned this into a category that works.

Interesting bits:

  • Categories cannot add member variables. Everything that you add becomes static for the class and thus gets strayed by Apple by many UITapGestureRecognizers.
    • So, use bound_object to do the magic.
    • NSValue for storing non objects
  • The Apple init method contains important configuration logic; we can guess what is installed (number of taps, number of touches, what else?
    • But it is doomed. So we swizzle in our init method, which saves mocks.

The header file is trivial; here is the implementation.

 #import "UITapGestureRecognizer+Spec.h" #import "objc/runtime.h" /* * With great contributions from Matt Gallagher (http://www.cocoawithlove.com/2008/10/synthesizing-touch-event-on-iphone.html) * And Glauco Aquino (http://stackoverflow.com/users/2276639/glauco-aquino) * And Codeshaker (http://codeshaker.blogspot.com/2012/01/calling-original-overridden-method-from.html) */ @interface UITapGestureRecognizer (SpecPrivate) @property (strong, nonatomic, readwrite) UIView *mockTappedView_; @property (assign, nonatomic, readwrite) CGPoint mockTappedPoint_; @property (strong, nonatomic, readwrite) id mockTarget_; @property (assign, nonatomic, readwrite) SEL mockAction_; @end NSString const *MockTappedViewKey = @"MockTappedViewKey"; NSString const *MockTappedPointKey = @"MockTappedPointKey"; NSString const *MockTargetKey = @"MockTargetKey"; NSString const *MockActionKey = @"MockActionKey"; @implementation UITapGestureRecognizer (Spec) // It is necessary to call the original init method; super does not set appropriate variables. // (eg, number of taps, number of touches, gods know what else) // Swizzle our own method into its place. Note that Apple misspells 'swizzle' as 'exchangeImplementation'. +(void)load { method_exchangeImplementations(class_getInstanceMethod(self, @selector(initWithTarget:action:)), class_getInstanceMethod(self, @selector(initWithMockTarget:mockAction:))); } -(id)initWithMockTarget:(id)target mockAction:(SEL)action { self = [self initWithMockTarget:target mockAction:action]; self.mockTarget_ = target; self.mockAction_ = action; self.mockTappedView_ = nil; return self; } -(UIView *)view { return self.mockTappedView_; } -(CGPoint)locationInView:(UIView *)view { return [view convertPoint:self.mockTappedPoint_ fromView:self.mockTappedView_]; } //-(UIGestureRecognizerState)state { // return UIGestureRecognizerStateEnded; //} -(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point { self.mockTappedView_ = view; self.mockTappedPoint_ = point; // warning because a leak is possible because the compiler can't tell whether this method // adheres to standard naming conventions and make the right behavioral decision. Suppress it. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.mockTarget_ performSelector:self.mockAction_]; #pragma clang diagnostic pop } # pragma mark - Who says we can't add members in a category? - (void)setMockTappedView_:(UIView *)mockTappedView { objc_setAssociatedObject(self, &MockTappedViewKey, mockTappedView, OBJC_ASSOCIATION_ASSIGN); } -(UIView *)mockTappedView_ { return objc_getAssociatedObject(self, &MockTappedViewKey); } - (void)setMockTappedPoint_:(CGPoint)mockTappedPoint { objc_setAssociatedObject(self, &MockTappedPointKey, [NSValue value:&mockTappedPoint withObjCType:@encode(CGPoint)], OBJC_ASSOCIATION_COPY); } - (CGPoint)mockTappedPoint_ { NSValue *value = objc_getAssociatedObject(self, &MockTappedPointKey); CGPoint aPoint; [value getValue:&aPoint]; return aPoint; } - (void)setMockTarget_:(id)mockTarget { objc_setAssociatedObject(self, &MockTargetKey, mockTarget, OBJC_ASSOCIATION_ASSIGN); } - (id)mockTarget_ { return objc_getAssociatedObject(self, &MockTargetKey); } - (void)setMockAction_:(SEL)mockAction { objc_setAssociatedObject(self, &MockActionKey, NSStringFromSelector(mockAction), OBJC_ASSOCIATION_COPY); } - (SEL)mockAction_ { NSString *selectorString = objc_getAssociatedObject(self, &MockActionKey); return NSSelectorFromString(selectorString); } @end 
+2
May 9 '13 at 22:10
source share
 CGPoint tapPoint = [gesture locationInView:self.view]; 

it should be

 CGPoint tapPoint = [gesture locationInView:gesture.view]; 

because cgpoint should be taken from the place where the goal of the gesture is, and not try to guess where in the view it is in

+2
Nov 21 '13 at 23:00
source share

What you are trying to do is very difficult (but not entirely impossible), remaining on the legal path (iTunes-).




Let me first prepare the right path;

The right way out for this is to use UIAutomation. UIAutomation does exactly what you ask for, it models user behavior for all kinds of tests.




Now that it's hard

The problem with your problems is creating a new UIEvent. (Un), fortunately, UIKit does not offer any constructors for such events due to obvious security concerns. There are, however, workarounds that have worked in the past, but are not sure what they still do.

Take a look at Matt Galager's wonderful blog, which describes the decision on how to synthesize touch events .

+1
Dec 30
source share

I ran into the same problem trying to simulate a click on a table cell in order to automate a test for a view controller that handles clicking on a table.

The controller has its own UITapGestureRecognizer created as shown below:

 gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didRecognizeTapOnTableView)]; 

unit test should simulate a touch so that gestureRecognizer initiates an action because it was created from user interaction.

None of the proposed solutions worked in this scenario, so I decided that it decorates the UITapGestureRecognizer by creating precise methods called the controller. Therefore, I added a "performTap" method that calls the action in such a way that the controller itself does not know where this is coming from. That way, I could make a test block for the controller, regardless of the gesture recognizer, just the called action.

This is my category, hope this helps someone.

 CGPoint mockTappedPoint; UIView *mockTappedView = nil; id mockTarget = nil; SEL mockAction; @implementation UITapGestureRecognizer (MockedGesture) -(id)initWithTarget:(id)target action:(SEL)action { mockTarget = target; mockAction = action; return [super initWithTarget:target action:action]; // code above calls UIGestureRecognizer init..., but it doesn't matters } -(UIView *)view { return mockTappedView; } -(CGPoint)locationInView:(UIView *)view { return [view convertPoint:mockTappedPoint fromView:mockTappedView]; } -(UIGestureRecognizerState)state { return UIGestureRecognizerStateEnded; } -(void)performTapWithView:(UIView *)view andPoint:(CGPoint)point { mockTappedView = view; mockTappedPoint = point; [mockTarget performSelector:mockAction]; } @end 
+1
Apr 13 '13 at 4:39
source share



All Articles