Подтвердить что ты не робот

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

Ответ 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