21

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

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

8 回答 8

9

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

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

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

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

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

于 2012-12-30T22:44:26.237 回答
9

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

singleTapGestureRecognizer?.state = .ended
于 2020-03-10T16:00:48.793 回答
4

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


让我先草拟正确的方法;

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


现在很难;

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

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

于 2012-12-30T22:58:36.380 回答
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,如果在测试套件之外使用,可能会导致拒绝......

于 2017-09-11T21:56:36.860 回答
3

@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)
于 2018-11-23T22:30:22.963 回答
2

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

控制器有一个私有的 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
于 2013-04-13T04:39:46.867 回答
2

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

有趣的位:

  • 类别不能添加成员变量。你添加的任何东西都会变成静态的类,因此会被 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
于 2013-05-09T22:10:04.163 回答
2
CGPoint tapPoint = [gesture locationInView:self.view];

应该

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

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

于 2013-11-21T23:00:52.273 回答