web-dev-qa-db-ja.com

UITapGestureRecognizerプログラムで私のビューでタップをトリガーする

編集:質問をより明確にするために更新

編集2:実際の問題に対して質問をより正確にしました。実際に、画面のテキストフィールド以外の場所をタップした場合に行動を起こそうとしています。したがって、単純にイベントを聞くことはできません。テキストフィールド内で、ビューのどこかをタップしたかどうかを知る必要がありますビューで。

私は、ビューの特定の座標内でジェスチャ認識機能がタップを認識すると特定のアクションが実行されることを表明するユニットテストを作成しています UITapGestureRecognizerで処理されるタッチを(特定の座標で)プログラムで作成できるかどうかを知りたいです。 単体テスト中にユーザーインタラクションをシミュレートしようとしています。

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 ]]
}
16
Matt H.

ここには複数のオプションがあると思います:

  1. 最も簡単なのは、Push event actionあなたの見解ですが、タップアクションが発生する場所を選択できるようにしたいので、あなたが本当に望んでいることはないと思います。

    [yourView sendActionsForControlEvents:UIControlEventTouchUpInside];

  2. UI automation tool XCodeインストゥルメントに付属しています。この ブログ は、スクリプトを使用してUIテストを自動化する方法をよく説明しています。

  3. これも solution であり、iPhoneでタッチイベントを合成する方法を説明していますが、ユニットテストのみに使用するようにしてください。これは私にとってはハックのように聞こえますが、前の2つの点でニーズが満たされない場合は、このソリューションを最後の手段と見なします。

9
tiguero

あなたがしようとすることは、(iTunes-)合法的な道に留まっている間、非常に困難です(しかし完全に不可能ではありません).


最初にrightをドラフトさせてください。

これを行うための適切な方法は、UIAutomationを使用することです。 UIAutomationは、ユーザーが求めていることを正確に実行し、あらゆる種類のテストのユーザー動作をシミュレートします。


今は難しい方法です。

問題が解決する問題は、新しいUIEventをインスタンス化することです。 (Un)残念ながら、UIKitはセキュリティ上の理由により、このようなイベントのコンストラクターを提供していません。ただし、過去に機能した回避策はありますが、まだ機能するかどうかはわかりません。

Matt Galagherの素晴らしいブログの草案 タッチイベントを合成する方法のソリューション をご覧ください。

4
Till

テストで使用する場合、 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)
        }
    }

}

ライブラリとスニペットはどちらもプライベートAPIを使用しており、テストスイートの外部で使用すると、おそらく拒否の原因となります...

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

する必要があります

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

cgpointは、ビューのどこにあるかを推測しようとするのではなく、ジェスチャターゲットが正確にある場所から取得する必要があるためです。

2
kevinl

さて、私は上記を機能するカテゴリに変えました。

興味深いビット:

  • カテゴリはメンバー変数を追加できません。追加したものはすべてクラスに対して静的になるため、Appleの多くのUITapGestureRecognizerによって破棄されます。
    • そのため、associated_objectを使用して魔法を起こします。
    • 非オブジェクトを格納するためのNSValue
  • Appleのinitメソッドには、重要な構成ロジックが含まれています。何が設定されているかを推測できます(タップ数、タッチ数、その他何ですか?
    • しかし、これは運命です。したがって、モックを保持するinitメソッドでスウィズルします。

ヘッダーファイルは簡単です。これが実装です。

#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

私は同じ問題に直面し、テーブルのタップを処理するビューコントローラーのテストを自動化するために、テーブルセルのタップをシミュレートしようとしました。

コントローラには、以下のように作成されたプライベートUITapGestureRecognizerがあります。

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

ユニットテストはタッチをシミュレートする必要があります。これにより、ユーザーインタラクションから発生したジェスチャーリコグナイザがアクションをトリガーします。

提案された解決策はどれもこのシナリオでは機能しなかったため、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
2
Glauco Aquino

単一の行を使用する単体テストでUITapGestureRecognizerのタッチをトリガーするはるかに簡単な方法があります。タップジェスチャレコグナイザへの参照を保持する変数があるとすると、必要なのは次のとおりです。

singleTapGestureRecognizer?.state = .ended
1
DCDC

Swift 4に更新された@Ondrejによる回答:

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

}

使用法:

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)
1
Vlad