UITapGestureRecognizer以编程方式触发我的视图中的一个水龙头

编辑:更新,使问题更加明显

编辑2:提出更准确的问题,我的现实世界的问题。 我实际上是希望采取行动,如果他们点击任何地方,除了在屏幕上的文本字段。 因此,我不能简单地听取文本框内的事件,我需要知道他们是否在视图中的任何地方点击。

我正在编写unit testing来断言当手势识别器在我的视图的特定坐标中识别出一个水龙头时会采取某个动作。 我想知道是否可以通过编程方式创build将由UITapGestureRecognizer处理的触摸(在特定坐标处)。 我试图在unit testing中模拟用户交互。

UITapGestureRecognizer是在Interface Builder中configuration的

//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 ]] } 

我想你在这里有多种select:

  1. 可能最简单的方法就是push event actionpush event action发送给您的视图,但我不认为您真正想要什么,因为您希望能够select点按操作发生的位置。

    [yourView sendActionsForControlEvents:UIControlEventTouchUpInside];

  2. 您可以使用XCode仪器提供的UI automation tool 。 这个博客解释了如何使用脚本自动化您的UItesting。

  3. 这个解决scheme也解释了如何在iPhone上综合触摸事件,但是确保你只使用那些unit testing。 这听起来更像是一个黑客,我会考虑这个解决scheme作为最后的手段,如果前两个点不能满足您的需要。

如果在testing中使用,您可以使用一个称为SpecTools的testing库,它可以帮助您处理所有这些事情,或者直接使用它的代码:

 // 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's 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) } } } 

库和代码片段都使用私有API,如果在testing套件之外使用,可能会导致拒绝。

好的,我把上面的内容变成了一个可行的类别。

有趣的比特:

  • 分类不能添加成员variables。 你添加的任何东西都会变成静态的,因此被Apple的许多UITapGestureRecognizer所破坏。
    • 所以,使用associated_object来使魔法发生。
    • 用于存储非对象的NSValue
  • 苹果的init方法包含重要的configuration逻辑; 我们可以猜测什么是设置(点击数,触摸点数,还有什么?
    • 但是这是注定的。 所以,我们在我们的初始化方法中调整,以保留模拟。

头文件是微不足道的; 这是实现。

 #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 
 CGPoint tapPoint = [gesture locationInView:self.view]; 

应该

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

因为cgpoint应该从手势目标的确切位置检索,而不是试图猜测它在视图中的位置

在(iTunes-)合法path上,您试图做的事情非常困难(但并非完全不可能)。


让我先草拟正确的方法;

正确的出路是使用UIAutomation。 UIAutomation正是你所要求的,它模拟了各种testing的用户行为。


现在这个艰难的方式;

问题归结为实例化一个新的UIEvent。 (Un)幸运的是由于明显的安全原因,UIKit不提供这样的事件的任何构造函数。 然而,过去有效的解决方法,不确定是否仍然有效。

看看Matt Galagher真棒博客撰写关于如何合成触摸事件的解决scheme 。

我面临着同样的问题,试图模拟表格单元上的一个龙头来自动化一个视图控制器的testing,该控制器处理在一个表上敲击。

控制器有一个私人的UITapGestureRecognizer创build如下:

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

unit testing应该模拟一个触摸,以便gestureRecognizer触发这个动作,因为它是从用户交互中发起的。

在这种情况下,所提出的解决scheme都没有工作,所以我解决了它的装饰UITapGestureRecognizer,伪造控制器调用的确切方法。 所以我添加了一个“performTap”方法,以一种控制器本身不知道动作起源于何处的方式来调用动作。 这样,我就可以独立于手势识别器,为控制器创build一个testing单元,就是触发的动作。

这是我的类别,希望它可以帮助别人。

 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