├── LICENCE ├── PSMenuItem.h ├── PSMenuItem.m ├── PSMenuItem.podspec ├── PSPDFMenuItemDemo.xcodeproj └── project.pbxproj ├── PSPDFMenuItemDemo ├── PSAppDelegate.h ├── PSAppDelegate.m ├── PSPDFMenuItemDemo-Info.plist ├── PSPDFMenuItemDemo-Prefix.pch ├── PSViewController.h ├── PSViewController.m ├── en.lproj │ ├── InfoPlist.strings │ └── MainStoryboard_iPad.storyboard └── main.m └── README.md /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Peter Steinberger 2 | This code is a part of http://pspdfkit.com and has been put under MIT license. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PSMenuItem.h: -------------------------------------------------------------------------------- 1 | // PSMenuItem.h 2 | // 3 | // Copyright (c) 2012 Peter Steinberger (http://petersteinberger.com) 4 | // This code is a part of http://pspdfkit.com and has been put under MIT license. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import 25 | 26 | /** 27 | This subclass adds support for a block-based action on UIMenuItem. 28 | If you are as annoyed about the missing target/action pattern, you will love this. 29 | 30 | If you use PSMenuItem with the classic initWithTitle:selector initializer, 31 | this will work and be handled just like a UIMenuItem. 32 | */ 33 | @interface PSMenuItem : UIMenuItem 34 | 35 | // Initialize PSMenuItem with a block. 36 | - (id)initWithTitle:(NSString *)title block:(void(^)())block; 37 | 38 | // Menu Item can be enabled/disabled. (disable simply hides it from the UIMenuController) 39 | @property(nonatomic, assign, getter=isEnabled) BOOL enabled; 40 | 41 | // Action block. 42 | @property(nonatomic, copy) void(^block)(); 43 | 44 | 45 | /** 46 | Installs the menu handler. Needs to be called once per class. 47 | (A good place is the +load method) 48 | 49 | Following methods will be swizzled: 50 | - canBecomeFirstResponder (if object doesn't already return YES) 51 | - canPerformAction:withSender: 52 | - methodSignatureForSelector: 53 | - forwardInvocation: 54 | 55 | The original implementation will be called if the PSMenuItem selector is not detected. 56 | 57 | @parm object can be an instance or a class. 58 | */ 59 | + (void)installMenuHandlerForObject:(id)object; 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /PSMenuItem.m: -------------------------------------------------------------------------------- 1 | // PSMenuItem.m 2 | // 3 | // Copyright (c) 2012 Peter Steinberger (http://petersteinberger.com) 4 | // This code is a part of http://pspdfkit.com and has been put under MIT license. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import "PSMenuItem.h" 25 | #import 26 | #import 27 | 28 | // imp_implementationWithBlock changed it's type in iOS6 (XCode 4.5) 29 | #if __IPHONE_OS_VERSION_MAX_ALLOWED < 60000 30 | #define PSPDFBlockImplCast (__bridge void *) 31 | #else 32 | #define PSPDFBlockImplCast 33 | #endif 34 | 35 | NSString *kMenuItemTrailer = @"ps_performMenuItem"; 36 | 37 | // Add method + swizzle. 38 | void PSPDFReplaceMethod(Class c, SEL origSEL, SEL newSEL, IMP impl); 39 | void PSPDFReplaceMethod(Class c, SEL origSEL, SEL newSEL, IMP impl) { 40 | Method origMethod = class_getInstanceMethod(c, origSEL); 41 | class_addMethod(c, newSEL, impl, method_getTypeEncoding(origMethod)); 42 | Method newMethod = class_getInstanceMethod(c, newSEL); 43 | if(class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) { 44 | class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod)); 45 | }else { 46 | method_exchangeImplementations(origMethod, newMethod); 47 | } 48 | } 49 | 50 | // Checks for our custom selector. 51 | BOOL PSPDFIsMenuItemSelector(SEL selector); 52 | BOOL PSPDFPIsMenuItemSelector(SEL selector) { 53 | return [NSStringFromSelector(selector) hasPrefix:kMenuItemTrailer]; 54 | } 55 | 56 | @interface PSMenuItem() 57 | @property(nonatomic, assign) SEL customSelector; 58 | @end 59 | 60 | @implementation PSMenuItem 61 | 62 | /////////////////////////////////////////////////////////////////////////////////////////// 63 | #pragma mark - Static 64 | 65 | /* 66 | This might look scary, but it's actually not that bad. 67 | We hook into the three methods of UIResponder and NSObject to capture calls to our custom created selector. 68 | Then we find the UIMenuController and search for the corresponding PSMenuItem. 69 | If the kMenuItemTrailer is not detected, we call the original implementation. 70 | 71 | This all wouldn't be necessary if UIMenuController would call our selectors with the UIMenuItem as sender. 72 | */ 73 | + (void)installMenuHandlerForObject:(id)object { 74 | @autoreleasepool { 75 | @synchronized(self) { 76 | // object can be both a class or an instance of a class. 77 | Class objectClass = class_isMetaClass(object_getClass(object)) ? object : [object class]; 78 | 79 | // check if menu handler has been already installed. 80 | SEL canPerformActionSEL = @selector(pspdf_canPerformAction:withSender:); 81 | if (!class_getInstanceMethod(objectClass, canPerformActionSEL)) { 82 | 83 | // add canBecomeFirstResponder if it is not returning YES. (or if we don't know) 84 | if (object == objectClass || ![object canBecomeFirstResponder]) { 85 | SEL canBecomeFRSEL = @selector(pspdf_canBecomeFirstResponder); 86 | IMP canBecomeFRIMP = imp_implementationWithBlock(PSPDFBlockImplCast(^(id _self) { 87 | return YES; 88 | })); 89 | PSPDFReplaceMethod(objectClass, @selector(canBecomeFirstResponder), canBecomeFRSEL, canBecomeFRIMP); 90 | } 91 | 92 | // swizzle canPerformAction:withSender: for our custom selectors. 93 | // Queried before the UIMenuController is shown. 94 | IMP canPerformActionIMP = imp_implementationWithBlock(PSPDFBlockImplCast(^(id _self, SEL action, id sender) { 95 | return PSPDFPIsMenuItemSelector(action) ? YES : ((BOOL (*)(id, SEL, SEL, id))objc_msgSend)(_self, canPerformActionSEL, action, sender); 96 | })); 97 | PSPDFReplaceMethod(objectClass, @selector(canPerformAction:withSender:), canPerformActionSEL, canPerformActionIMP); 98 | 99 | // swizzle methodSignatureForSelector:. 100 | SEL methodSignatureSEL = @selector(pspdf_methodSignatureForSelector:); 101 | IMP methodSignatureIMP = imp_implementationWithBlock(PSPDFBlockImplCast(^(id _self, SEL selector) { 102 | if (PSPDFPIsMenuItemSelector(selector)) { 103 | return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // fake it. 104 | }else { 105 | return ((NSMethodSignature * (*)(id, SEL, SEL))objc_msgSend)(_self, methodSignatureSEL, selector); 106 | } 107 | })); 108 | PSPDFReplaceMethod(objectClass, @selector(methodSignatureForSelector:), methodSignatureSEL, methodSignatureIMP); 109 | 110 | // swizzle forwardInvocation: 111 | SEL forwardInvocationSEL = @selector(pspdf_forwardInvocation:); 112 | IMP forwardInvocationIMP = imp_implementationWithBlock(PSPDFBlockImplCast(^(id _self, NSInvocation *invocation) { 113 | if (PSPDFPIsMenuItemSelector([invocation selector])) { 114 | for (PSMenuItem *menuItem in [UIMenuController sharedMenuController].menuItems) { 115 | if ([menuItem isKindOfClass:[PSMenuItem class]] && sel_isEqual([invocation selector], menuItem.customSelector)) { 116 | [menuItem performBlock]; break; // find corresponding MenuItem and forward 117 | } 118 | } 119 | }else { 120 | ((void (*)(id, SEL, NSInvocation *))objc_msgSend)(_self, forwardInvocationSEL, invocation); 121 | } 122 | })); 123 | PSPDFReplaceMethod(objectClass, @selector(forwardInvocation:), forwardInvocationSEL, forwardInvocationIMP); 124 | } 125 | } 126 | } 127 | } 128 | 129 | /////////////////////////////////////////////////////////////////////////////////////////// 130 | #pragma mark - NSObject 131 | 132 | - (id)initWithTitle:(NSString *)title block:(void(^)())block { 133 | // Create a unique, still debuggable selector unique per PSMenuItem. 134 | NSString *strippedTitle = [[[title componentsSeparatedByCharactersInSet:[[NSCharacterSet letterCharacterSet] invertedSet]] componentsJoinedByString:@""] lowercaseString]; 135 | CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); 136 | NSString *uuidString = CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid)); 137 | CFRelease(uuid); 138 | SEL customSelector = NSSelectorFromString([NSString stringWithFormat:@"%@_%@_%@:", kMenuItemTrailer, strippedTitle, uuidString]); 139 | 140 | if((self = [super initWithTitle:title action:customSelector])) { 141 | self.customSelector = customSelector; 142 | _enabled = YES; 143 | _block = [block copy]; 144 | } 145 | return self; 146 | } 147 | 148 | /////////////////////////////////////////////////////////////////////////////////////////// 149 | #pragma mark - Public 150 | 151 | // Nils out action selector if we get disabled; auto-hides it from the UIMenuController. 152 | - (void)setEnabled:(BOOL)enabled { 153 | _enabled = enabled; 154 | self.action = enabled ? self.customSelector : NULL; 155 | } 156 | 157 | /////////////////////////////////////////////////////////////////////////////////////////// 158 | #pragma mark - Private 159 | 160 | // Trampuline executor. 161 | - (void)performBlock { 162 | if (_block) _block(); 163 | } 164 | 165 | @end 166 | -------------------------------------------------------------------------------- /PSMenuItem.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'PSMenuItem' 3 | s.version = '1.0.0' 4 | s.summary = 'A block based UIMenuItem subclass.' 5 | s.homepage = 'https://github.com/steipete/PSMenuItem' 6 | s.license = { 7 | :type => 'MIT', 8 | :file => 'LICENSE' 9 | } 10 | s.author = 'Peter Steinberger', 'steipete@gmail.com' 11 | s.source = { 12 | :git => 'https://github.com/steipete/PSMenuItem.git', 13 | :tag => s.version.to_s 14 | } 15 | s.platform = :ios, '4.3' 16 | s.source_files = '*.{h,m}' 17 | s.frameworks = 'UIKit' 18 | s.requires_arc = true 19 | end 20 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 781869D415B4CCFB00FA11BE /* LICENCE in Resources */ = {isa = PBXBuildFile; fileRef = 781869D315B4CCFB00FA11BE /* LICENCE */; }; 11 | 785B39EE15B4C01300F5DD0A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 785B39ED15B4C01300F5DD0A /* UIKit.framework */; }; 12 | 785B39F015B4C01300F5DD0A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 785B39EF15B4C01300F5DD0A /* Foundation.framework */; }; 13 | 785B39F215B4C01300F5DD0A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 785B39F115B4C01300F5DD0A /* CoreGraphics.framework */; }; 14 | 785B39F815B4C01300F5DD0A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 785B39F615B4C01300F5DD0A /* InfoPlist.strings */; }; 15 | 785B39FA15B4C01300F5DD0A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 785B39F915B4C01300F5DD0A /* main.m */; }; 16 | 785B39FE15B4C01300F5DD0A /* PSAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 785B39FD15B4C01300F5DD0A /* PSAppDelegate.m */; }; 17 | 785B3A0415B4C01300F5DD0A /* MainStoryboard_iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 785B3A0215B4C01300F5DD0A /* MainStoryboard_iPad.storyboard */; }; 18 | 785B3A0715B4C01300F5DD0A /* PSViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 785B3A0615B4C01300F5DD0A /* PSViewController.m */; }; 19 | 785B3A1415B4C2AB00F5DD0A /* PSMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 785B3A1315B4C2AB00F5DD0A /* PSMenuItem.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 781869D315B4CCFB00FA11BE /* LICENCE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENCE; sourceTree = SOURCE_ROOT; }; 24 | 785B39E915B4C01300F5DD0A /* PSPDFMenuItemDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PSPDFMenuItemDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 785B39ED15B4C01300F5DD0A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 26 | 785B39EF15B4C01300F5DD0A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 27 | 785B39F115B4C01300F5DD0A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 28 | 785B39F515B4C01300F5DD0A /* PSPDFMenuItemDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "PSPDFMenuItemDemo-Info.plist"; sourceTree = ""; }; 29 | 785B39F715B4C01300F5DD0A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 30 | 785B39F915B4C01300F5DD0A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 31 | 785B39FB15B4C01300F5DD0A /* PSPDFMenuItemDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PSPDFMenuItemDemo-Prefix.pch"; sourceTree = ""; }; 32 | 785B39FC15B4C01300F5DD0A /* PSAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSAppDelegate.h; sourceTree = ""; }; 33 | 785B39FD15B4C01300F5DD0A /* PSAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSAppDelegate.m; sourceTree = ""; }; 34 | 785B3A0315B4C01300F5DD0A /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard_iPad.storyboard; sourceTree = ""; }; 35 | 785B3A0515B4C01300F5DD0A /* PSViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSViewController.h; sourceTree = ""; }; 36 | 785B3A0615B4C01300F5DD0A /* PSViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSViewController.m; sourceTree = ""; }; 37 | 785B3A1215B4C2AB00F5DD0A /* PSMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PSMenuItem.h; path = ../PSMenuItem.h; sourceTree = ""; }; 38 | 785B3A1315B4C2AB00F5DD0A /* PSMenuItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PSMenuItem.m; path = ../PSMenuItem.m; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 785B39E615B4C01300F5DD0A /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | 785B39EE15B4C01300F5DD0A /* UIKit.framework in Frameworks */, 47 | 785B39F015B4C01300F5DD0A /* Foundation.framework in Frameworks */, 48 | 785B39F215B4C01300F5DD0A /* CoreGraphics.framework in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 785B39DE15B4C01300F5DD0A = { 56 | isa = PBXGroup; 57 | children = ( 58 | 785B39F315B4C01300F5DD0A /* PSMenuItemDemo */, 59 | 785B39EC15B4C01300F5DD0A /* Frameworks */, 60 | 785B39EA15B4C01300F5DD0A /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 785B39EA15B4C01300F5DD0A /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 785B39E915B4C01300F5DD0A /* PSPDFMenuItemDemo.app */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | 785B39EC15B4C01300F5DD0A /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 785B39ED15B4C01300F5DD0A /* UIKit.framework */, 76 | 785B39EF15B4C01300F5DD0A /* Foundation.framework */, 77 | 785B39F115B4C01300F5DD0A /* CoreGraphics.framework */, 78 | ); 79 | name = Frameworks; 80 | sourceTree = ""; 81 | }; 82 | 785B39F315B4C01300F5DD0A /* PSMenuItemDemo */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 785B3A1215B4C2AB00F5DD0A /* PSMenuItem.h */, 86 | 785B3A1315B4C2AB00F5DD0A /* PSMenuItem.m */, 87 | 781869D315B4CCFB00FA11BE /* LICENCE */, 88 | 785B3A1515B4C2B600F5DD0A /* Demo */, 89 | ); 90 | name = PSMenuItemDemo; 91 | path = PSPDFMenuItemDemo; 92 | sourceTree = ""; 93 | }; 94 | 785B39F415B4C01300F5DD0A /* Supporting Files */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 785B39F515B4C01300F5DD0A /* PSPDFMenuItemDemo-Info.plist */, 98 | 785B39F615B4C01300F5DD0A /* InfoPlist.strings */, 99 | 785B39F915B4C01300F5DD0A /* main.m */, 100 | 785B39FB15B4C01300F5DD0A /* PSPDFMenuItemDemo-Prefix.pch */, 101 | ); 102 | name = "Supporting Files"; 103 | sourceTree = ""; 104 | }; 105 | 785B3A1515B4C2B600F5DD0A /* Demo */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 785B39FC15B4C01300F5DD0A /* PSAppDelegate.h */, 109 | 785B39FD15B4C01300F5DD0A /* PSAppDelegate.m */, 110 | 785B3A0215B4C01300F5DD0A /* MainStoryboard_iPad.storyboard */, 111 | 785B3A0515B4C01300F5DD0A /* PSViewController.h */, 112 | 785B3A0615B4C01300F5DD0A /* PSViewController.m */, 113 | 785B39F415B4C01300F5DD0A /* Supporting Files */, 114 | ); 115 | name = Demo; 116 | sourceTree = ""; 117 | }; 118 | /* End PBXGroup section */ 119 | 120 | /* Begin PBXNativeTarget section */ 121 | 785B39E815B4C01300F5DD0A /* PSPDFMenuItemDemo */ = { 122 | isa = PBXNativeTarget; 123 | buildConfigurationList = 785B3A0A15B4C01300F5DD0A /* Build configuration list for PBXNativeTarget "PSPDFMenuItemDemo" */; 124 | buildPhases = ( 125 | 785B39E515B4C01300F5DD0A /* Sources */, 126 | 785B39E615B4C01300F5DD0A /* Frameworks */, 127 | 785B39E715B4C01300F5DD0A /* Resources */, 128 | ); 129 | buildRules = ( 130 | ); 131 | dependencies = ( 132 | ); 133 | name = PSPDFMenuItemDemo; 134 | productName = PSPDFMenuItemDemo; 135 | productReference = 785B39E915B4C01300F5DD0A /* PSPDFMenuItemDemo.app */; 136 | productType = "com.apple.product-type.application"; 137 | }; 138 | /* End PBXNativeTarget section */ 139 | 140 | /* Begin PBXProject section */ 141 | 785B39E015B4C01300F5DD0A /* Project object */ = { 142 | isa = PBXProject; 143 | attributes = { 144 | CLASSPREFIX = PS; 145 | LastUpgradeCheck = 0440; 146 | ORGANIZATIONNAME = "Peter Steinberger"; 147 | }; 148 | buildConfigurationList = 785B39E315B4C01300F5DD0A /* Build configuration list for PBXProject "PSPDFMenuItemDemo" */; 149 | compatibilityVersion = "Xcode 3.2"; 150 | developmentRegion = English; 151 | hasScannedForEncodings = 0; 152 | knownRegions = ( 153 | en, 154 | ); 155 | mainGroup = 785B39DE15B4C01300F5DD0A; 156 | productRefGroup = 785B39EA15B4C01300F5DD0A /* Products */; 157 | projectDirPath = ""; 158 | projectRoot = ""; 159 | targets = ( 160 | 785B39E815B4C01300F5DD0A /* PSPDFMenuItemDemo */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 785B39E715B4C01300F5DD0A /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 785B39F815B4C01300F5DD0A /* InfoPlist.strings in Resources */, 171 | 785B3A0415B4C01300F5DD0A /* MainStoryboard_iPad.storyboard in Resources */, 172 | 781869D415B4CCFB00FA11BE /* LICENCE in Resources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXResourcesBuildPhase section */ 177 | 178 | /* Begin PBXSourcesBuildPhase section */ 179 | 785B39E515B4C01300F5DD0A /* Sources */ = { 180 | isa = PBXSourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 785B39FA15B4C01300F5DD0A /* main.m in Sources */, 184 | 785B39FE15B4C01300F5DD0A /* PSAppDelegate.m in Sources */, 185 | 785B3A0715B4C01300F5DD0A /* PSViewController.m in Sources */, 186 | 785B3A1415B4C2AB00F5DD0A /* PSMenuItem.m in Sources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXSourcesBuildPhase section */ 191 | 192 | /* Begin PBXVariantGroup section */ 193 | 785B39F615B4C01300F5DD0A /* InfoPlist.strings */ = { 194 | isa = PBXVariantGroup; 195 | children = ( 196 | 785B39F715B4C01300F5DD0A /* en */, 197 | ); 198 | name = InfoPlist.strings; 199 | sourceTree = ""; 200 | }; 201 | 785B3A0215B4C01300F5DD0A /* MainStoryboard_iPad.storyboard */ = { 202 | isa = PBXVariantGroup; 203 | children = ( 204 | 785B3A0315B4C01300F5DD0A /* en */, 205 | ); 206 | name = MainStoryboard_iPad.storyboard; 207 | sourceTree = ""; 208 | }; 209 | /* End PBXVariantGroup section */ 210 | 211 | /* Begin XCBuildConfiguration section */ 212 | 785B3A0815B4C01300F5DD0A /* Debug */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 217 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 218 | CLANG_ENABLE_OBJC_ARC = YES; 219 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 220 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 221 | COPY_PHASE_STRIP = NO; 222 | GCC_C_LANGUAGE_STANDARD = gnu99; 223 | GCC_DYNAMIC_NO_PIC = NO; 224 | GCC_OPTIMIZATION_LEVEL = 0; 225 | GCC_PREPROCESSOR_DEFINITIONS = ( 226 | "DEBUG=1", 227 | "$(inherited)", 228 | ); 229 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 230 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 231 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 232 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 233 | GCC_WARN_UNUSED_VARIABLE = YES; 234 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 235 | SDKROOT = iphoneos; 236 | TARGETED_DEVICE_FAMILY = "1,2"; 237 | }; 238 | name = Debug; 239 | }; 240 | 785B3A0915B4C01300F5DD0A /* Release */ = { 241 | isa = XCBuildConfiguration; 242 | buildSettings = { 243 | ALWAYS_SEARCH_USER_PATHS = NO; 244 | ARCHS = "$(ARCHS_STANDARD_32_BIT)"; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 248 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 249 | COPY_PHASE_STRIP = YES; 250 | GCC_C_LANGUAGE_STANDARD = gnu99; 251 | GCC_VERSION = com.apple.compilers.llvm.clang.1_0; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 253 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 254 | GCC_WARN_UNUSED_VARIABLE = YES; 255 | IPHONEOS_DEPLOYMENT_TARGET = 5.1; 256 | OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; 257 | SDKROOT = iphoneos; 258 | TARGETED_DEVICE_FAMILY = "1,2"; 259 | VALIDATE_PRODUCT = YES; 260 | }; 261 | name = Release; 262 | }; 263 | 785B3A0B15B4C01300F5DD0A /* Debug */ = { 264 | isa = XCBuildConfiguration; 265 | buildSettings = { 266 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 267 | GCC_PREFIX_HEADER = "PSPDFMenuItemDemo/PSPDFMenuItemDemo-Prefix.pch"; 268 | INFOPLIST_FILE = "PSPDFMenuItemDemo/PSPDFMenuItemDemo-Info.plist"; 269 | PRODUCT_NAME = "$(TARGET_NAME)"; 270 | TARGETED_DEVICE_FAMILY = 2; 271 | WRAPPER_EXTENSION = app; 272 | }; 273 | name = Debug; 274 | }; 275 | 785B3A0C15B4C01300F5DD0A /* Release */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 279 | GCC_PREFIX_HEADER = "PSPDFMenuItemDemo/PSPDFMenuItemDemo-Prefix.pch"; 280 | INFOPLIST_FILE = "PSPDFMenuItemDemo/PSPDFMenuItemDemo-Info.plist"; 281 | PRODUCT_NAME = "$(TARGET_NAME)"; 282 | TARGETED_DEVICE_FAMILY = 2; 283 | WRAPPER_EXTENSION = app; 284 | }; 285 | name = Release; 286 | }; 287 | /* End XCBuildConfiguration section */ 288 | 289 | /* Begin XCConfigurationList section */ 290 | 785B39E315B4C01300F5DD0A /* Build configuration list for PBXProject "PSPDFMenuItemDemo" */ = { 291 | isa = XCConfigurationList; 292 | buildConfigurations = ( 293 | 785B3A0815B4C01300F5DD0A /* Debug */, 294 | 785B3A0915B4C01300F5DD0A /* Release */, 295 | ); 296 | defaultConfigurationIsVisible = 0; 297 | defaultConfigurationName = Release; 298 | }; 299 | 785B3A0A15B4C01300F5DD0A /* Build configuration list for PBXNativeTarget "PSPDFMenuItemDemo" */ = { 300 | isa = XCConfigurationList; 301 | buildConfigurations = ( 302 | 785B3A0B15B4C01300F5DD0A /* Debug */, 303 | 785B3A0C15B4C01300F5DD0A /* Release */, 304 | ); 305 | defaultConfigurationIsVisible = 0; 306 | defaultConfigurationName = Release; 307 | }; 308 | /* End XCConfigurationList section */ 309 | }; 310 | rootObject = 785B39E015B4C01300F5DD0A /* Project object */; 311 | } 312 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSAppDelegate.h 3 | // PSMenuItemDemo 4 | // 5 | // Copyright (c) 2012 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface PSAppDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow *window; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSAppDelegate.m 3 | // PSMenuItemDemo 4 | // 5 | // Copyright (c) 2012 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | #import "PSAppDelegate.h" 9 | 10 | @implementation PSAppDelegate 11 | 12 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 13 | return YES; 14 | } 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSPDFMenuItemDemo-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ${PRODUCT_NAME} 9 | CFBundleExecutable 10 | ${EXECUTABLE_NAME} 11 | CFBundleIdentifier 12 | com.petersteinberger.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | UIMainStoryboardFile~ipad 28 | MainStoryboard_iPad 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSPDFMenuItemDemo-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'PSMenuItemDemo' target in the 'PSMenuItemDemo' project 3 | // 4 | 5 | #import 6 | 7 | #ifndef __IPHONE_5_0 8 | #warning "This project uses features only available in iOS SDK 5.0 and later." 9 | #endif 10 | 11 | #ifdef __OBJC__ 12 | #import 13 | #import 14 | #endif 15 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // PSViewController.h 3 | // PSMenuItemDemo 4 | // 5 | // Copyright (c) 2012 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | @interface PSViewController : UIViewController 11 | 12 | - (IBAction)buttonPressed:(UIButton *)button; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/PSViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // PSViewController.m 3 | // PSMenuItemDemo 4 | // 5 | // Copyright (c) 2012 Peter Steinberger. All rights reserved. 6 | // This is code from http://pspdfkit.com. 7 | // 8 | 9 | #import "PSViewController.h" 10 | #import "PSMenuItem.h" 11 | 12 | @implementation PSViewController 13 | 14 | // add support for PSMenuItem. Needs to be called once per class. 15 | + (void)load { 16 | [PSMenuItem installMenuHandlerForObject:self]; 17 | } 18 | 19 | + (void)initialize { 20 | [PSMenuItem installMenuHandlerForObject:self]; 21 | } 22 | 23 | - (IBAction)buttonPressed:(UIButton *)button { 24 | PSMenuItem *actionItem = [[PSMenuItem alloc] initWithTitle:@"Action 1" block:^{ 25 | [[[UIAlertView alloc] initWithTitle:@"Action 1" message:@"From a block!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; 26 | }]; 27 | 28 | PSMenuItem *action2Item = [[PSMenuItem alloc] initWithTitle:@"Action 2" block:^{ 29 | [[[UIAlertView alloc] initWithTitle:@"Action 2" message:@"From a block!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; 30 | }]; 31 | 32 | PSMenuItem *submenuItem = [[PSMenuItem alloc] initWithTitle:@"Submenu..." block:^{ 33 | [UIMenuController sharedMenuController].menuItems = @[ 34 | [[PSMenuItem alloc] initWithTitle:@"Back..." block:^{ 35 | [self buttonPressed:button]; 36 | }], 37 | [[PSMenuItem alloc] initWithTitle:@"Sub 1" block:NULL], 38 | [[PSMenuItem alloc] initWithTitle:@"Sub 2" block:^{ 39 | NSLog(@"Sub 2 pressed."); 40 | }]]; 41 | 42 | [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; 43 | }]; 44 | 45 | [UIMenuController sharedMenuController].menuItems = @[actionItem, action2Item, submenuItem]; 46 | [[UIMenuController sharedMenuController] setTargetRect:button.bounds inView:button]; 47 | [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/en.lproj/MainStoryboard_iPad.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /PSPDFMenuItemDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // PSMenuItemDemo 4 | // 5 | // Created by Peter Steinberger on 7/16/12. 6 | // Copyright (c) 2012 Peter Steinberger. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "PSAppDelegate.h" 12 | 13 | int main(int argc, char *argv[]) 14 | { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([PSAppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PSMenuItem 2 | ============= 3 | 4 | A block based UIMenuItem subclass. 5 | 6 | The inflexible @selector based approach in UIMenuItem was driving me crazy. 7 | I searched quite a while for a block-based UIMenuItem, but couldn't find one. 8 | So finally, I sat down and wrote my own implementation for [my iOS PDF framework PSPDFKit](http://pspdfkit.com). 9 | 10 | If you are as annoyed about the missing target/action pattern, as I am, you will *love* this. [Also read the in-depth article on my website.](http://petersteinberger.com/blog/2012/hacking-block-support-into-uimenuitem/) 11 | 12 | ## How to use 13 | ``` objective-c 14 | // one time-call needed to prepare the block based PSMenuItem handler. 15 | [PSMenuItem installMenuHandlerForObject:button]; 16 | 17 | PSMenuItem *actionItem = [[PSMenuItem alloc] initWithTitle:@"Action 1" block:^{ 18 | [[[UIAlertView alloc] initWithTitle:@"Message" message:@"From a block!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; 19 | }]; 20 | 21 | PSMenuItem *submenuItem = [[PSMenuItem alloc] initWithTitle:@"Submenu..." block:^{ 22 | [UIMenuController sharedMenuController].menuItems = @[ 23 | [[PSMenuItem alloc] initWithTitle:@"Back..." block:^{ 24 | [self buttonPressed:button]; 25 | }], 26 | [[PSMenuItem alloc] initWithTitle:@"Sub 1" block:^{ 27 | NSLog(@"Sub 1 pressed"); 28 | }]]; 29 | [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; 30 | }]; 31 | 32 | [UIMenuController sharedMenuController].menuItems = @[actionItem, submenuItem]; 33 | [[UIMenuController sharedMenuController] setTargetRect:button.bounds inView:button]; 34 | [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; 35 | ``` 36 | 37 | PSMenuItem uses ARC and is tested with Xcode 4.4 and 4.5 (iOS 4.3+) 38 | 39 | The code looks a bit scary and involves swizzling certain methods, but it's actually not that bad. No private API is used, and it's highly unlikely that Apple ever changes something as basic as the UIResponder chain. 40 | 41 | ## Creator 42 | 43 | [Peter Steinberger](http://github.com/steipete) 44 | [@steipete](https://twitter.com/steipete) 45 | 46 | I'd love a thank you tweet if you find this useful. 47 | 48 | ## License 49 | 50 | PSMenuItem is available under the MIT license. See the LICENSE file for more info. 51 | --------------------------------------------------------------------------------