UITapGestureRecognizer以编程方式触发我视图中的点按

时间:2012-12-30 21:11:03

标签: ios uigesturerecognizer

修改:更新以使问题更加明显

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

8 个答案:

答案 0 :(得分:10)

我认为你有多种选择:

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

      

    [yourView sendActionsForControlEvents:UIControlEventTouchUpInside];

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

  3. 还有这个solution解释了如何在iPhone上合成触摸事件,但确保只使用它们进行单元测试。这听起来更像是对我的破解,如果之前的两点不能满足您的需要,我会将此解决方案视为最后的手段。

答案 1 :(得分:4)

在单元测试中,使用单行触发UITapGestureRecognizer触摸的方法要简单得多。假设您有一个变量,其中包含对轻击手势识别器的引用,那么您所需要做的就是:

singleTapGestureRecognizer?.state = .ended

答案 2 :(得分:3)

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


让我先草拟方式;

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


现在很难;

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

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

答案 3 :(得分: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)
        }
    }

}

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

答案 4 :(得分:2)

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

控制器有一个私有的UITapGestureRecognizer,如下所示:

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

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

所提出的解决方案都没有在这种情况下工作,因此我解决了它装饰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

答案 5 :(得分:2)

好的,我已将上述内容转变为可行的类别。

有趣的位:

  • 类别无法添加成员变量。你添加的任何东西都会变成静态的,因此被Apple的许多UITapGestureRecognizers所破坏。
    • 所以,使用associated_object让神奇的事情发生。
    • 用于存储非对象的NSValue
  • Apple的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

答案 6 :(得分:2)

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

应该是

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

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

答案 7 :(得分:0)

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