web-dev-qa-db-fra.com

UITapGestureRecognizer Déclenche par programme un robinet de mon point de vue

Modifier: mise à jour pour rendre la question plus évidente

Edit 2: Question plus précise à mon problème du monde réel. Je cherche en fait à agir s'ils appuient n'importe où, SAUF dans un champ de texte à l'écran. Ainsi, je ne peux pas simplement écouter les événements dans le champ de texte, j'ai besoin de savoir s'ils ont tapé n'importe où dans la vue.

J'écris des tests unitaires pour affirmer qu'une certaine action est entreprise lorsqu'un identificateur de geste reconnaît un tap dans certaines coordonnées de ma vue . Je veux savoir si je peux créer par programme une touche (à des coordonnées spécifiques) qui sera gérée par UITapGestureRecognizer. Je tente de simuler l'interaction de l'utilisateur lors d'un test unitaire.

UITapGestureRecognizer est configuré dans 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 ]]
}
15
Matt H.

Je pense que vous avez plusieurs options ici:

  1. Le plus simple serait peut-être d’envoyer un Push event action à votre vue, mais je ne pense pas que ce que vous voulez vraiment, car vous voulez pouvoir choisir l’emplacement de l’action tap. 

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

  2. Vous pouvez utiliser UI automation tool fourni avec les instruments XCode. Ce blog explique bien comment automatiser vos tests d'interface utilisateur avec un script. 

  3. Il existe cette solution qui explique également comment synthétiser les événements tactiles sur l’iPhone tout en veillant à ne les utiliser que pour les tests unitaires. Cela ressemble plus à un bidouillage pour moi et je considérerai cette solution en dernier recours si les deux points précédents ne répondent pas à vos besoins.

10
tiguero

Si utilisé dans les tests, vous pouvez utiliser une bibliothèque de tests appelée SpecTools qui vous aide dans tout cela et plus, ou bien utiliser son code directement:

// 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)
        }
    }

}

Tant la bibliothèque que l'extrait utilisent des API privées et vont probablement provoquer un rejet s'ils sont utilisés en dehors de votre suite de tests ...

3
Ondrej

Ce que vous essayez de faire est très difficile (mais pas tout à fait impossible) tout en restant sur le chemin légal (iTunes). 


Laissez-moi d’abord rédiger le right way;

La meilleure façon de le faire est d'utiliser UIAutomation. UIAutomation fait exactement ce que vous demandez, il simule le comportement de l'utilisateur pour tous types de tests.


Maintenant ce moyen difficile;

Le problème auquel vos problèmes se résument consiste à instancier un nouvel UIEvent. (Un) heureusement, UIKit ne propose aucun constructeur pour de tels événements pour des raisons évidentes de sécurité. Il existe cependant des solutions de contournement qui fonctionnaient dans le passé, mais elles ne le sont pas toujours.

Jetez un coup d’œil à l’impressionnant blog de Matt Galagher en train de rédiger une solution sur la façon de synthétiser des événements tactiles

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

devrait être

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

parce que le point de contrôle doit être récupéré exactement là où se trouve la cible du geste plutôt que d'essayer de deviner où, dans la vue, il se trouve

2
kevinl

D'accord, j'ai transformé ce qui précède en une catégorie qui fonctionne. 

Bits intéressants:

  • Les catégories ne peuvent pas ajouter de variables de membre. Tout ce que vous ajoutez devient statique dans la classe et est donc encombré par les nombreux UITapGestureRecognizers d'Apple.
    • Donc, utilisez related_object pour que la magie opère.
    • NSValue pour stocker des non-objets
  • La méthode init d'Apple contient une logique de configuration importante; on peut deviner ce qui est réglé (nombre de prises, nombre de touches, quoi d'autre?
    • Mais c'est condamné. Nous passons donc dans notre méthode init qui préserve les simulacres. 

Le fichier d'en-tête est trivial; voici la mise en œuvre.

#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
2
tooluser

Je rencontrais le même problème en essayant de simuler un tap sur une cellule de tableau pour automatiser un test pour un contrôleur de vue qui gère le tapotement sur une table.

Le contrôleur a un UITapGestureRecognizer privé créé comme suit:

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

Le test unitaire doit simuler une touche pour que le gestureRecognizer déclenche l'action telle qu'elle a été générée par l'interaction de l'utilisateur.

Aucune des solutions proposées ne fonctionnant dans ce scénario, je l'ai donc résolue en décorant UITapGestureRecognizer, en simulant les méthodes exactes appelées par le contrôleur. J'ai donc ajouté une méthode "performTap" qui appelle l'action de manière à ce que le contrôleur lui-même ne sache pas d'où provient l'action. De cette façon, je pourrais faire une unité de test pour le contrôleur indépendante de la reconnaissance des gestes, juste de l'action déclenchée.

Ceci est ma catégorie, espérons que cela aide quelqu'un.

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
2
Glauco Aquino

Réponse de @Ondrej mise à jour vers 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 { $0 as? UITapGestureRecognizer } ?? []
        grs.forEach { $0.sendActions() }
    }
}


let myView = ... // View under UI Logic Test
Automator.tap(view: myView)
0
Vlad