UITapGestureRecognizer Программирует триггер в моем представлении
Изменить: обновлено, чтобы сделать вопрос более очевидным
Изменить 2: Сделал вопрос более точным для моей реальной проблемы. Я на самом деле ищут действия, если они нажимают в любом месте EXCEPT в текстовом поле на экране. Таким образом, я не могу просто прослушивать события в текстовом поле, мне нужно знать, если они будут использоваться в любом месте в представлении.
Я пишу модульные тесты, чтобы утверждать, что определенное действие предпринимается, когда распознаватель жестов распознает касание в определенных координатах моего представления. Я хочу знать, могу ли я программно создать прикосновение (при определенных координатах), которое будет обрабатываться UITapGestureRecognizer. Я пытаюсь имитировать взаимодействие пользователя во время unit test.
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 ]]
}
Ответы
Ответ 1
Я думаю, у вас есть несколько вариантов:
-
Может быть самым простым было бы отправить push event action
на ваш просмотр, но я не думаю, что вы действительно хотите, так как вы хотите выбрать, где происходит действие крана.
[yourView sendActionsForControlEvents: UIControlEventTouchUpInside];
-
Вы можете использовать UI automation tool
, который снабжен инструментами XCode. Этот blog объясняет, как автоматизировать ваши тесты пользовательского интерфейса с помощью script.
-
Существует также решение, которое объясняет, как синтезировать события касания на iPhone, но убедитесь, что вы используете их только для модульных тестов, Это звучит скорее как взломать меня, и я рассмотрю это решение как последнее средство, если два предыдущих пункта не будут соответствовать вашим потребностям.
Ответ 2
При использовании в тестах вы можете использовать либо тестовую библиотеку, называемую 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 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 и, вероятно, вызывают отклонение, если они используются вне вашего тестового набора...
Ответ 3
Хорошо, я превратил это в категорию, которая работает.
Интересные биты:
- Категории не могут добавлять переменные-члены. Все, что вы добавляете, становится статичным для класса и, таким образом, сбивается Apple многими UITapGestureRecognizers.
- Итак, используйте связанный_объект, чтобы сделать волшебство.
- NSValue для хранения не объектов
- Метод Apple
init
содержит важную конфигурационную логику; мы можем догадаться о том, что установлено (количество кранов, количество касаний, что еще?
- Но это обречено. Таким образом, мы swizzle в нашем методе init, который сохраняет mocks.
Файл заголовка тривиален; здесь реализация.
#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
Ответ 4
CGPoint tapPoint = [gesture locationInView:self.view];
должен быть
CGPoint tapPoint = [gesture locationInView:gesture.view];
потому что cgpoint следует извлекать от того места, где находится цель жеста, а не пытаться угадать, где в представлении он находится в
Ответ 5
То, что вы пытаетесь сделать, очень сложно (но не совсем невозможно), оставаясь на юридическом пути (iTunes-).
Позвольте мне сначала подготовить правильный путь;
Правильный выход для этого - использование UIAutomation. UIAutomation делает именно то, о чем вы просите, это моделирует поведение пользователей для всех видов тестов.
Теперь, когда это трудно,
Проблема, связанная с вашими проблемами, заключается в создании нового UIEvent. (Un), к счастью, UIKit не предлагает никаких конструкторов для таких событий из-за очевидных соображений безопасности. Есть, однако, обходные пути, которые работали в прошлом, но не уверены, что они все еще делают.
Посмотрите на замечательный блог Мэтта Галагера, в котором описывается решение о том, как синтезировать события касания.
Ответ 6
Я столкнулся с той же проблемой, пытаясь имитировать нажатие на ячейке таблицы, чтобы автоматизировать тест для контроллера вида, который обрабатывает нажатие на таблицу.
Контроллер имеет собственный UITapGestureRecognizer, созданный, как показано ниже:
gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(didRecognizeTapOnTableView)];
unit test должен имитировать касание так, чтобы 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