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
UITapGestureRecognizer Programmatically trigger a tap in my view
提问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:
我认为您在这里有多种选择:
May be the simplest would be to send a
push event action
to 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];
You could use
UI automation tool
that is provided with XCode instruments. This blogexplains well how to automate your UI tests with script then.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.
回答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
init
method 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)