編集:質問をより明確にするために更新
編集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 ]]
}
ここには複数のオプションがあると思います:
最も簡単なのは、Push event action
あなたの見解ですが、タップアクションが発生する場所を選択できるようにしたいので、あなたが本当に望んでいることはないと思います。
[yourView sendActionsForControlEvents:UIControlEventTouchUpInside];
UI automation tool
XCodeインストゥルメントに付属しています。この ブログ は、スクリプトを使用してUIテストを自動化する方法をよく説明しています。
これも solution であり、iPhoneでタッチイベントを合成する方法を説明していますが、ユニットテストのみに使用するようにしてください。これは私にとってはハックのように聞こえますが、前の2つの点でニーズが満たされない場合は、このソリューションを最後の手段と見なします。
あなたがしようとすることは、(iTunes-)合法的な道に留まっている間、非常に困難です(しかし完全に不可能ではありません).
最初にrightをドラフトさせてください。
これを行うための適切な方法は、UIAutomationを使用することです。 UIAutomationは、ユーザーが求めていることを正確に実行し、あらゆる種類のテストのユーザー動作をシミュレートします。
今は難しい方法です。
問題が解決する問題は、新しいUIEventをインスタンス化することです。 (Un)残念ながら、UIKitはセキュリティ上の理由により、このようなイベントのコンストラクターを提供していません。ただし、過去に機能した回避策はありますが、まだ機能するかどうかはわかりません。
Matt Galagherの素晴らしいブログの草案 タッチイベントを合成する方法のソリューション をご覧ください。
テストで使用する場合、 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を使用しており、テストスイートの外部で使用すると、おそらく拒否の原因となります...
CGPoint tapPoint = [gesture locationInView:self.view];
する必要があります
CGPoint tapPoint = [gesture locationInView:gesture.view];
cgpointは、ビューのどこにあるかを推測しようとするのではなく、ジェスチャターゲットが正確にある場所から取得する必要があるためです。
さて、私は上記を機能するカテゴリに変えました。
興味深いビット:
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
私は同じ問題に直面し、テーブルのタップを処理するビューコントローラーのテストを自動化するために、テーブルセルのタップをシミュレートしようとしました。
コントローラには、以下のように作成されたプライベート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
単一の行を使用する単体テストでUITapGestureRecognizerのタッチをトリガーするはるかに簡単な方法があります。タップジェスチャレコグナイザへの参照を保持する変数があるとすると、必要なのは次のとおりです。
singleTapGestureRecognizer?.state = .ended
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)