2012-12-30 13 views
13

Edycja: Zaktualizowany aby kwestia bardziej oczywisteUITapGestureRecognizer programowo wywołać kranu moim zdaniem

Edit 2: Wykonane pytanie bardziej dokładne, aby mój problem w świecie rzeczywistym. Właściwie zamierzam podjąć działania, jeśli dotkną w dowolnym miejscu Z WYJĄTKIEM w polu tekstowym na ekranie. W związku z tym nie mogę po prostu posłuchać wydarzeń w polu tekstowym, muszę wiedzieć, czy kliknęli w dowolnym miejscu w widoku.

Piszę testy jednostkowe, aby potwierdzić, że wykonano pewną czynność, gdy rozpoznawacz gestów rozpoznaje dotknięcie w pewnych współrzędnych mojego widoku. Chcę wiedzieć, czy mogę programowo utworzyć dotyk (przy określonych współrzędnych), który będzie obsługiwany przez UITapGestureRecognizer. Próbuję zasymulować interakcję użytkownika podczas testu jednostki.

UITapGestureRecognizer jest skonfigurowany w konstruktorze Interface

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

co to za pytanie? – tiguero

+0

zaktualizowane pytanie –

+1

będzie nagrywać na określony widok lub przycisk będzie wystarczający? – tiguero

Odpowiedz

9

myślę, że masz wiele opcji tutaj:

  1. Może być najprostsze byłoby wysłać push event action do widzenia ale don nie sądzisz, że tak naprawdę chcesz, ponieważ chcesz mieć możliwość wyboru miejsca, w którym pojawia się działanie "dotknij".

    [yourView sendActionsForControlEvents: UIControlEventTouchUpInside];

  2. Można użyć UI automation tool, który jest dostarczany z instrumentami XCode. To blog wyjaśnia, jak zautomatyzować testy interfejsu użytkownika za pomocą skryptu.

  3. Jest to również solution, które wyjaśniają, jak syntetyzować zdarzenia dotykowe na iPhonie, ale upewnij się, że używasz ich tylko do testów jednostkowych. To brzmi bardziej jak włamanie do mnie i rozważę to rozwiązanie jako ostateczność, jeśli dwa poprzednie punkty nie spełniają twoich potrzeb.

+0

Zobacz też [MonkeyTalk] (http://cloudmonkeymobile.com/monkeytalk) –

+14

Opcja 1 jest nieprawidłowa. "sendActionsForControlEvents" działa tylko dla obiektów UIControl; nie UIView nich. –

1

To, co próbujesz zrobić, jest bardzo trudne (ale nie całkowicie niemożliwe) podczas pozostawania na legalnej ścieżce (iTunes-).


Pozwól mi najpierw sporządzić prawo drogę;

Właściwym wyjściem z tego jest użycie UIAutomation. UIAutomation robi dokładnie to, o co prosisz, symuluje zachowanie użytkownika dla wszystkich rodzajów testów.


Teraz na twardą drogę;

Problem, na którym sprowadzają się twoje problemy, polega na utworzeniu nowego UIEvent. (Un) na szczęście UIKit nie oferuje konstruktorów do takich wydarzeń z oczywistych względów bezpieczeństwa. Istnieją jednak rozwiązania, które działały w przeszłości, ale nie są pewne, czy nadal istnieją.

Zapoznaj się z niesamowitym blogiem Matta Galaghera, opracowując solution on how to synthesise touch events.

1

Wystąpił ten sam problem, próbując symulować dotknięcie komórki tabeli, aby zautomatyzować test dla kontrolera widoku, który obsługuje stukanie w tabelę.

Kontroler posiada prywatny UITapGestureRecognizer utworzony jak poniżej:

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

Urządzenie powinno symulować dotyk tak że gestureRecognizer by wywołać działanie jak to było pochodzi z interakcji użytkownika.

Żadne z proponowanych rozwiązań nie działało w tym scenariuszu, więc rozwiązałem go, dekorując UITapGestureRecognizer, udając dokładne metody wywoływane przez kontroler. Dlatego dodałem metodę "performTap", która wywołuje akcję w taki sposób, że kontroler nie jest świadomy skąd pochodzi działanie. W ten sposób mógłbym ustawić jednostkę testową dla kontrolera niezależnie od rozpoznawania gestów, po prostu wywołanej akcji.

To jest moja kategoria, mam nadzieję, że komuś pomaga.

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 
+1

To jest genialny pomysł. Niestety, nie można dodawać zmiennych członków do kategorii. Zmienne te będą skutecznie statyczne i nie będziesz mógł na nich polegać, gdy w twoim widoku znajduje się> 1 rozpoznawanie gestów. Będzie tak w przypadku innych pól itp., Ponieważ Apple używa ich wewnętrznie. Rozwiązaniem problemu jest użycie powiązanych obiektów do sfałszowania we właściwościach. Jeśli jest zainteresowanie, opublikuję to rozwiązanie. – tooluser

2

OK, zmieniłem powyższe w kategorię, która działa.

Ciekawe bity:

  • kategorie nie mogą dodawać zmienne składowe. Wszystko, co dodasz, staje się statyczne dla klasy, a tym samym jest odrzucane przez wiele UITapGestureRecognizers firmy Apple.
    • Używając obiektu related_object, aby magia się wydarzyła.
    • NSValue do przechowywania przedmiotów niebędących
  • init metoda Apple zawiera ważne logikę konfiguracji; możemy domyślać się, co jest ustawiony (liczby kranów, liczba dotknięć, co jeszcze?
    • Ale to jest skazane. Więc swizzle w naszej metodzie init, który zachowuje mocks.

plik nagłówka jest trywialne;. oto realizacja

#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 
+0

Mogę użyć tego w aplikacji AppStore? –

+1

Bije mnie. Spróbuj! Ale wszystko, co musisz zrobić w aplikacji, powinieneś być w stanie zrobić bez dodatkowej warstwy udawania rzeczy. – tooluser

+0

Czy to nadal działa? Próbowałem tego, ale nie otrzymałem odpowiedzi – hoangpx

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

powinny być

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

ponieważ cgpoint powinny być pobierane z dokładnie gdzie cel gest jest raczej niż starając się odgadnąć, gdzie zdaniem to w

3

Jeśli stosowane w testach można użyć biblioteki testowy o nazwie SpecTools który pomaga ze wszystkimi tym bardziej, lub jego kod bezpośrednio:

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

} 

Zarówno biblioteka, jak również fragment użytku prywatnego API i prawdopodobnie spowoduje odrzucenie, jeżeli są stosowane spoza zestawu testowego ...

Powiązane problemy