ios UITapGestureRecognizer 在我的视图中以编程方式触发点击

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14094691/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 21:40:48  来源:igfitidea点击:

UITapGestureRecognizer Programmatically trigger a tap in my view

iosuigesturerecognizer

提问by Matt H.

Edit: Updated to make question more obvious

编辑:更新使问题更明显

Edit 2: Made question more accurate to my real-world problem. I'm actually looking to take action if they tap anywhere EXCEPT in an on-screen text-field. Thus, I can't simply listen for events within the textfield, I need to know if they tapped anywherein the View.

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

I'm writing unit tests to assert that a certain action is taken when a gesture recognizer recognizes a tap within certain coordinates of my view. I want to know if I can programmatically create a touch (at specific coordinates) that will be handled by the UITapGestureRecognizer. I'm attempting to simulate the user interaction during a unit test.

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

The UITapGestureRecognizer is configured in Interface Builder

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

采纳答案by tiguero

I think you have multiple options here:

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

  1. May be the simplest would be to send a push event actionto your view but i don't think that what you really want since you want to be able to choose where the tap action occurs.

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

  2. You could use UI automation toolthat is provided with XCode instruments. This blogexplains well how to automate your UI tests with script then.

  3. There is this solutiontoo that explain how to synthesize touch events on the iPhone but make sure you only use those for unit tests. This sounds more like a hack to me and I will consider this solution as the last resort if the two previous points doesn't fulfill your need.

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

    [你的视图 sendActionsForControlEvents: UIControlEventTouchUpInside];

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

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

回答by Till

What you attempt to do is very hard (but not entirely impossible) while staying on the (iTunes-)legal path.

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



Let me first draft the rightway;

让我先起草正确的方法;

The proper way out for doing this is using UIAutomation. UIAutomation does exactly what you ask for, it simulates user behaviour for all kinds of tests.

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



Now that hard way;

现在那艰难的道路;

The issue that your problems boils down to is to instantiate a new UIEvent. (Un)fortunately UIKit does not offer any constructors for such events due to obvious security reasons. There are however workarounds that did work in the past, not sure if they still do.

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

Have a look at Matt Galagher's awesome blog drafting a solution on how to synthesise touch events.

看看 Matt Galagher 的精彩博客,它起草了一个关于如何合成触摸事件解决方案

回答by Ondrej Rafaj

If used in tests you can use either a test library called SpecToolswhich helps with all this and more or use it's code directly:

如果在测试中使用,您可以使用名为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)
        }
    }

}

Both, library as well as the snippet use private API's and will probably cause a rejection if used outside of your test suite ...

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

回答by DCDC

There is a much simpler way to trigger a touch for a UITapGestureRecognizer in a unit test using a single line. Assuming you have a var that holds a reference to the tap gesture recognizer all you need is the following:

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

singleTapGestureRecognizer?.state = .ended

回答by Glauco Aquino

I was facing the same issue, trying to simulate a tap on a table cell to automate a test for a view controller which handles tapping on a table.

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

The controller has a private UITapGestureRecognizer created as below:

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

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

The unit test should simulate a touch so that the gestureRecognizer would trigger the action as it was originated from the user interaction.

单元测试应模拟触摸,以便gestureRecognizer 触发源自用户交互的动作。

None of the proposed solutions worked in this scenario, so I solved it decorating UITapGestureRecognizer, faking the exact methods called by the controller. So I added a "performTap" method that call the action in a way the controller itself is unaware of where the action is originated from. This way, I could make a test unit for the controller independent of the gesture recognizer, just of the action triggered.

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

This is my category, hope it 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

回答by tooluser

Okay, I've turned the above into a category that works.

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

Interesting bits:

有趣的位:

  • Categories can't add member variables. Anything you add becomes static to the class and thus is clobbered by Apple's many UITapGestureRecognizers.
    • So, use associated_object to make the magic happen.
    • NSValue for storing non-objects
  • Apple's initmethod contains important configuration logic; we could guess at what is set (number of taps, number of touches, what else?
    • But this is doomed. So, we swizzle in our init method that preserves the mocks.
  • 类别不能添加成员变量。您添加的任何内容都将成为类的静态内容,因此会被 Apple 的许多 UITapGestureRecognizers 破坏。
    • 所以,使用 associated_object 来让魔法发生。
    • NSValue 用于存储非对象
  • Apple 的init方法包含重要的配置逻辑;我们可以猜测设置了什么(点击次数,触摸次数,还有什么?
    • 但这是注定的。因此,我们在保留模拟的 init 方法中混用。

The header file is trivial; here's 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

回答by kevinl

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

should be

应该

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

because the cgpoint should be retrieved from exactly where the gesture target is rather than trying to guess where in the view it's in

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

回答by Vlad

Answer by @Ondrej updated to Swift 4:

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

}

Usage:

用法:

struct Automator {

    static func tap(view: UIView) {
        let grs = view.gestureRecognizers?.compactMap { ##代码## as? UITapGestureRecognizer } ?? []
        grs.forEach { ##代码##.sendActions() }
    }
}


let myView = ... // View under UI Logic Test
Automator.tap(view: myView)