diff --git a/FBSnapshotTestCase.xcodeproj/project.pbxproj b/FBSnapshotTestCase.xcodeproj/project.pbxproj index 2cad090..4b682b5 100644 --- a/FBSnapshotTestCase.xcodeproj/project.pbxproj +++ b/FBSnapshotTestCase.xcodeproj/project.pbxproj @@ -19,11 +19,9 @@ 827137911C63ABE900354E42 /* UIImage+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564101B59C3F500A4E4BF /* UIImage+Compare.h */; }; 827137921C63ABF000354E42 /* UIImage+Diff.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564121B59C3F500A4E4BF /* UIImage+Diff.h */; }; 827137931C63ABF000354E42 /* UIImage+Snapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 133564141B59C3F500A4E4BF /* UIImage+Snapshot.h */; }; - 827137941C63ABF000354E42 /* UIApplication+StrictKeyWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */; }; 827137951C63ABF400354E42 /* UIImage+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564111B59C3F500A4E4BF /* UIImage+Compare.m */; }; 827137961C63ABF400354E42 /* UIImage+Diff.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564131B59C3F500A4E4BF /* UIImage+Diff.m */; }; 827137971C63ABF400354E42 /* UIImage+Snapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */; }; - 827137981C63ABF400354E42 /* UIApplication+StrictKeyWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */; }; 827137991C63ABF900354E42 /* FBSnapshotTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = B31988201AB7849400B0A900 /* FBSnapshotTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8271379A1C63ABF900354E42 /* FBSnapshotTestCasePlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 13CBB39B1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8271379B1C63ABF900354E42 /* FBSnapshotTestController.h in Headers */ = {isa = PBXBuildFile; fileRef = B31988221AB7849400B0A900 /* FBSnapshotTestController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -47,11 +45,7 @@ B32447DE1AB78B5E00B1D6FF /* square.png in Resources */ = {isa = PBXBuildFile; fileRef = B32447DB1AB78B5E00B1D6FF /* square.png */; }; B76C68291C6BD6D200586E5B /* rect.png in Resources */ = {isa = PBXBuildFile; fileRef = B76C68271C6BD68100586E5B /* rect.png */; }; B76C682A1C6BD6D500586E5B /* rect.png in Resources */ = {isa = PBXBuildFile; fileRef = B76C68271C6BD68100586E5B /* rect.png */; }; - BC45D5211C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */; }; - BC45D5221C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */; }; E5C2CD621B1F399A00669887 /* square_with_pixel.png in Resources */ = {isa = PBXBuildFile; fileRef = E5C2CD611B1F399A00669887 /* square_with_pixel.png */; }; - E704DBE4202B898800A48406 /* UIControl+SendActions.h in Headers */ = {isa = PBXBuildFile; fileRef = E704DBE2202B898800A48406 /* UIControl+SendActions.h */; }; - E704DBE5202B898800A48406 /* UIControl+SendActions.m in Sources */ = {isa = PBXBuildFile; fileRef = E704DBE3202B898800A48406 /* UIControl+SendActions.m */; }; F0D698F51B204E120005CAC9 /* SwiftSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D698F41B204E120005CAC9 /* SwiftSupport.swift */; }; /* End PBXBuildFile section */ @@ -96,11 +90,7 @@ B32447DA1AB78B5E00B1D6FF /* square-copy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "square-copy.png"; sourceTree = ""; }; B32447DB1AB78B5E00B1D6FF /* square.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square.png; sourceTree = ""; }; B76C68271C6BD68100586E5B /* rect.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = rect.png; sourceTree = ""; }; - BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIApplication+StrictKeyWindow.h"; sourceTree = ""; }; - BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+StrictKeyWindow.m"; sourceTree = ""; }; E5C2CD611B1F399A00669887 /* square_with_pixel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = square_with_pixel.png; sourceTree = ""; }; - E704DBE2202B898800A48406 /* UIControl+SendActions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIControl+SendActions.h"; sourceTree = ""; }; - E704DBE3202B898800A48406 /* UIControl+SendActions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIControl+SendActions.m"; sourceTree = ""; }; F0D698F41B204E120005CAC9 /* SwiftSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSupport.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -147,10 +137,6 @@ 133564131B59C3F500A4E4BF /* UIImage+Diff.m */, 133564141B59C3F500A4E4BF /* UIImage+Snapshot.h */, 133564151B59C3F500A4E4BF /* UIImage+Snapshot.m */, - BC45D51F1C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h */, - BC45D5201C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m */, - E704DBE2202B898800A48406 /* UIControl+SendActions.h */, - E704DBE3202B898800A48406 /* UIControl+SendActions.m */, ); path = Categories; sourceTree = ""; @@ -232,7 +218,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 827137941C63ABF000354E42 /* UIApplication+StrictKeyWindow.h in Headers */, 8271379A1C63ABF900354E42 /* FBSnapshotTestCasePlatform.h in Headers */, 827137911C63ABE900354E42 /* UIImage+Compare.h in Headers */, 827137991C63ABF900354E42 /* FBSnapshotTestCase.h in Headers */, @@ -246,9 +231,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - BC45D5211C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.h in Headers */, B31988281AB7849400B0A900 /* FBSnapshotTestCase.h in Headers */, - E704DBE4202B898800A48406 /* UIControl+SendActions.h in Headers */, 13CBB39D1AEE013900B6ADBA /* FBSnapshotTestCasePlatform.h in Headers */, B319882A1AB7849400B0A900 /* FBSnapshotTestController.h in Headers */, 133564181B59C3F500A4E4BF /* UIImage+Diff.h in Headers */, @@ -427,7 +410,6 @@ files = ( 827137961C63ABF400354E42 /* UIImage+Diff.m in Sources */, 8271379E1C63ABFD00354E42 /* FBSnapshotTestController.m in Sources */, - 827137981C63ABF400354E42 /* UIApplication+StrictKeyWindow.m in Sources */, 8271379D1C63ABFD00354E42 /* FBSnapshotTestCasePlatform.m in Sources */, 8271379C1C63ABFD00354E42 /* FBSnapshotTestCase.m in Sources */, 827137951C63ABF400354E42 /* UIImage+Compare.m in Sources */, @@ -449,8 +431,6 @@ buildActionMask = 2147483647; files = ( 133564171B59C3F500A4E4BF /* UIImage+Compare.m in Sources */, - BC45D5221C2AEFCE007C72F3 /* UIApplication+StrictKeyWindow.m in Sources */, - E704DBE5202B898800A48406 /* UIControl+SendActions.m in Sources */, B31988291AB7849400B0A900 /* FBSnapshotTestCase.m in Sources */, 133564191B59C3F500A4E4BF /* UIImage+Diff.m in Sources */, 1335641B1B59C3F500A4E4BF /* UIImage+Snapshot.m in Sources */, diff --git a/FBSnapshotTestCase.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/FBSnapshotTestCase.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/FBSnapshotTestCase.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.h b/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.h deleted file mode 100644 index 4bfe1a9..0000000 --- a/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.h +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2017-2018, Uber Technologies, Inc. - * Copyright (c) 2015-2018, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -#import - -@interface UIApplication (StrictKeyWindow) - -/** - @return The receiver's @c keyWindow. Raises an assertion if @c nil. - */ -- (UIWindow *)fb_strictKeyWindow; - -@end diff --git a/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.m b/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.m deleted file mode 100644 index 48766d6..0000000 --- a/FBSnapshotTestCase/Categories/UIApplication+StrictKeyWindow.m +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2017-2018, Uber Technologies, Inc. - * Copyright (c) 2015-2018, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -#import - -@implementation UIApplication (StrictKeyWindow) - -- (UIWindow *)fb_strictKeyWindow -{ - UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; - NSString *message = @"Snapshot tests should be hosted by an application with a key window. Please ensure your test host sets up a key window at launch (either via storyboards or programmatically) and doesn't do anything to remove it while snapshot tests are running."; - NSLog(@"[iOS Snapshot Test Case] : %@", message); - return keyWindow; -} - -@end diff --git a/FBSnapshotTestCase/Categories/UIControl+SendActions.h b/FBSnapshotTestCase/Categories/UIControl+SendActions.h deleted file mode 100644 index 1563ea9..0000000 --- a/FBSnapshotTestCase/Categories/UIControl+SendActions.h +++ /dev/null @@ -1,36 +0,0 @@ -/** - Copyright (c) 2018 Uber Technologies, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - -#import - -@interface UIControl (SendActions) - -/** - In library test bundles with no test host, the default sendActionsForControlEvents: does not work. - - This replacement mimics the same idea of that method by finding all the targets associated with the control, finding all the actions on that target for the given control event, and invoking those actions on those targets. - - @param controlEvents A bitmask whose set flags specify the control events for which action messages are sent. - */ -- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents; - -@end diff --git a/FBSnapshotTestCase/Categories/UIControl+SendActions.m b/FBSnapshotTestCase/Categories/UIControl+SendActions.m deleted file mode 100644 index 3befb3e..0000000 --- a/FBSnapshotTestCase/Categories/UIControl+SendActions.m +++ /dev/null @@ -1,51 +0,0 @@ -/** - Copyright (c) 2018 Uber Technologies, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - */ - -#import "UIControl+SendActions.h" - -/** - UIControlEvents has options in the range 0-8, 12-13, 16-19. 9-11 are reserved for future UIControlEventTouch* options. 14-15 are reserved for other options. If new options are added after 19, this const will need to be updated. - */ -static NSUInteger const UIControlEventsMaxOffset = 19; - - -@implementation UIControl (UberTesting) - -- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents -{ - for (NSUInteger i = 0; i < UIControlEventsMaxOffset; i++) { - UIControlEvents controlEvent = 1 << i; - if (controlEvents & controlEvent) { - for (id target in self.allTargets) { - NSArray *targetActions = [self actionsForTarget:target forControlEvent:controlEvent]; - for (NSString *action in targetActions) { - SEL selector = NSSelectorFromString(action); - IMP imp = [target methodForSelector:selector]; - void (*func)(id, SEL, id) = (void *)imp; - func(target, selector, self); - } - } - } - } -} - -@end diff --git a/FBSnapshotTestCase/Categories/UIImage+Snapshot.m b/FBSnapshotTestCase/Categories/UIImage+Snapshot.m index 2553e28..1457ca5 100644 --- a/FBSnapshotTestCase/Categories/UIImage+Snapshot.m +++ b/FBSnapshotTestCase/Categories/UIImage+Snapshot.m @@ -8,7 +8,6 @@ */ #import -#import @implementation UIImage (Snapshot) @@ -43,7 +42,7 @@ + (UIImage *)fb_imageForView:(UIView *)view UIWindow *window = [view isKindOfClass:[UIWindow class]] ? (UIWindow *)view : view.window; BOOL removeFromSuperview = NO; if (!window) { - window = [[UIApplication sharedApplication] fb_strictKeyWindow]; + window = [[UIApplication sharedApplication] keyWindow]; } if (!view.window && view != window) { diff --git a/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m index 5ff60c2..77909ba 100644 --- a/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m +++ b/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m @@ -8,7 +8,6 @@ */ #import -#import #import BOOL FBSnapshotTestCaseIs64Bit(void) @@ -34,7 +33,7 @@ BOOL FBSnapshotTestCaseIs64Bit(void) NSString *FBDeviceAgnosticNormalizedFileName(NSString *fileName) { UIDevice *device = [UIDevice currentDevice]; - UIWindow *keyWindow = [[UIApplication sharedApplication] fb_strictKeyWindow]; + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; CGSize screenSize = keyWindow.bounds.size; NSString *os = device.systemVersion; diff --git a/README.md b/README.md index caf3a71..2cbb36c 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ In Xcode 5 and later new projects only offer application tests, but older projects will have separate targets for the two types. -However, if you are writing snapshot tests inside a library/framework, you might want to keep your test bundle as a library test bundle without a Test Host or Simulator. +*However*, if you are writing snapshot tests inside a library/framework, you might want to keep your test bundle as a library test bundle without a Test Host or Simulator. Read more on this [here](docs/LibraryVsApplicationTestBundles.md). diff --git a/docs/LibraryVsApplicationTestBundles.md b/docs/LibraryVsApplicationTestBundles.md index 65c6682..5a9af70 100644 --- a/docs/LibraryVsApplicationTestBundles.md +++ b/docs/LibraryVsApplicationTestBundles.md @@ -10,13 +10,84 @@ Library Test Bundles were once called _Logic_ Test Bundles in Apple's nomenclatu ### Application tests -Unit tests that test parts of an application (such as UIViewControllers, UIWindows, UIViews) should typically be part of an Application test bundle. An Application test bundle requires a Test Host and at test run time, a Simulator too. +Unit tests that test parts of an application (such as UIViewControllers, UIWindows, UIViews) should typically be part of an Application test bundle. An Application test bundle requires a Test Host and at test run time, a Simulator too. The attached Simulator provides access to some iOS APIs that only work inside Application test bundles. In our experience, we've seen these: -There are some APIs that only work inside Application test bundles. In our testing, we've seen a few. Here are some: - -* `-[UIControl sendActionsForControlEvents:]` — This API is commonly used to trigger actions at runtime and sometimes you might want to use it inside a test to trigger a particular code path which is ordinarily run when a user performs an action. While it does not work inside a Library test bundle, [we've written our own version for unit tests](FBSnapshotTestCase/Categories/UIControl+SendActions.h) that works well for this need. If you decide to use that category, make sure it can only be seen inside unit tests and not all of your code. +* `-[UIControl sendActionsForControlEvents:]` — This API is commonly used to trigger actions at runtime and sometimes you might want to use it inside a test to trigger a particular code path which is ordinarily run when a user performs an action. While it does not work inside a Library test bundle, we've written our own version for unit tests (see 'Code Snippets' below) that works well for this need. * `UIAppearance` — Most `UIAppearance` APIs break when there is no test host present. * `UIWindow` — You cannot make a `UIWindow` you created during your test the 'key window' because `makeKeyAndVisible` crashes at test run time. One workaround is to instead set `hidden` to `false` on the `UIWindow` instance you created. However there still won't be a 'key window' so if you have code that adds a `UIView` as a subview of the `keyWindow` then that will break. +* Keychain — Keychain operations require an application test bundle. ### Library tests -Unit tests that test parts of a framework or libary should be part of a Library test bundle. This does not require a Test Host or a Simulator (though in Xcode 9, Apple still launches a Simulator for these tests). \ No newline at end of file +Unit tests that test parts of a framework or library should be part of a Library test bundle. This does not strictly require a Test Host or a Simulator (though in Xcode 9, Apple still launches a Simulator for these tests). If you are using Buck, removing the `test_host_app` option for `apple_test()` rules will allow Buck and `xctool` to run your test bundles in parallel. + +### Code Examples +#### ub_sendActionsForControlEvents: +This code snippet shows how you might replace `UIControl`'s `sendActionForControlEvents:` in a test that is inside a library test bundle. Since it doesn't have universal application we haven't included it directly in the project. If you decide to use this category, make sure it can only be seen inside unit tests and not all of your code. + +``` +/** + Copyright (c) 2018 Uber Technologies, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +#import + +@interface UIControl (SendActions) + +/** + In library test bundles with no test host, the default sendActionsForControlEvents: does not work. + + This replacement mimics the same idea of that method by finding all the targets associated with the control, finding all the actions on that target for the given control event, and invoking those actions on those targets. + + @param controlEvents A bitmask whose set flags specify the control events for which action messages are sent. + */ +- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents; + +@end + +/** + UIControlEvents has options in the range 0-8, 12-13, 16-19. 9-11 are reserved for future UIControlEventTouch* options. 14-15 are reserved for other options. If new options are added after 19, this const will need to be updated. + */ +static NSUInteger const UIControlEventsMaxOffset = 19; + + +@implementation UIControl (UberTesting) + +- (void)ub_sendActionsForControlEvents:(UIControlEvents)controlEvents +{ + for (NSUInteger i = 0; i < UIControlEventsMaxOffset; i++) { + UIControlEvents controlEvent = 1 << i; + if (controlEvents & controlEvent) { + for (id target in self.allTargets) { + NSArray *targetActions = [self actionsForTarget:target forControlEvent:controlEvent]; + for (NSString *action in targetActions) { + SEL selector = NSSelectorFromString(action); + IMP imp = [target methodForSelector:selector]; + void (*func)(id, SEL, id) = (void *)imp; + func(target, selector, self); + } + } + } + } +} + +@end + +``` \ No newline at end of file