【问题标题】:UITapGestureRecognizer Programmatically trigger a tap in my viewUITapGestureRecognizer 以编程方式在我的视图中触发点击
【发布时间】:2012-12-15 04:55:32
【问题描述】:

编辑:更新以使问题更明显

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

我正在编写单元测试来断言当手势识别器在我的视图的特定坐标内识别到点击时会执行特定操作。我想知道我是否可以以编程方式创建将由 UITapGestureRecognizer 处理的触摸(在特定坐标处)。 我正在尝试在单元测试期间模拟用户交互。

UITapGestureRecognizer 在 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 ]]
}

【问题讨论】:

  • 点击特定视图或按钮就足够了吗?
  • 手势识别器正在识别整个视图上的点击-然后确定该点击是否发生在特定文本字段内-但是,如果他们点击该文本字段外,我想采取行动,所以只听那个文本字段上的点击会破坏目的。我可能应该在问题中做出这种区分。
  • 更新了一个问题以进行区分。我最初将其排除在外是因为我不想发布太长的帖子——但你说得对,这是一个重要的区别。

标签: ios uigesturerecognizer


【解决方案1】:

有一种更简单的方法可以在单元测试中使用单行触发 UITapGestureRecognizer 的触摸。假设您有一个包含对点击手势识别器的引用的 var,您所需要的只是以下内容:

singleTapGestureRecognizer?.state = .ended

【讨论】:

    【解决方案2】:

    @Ondrej 的回答已更新到 Swift 4:

    // Return type alias
    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
        func getTargetInfo() -> TargetActionInfo {
            guard let targets = value(forKeyPath: "_targets") as? [NSObject] else {
                return []
            }
            var targetsInfo: TargetActionInfo = []
            for target in targets {
                // Getting selector by parsing the description string of a UIGestureRecognizerTarget
                let description = String(describing: target).trimmingCharacters(in: CharacterSet(charactersIn: "()"))
                var selectorString = description.components(separatedBy: ", ").first ?? ""
                selectorString = selectorString.components(separatedBy: "=").last ?? ""
                let selector = NSSelectorFromString(selectorString)
    
                // Getting target from iVars
                if let targetActionPairClass = NSClassFromString("UIGestureRecognizerTarget"),
                    let targetIvar = class_getInstanceVariable(targetActionPairClass, "_target"),
                    let targetObject = object_getIvar(target, targetIvar) {
                    targetsInfo.append((target: targetObject as AnyObject, action: selector))
                }
            }
    
            return targetsInfo
        }
    
        /// Executes all targets on a gesture recognizer
        func sendActions() {
            let targetsInfo = getTargetInfo()
            for info in targetsInfo {
                info.target.performSelector(onMainThread: info.action, with: self, waitUntilDone: true)
            }
        }
    
    }
    

    用法:

    struct Automator {
    
        static func tap(view: UIView) {
            let grs = view.gestureRecognizers?.compactMap { $0 as? UITapGestureRecognizer } ?? []
            grs.forEach { $0.sendActions() }
        }
    }
    
    
    let myView = ... // View under UI Logic Test
    Automator.tap(view: myView)
    

    【讨论】:

      【解决方案3】:

      如果在测试中使用,您可以使用名为 SpecTools 的测试库来帮助解决所有这些问题,或者直接使用它的代码:

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

      库和 sn-p 都使用私有 API,如果在测试套件之外使用可能会导致拒绝...

      【讨论】:

        【解决方案4】:
        CGPoint tapPoint = [gesture locationInView:self.view];
        

        应该是

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

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

        【讨论】:

          【解决方案5】:

          好的,我已将上述内容转换为有效的类别。

          有趣的位:

          • 类别不能添加成员变量。你添加的任何东西都会变成静态的类,因此会被 Apple 的许多 UITapGestureRecognizers 破坏。
            • 所以,请使用 associated_object 来创造奇迹。
            • NSValue 用于存储非对象
          • 苹果的init方法包含重要的配置逻辑;我们可以猜测设置了什么(点击次数,触摸次数,还有什么?
            • 但这是注定的。因此,我们在保留模拟的 init 方法中进行调配。

          头文件很琐碎;这是实现。

          #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
          

          【讨论】:

          • 我可以在 AppStore 应用上使用它吗?
          • 击败我。试一试!但是,您需要在应用程序中执行的任何操作都应该能够在没有额外的伪装层的情况下执行。
          • 现在还能用吗?我试过了,但没有反应
          【解决方案6】:

          我遇到了同样的问题,试图模拟一个表格单元格上的点击,以自动测试处理点击表格的视图控制器。

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

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

          单元测试应模拟触摸,以便手势识别器触发动作,因为它源自用户交互。

          建议的解决方案在这种情况下都不起作用,所以我解决了它装饰 UITapGestureRecognizer,伪造控制器调用的确切方法。因此,我添加了一个“performTap”方法,该方法以控制器本身不知道该动作来自何处的方式调用该动作。这样,我可以为控制器创建一个独立于手势识别器的测试单元,只需要触发的动作。

          这是我的分类,希望对大家有所帮助。

          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 个手势识别器时,您将无法依赖它们。如果有任何其他字段等,就会出现这种情况,因为 Apple 在内部使用它们。该问题的解决方案是使用关联对象来伪造属性。如果有兴趣,我会发布该解决方案。
          【解决方案7】:

          我认为您在这里有多种选择:

          1. 可能最简单的方法是将push event action 发送到您的视图,但我认为这不是您真正想要的,因为您希望能够选择点击操作发生的位置。

            [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

          2. 您可以使用 XCode 工具随附的 UI automation tool。这个blog 很好地解释了如何使用脚本自动化你的 UI 测试。

          3. 还有这个solution 解释了如何在 iPhone 上合成触摸事件,但请确保仅将它们用于单元测试。这对我来说听起来更像是一种 hack,如果前两点不能满足您的需求,我会考虑将此解决方案作为最后的手段。

          【讨论】:

          • 选项 1 无效。 “sendActionsForControlEvents”仅适用于 UIControl 对象;不是 UIView 的。
          【解决方案8】:

          在保持在 (iTunes-) 合法路径上时,您尝试做的事情非常困难(但并非完全不可能)。


          让我先草拟一下正确的方式;

          这样做的正确方法是使用 UIAutomation。 UIAutomation 完全符合您的要求,它模拟各种测试的用户行为。


          现在很难;

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

          看看 Matt Galagher 撰写的精彩博客solution on how to synthesise touch events

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-06-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-03-18
            相关资源
            最近更新 更多