Skip to content

Instantly share code, notes, and snippets.

@JensAyton
Created September 21, 2010 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JensAyton/589740 to your computer and use it in GitHub Desktop.
Save JensAyton/589740 to your computer and use it in GitHub Desktop.
Hack to enable the use of block ivars as IBAction targets. Stuffs a bunch of nastiness in NSObject, but transparenty works for classes implemeting actions: create a property of type BlockAction, assign a block to it, and use the property name (with a trai
/*
Hack to enable the use of block ivars as IBAction targets. Stuffs a bunch
of nastiness in NSObject, but transparenty works for classes implemeting
actions: create a property of type BlockAction, assign a block to it,
and use the property name (with a trailing colon) as the target selector.
Did I mention it's a hack? There are two major problems:
* Type safety. It assumes any block property will have the correct
signature. Can be partially worked around when compiling with Clang,
which embeds @encode() strings into blocks, but this is undocumented.
Also, @encode() strings are unreliable, but they're sufficient to
recognise valid actions. If you want to try this, see
http://github.com/mikeash/MABlockClosure
* It introduces slow code into some core runtime methods. This could be
alleviated with a cache.
Why? Because of this:
http://stackoverflow.com/questions/3738611/stored-block-closure-as-ibaction/
*/
#import <objc/runtime.h>
typedef void(^BlockAction)(id sender);
@interface BlockActionTestAppDelegate : NSObject <NSApplicationDelegate>
{
NSWindow *window;
BlockAction testAction;
}
@property (assign) IBOutlet NSWindow *window;
@property (copy) BlockAction testAction;
@end
@interface BlockActionTestAppDelegate (BlockActions)
// Note that this isn't implemented anywhere.
- (IBAction) testAction:(id)sender;
@end
@interface NSObject (BlockActions)
+ (void) ja_blockActionsInit;
- (BOOL) ja_blockActionsRespondsToSelector:(SEL)selector;
@end
@implementation BlockActionTestAppDelegate
@synthesize window, testAction;
+ (void) initialize
{
[NSObject ja_blockActionsInit];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.testAction = ^(id sender){ NSLog(@"Test action with sender: %@", sender); };
}
@end
@implementation NSObject (BlockActions)
static BOOL inited;
- (SEL) ja_blockActionsPropertySelectorForActionSelector:(SEL)selector
{
// If I was considering using this in real code, which I'm not, I'd use a per-class cache.
assert(inited);
const char *name = sel_getName(selector);
size_t index = 0;
// Ensure that it's a unary method, i.e. there is exactly one colon at the end.
for (;;)
{
char curr = name[index++];
if (curr == ':')
{
if (name[index] == '\0') break;
else return NULL;
}
else if (curr == '\0') return NULL;
}
// Create corresponding selector without trailing colon.
char propName[index];
memcpy(propName, selector, index - 1);
propName[index - 1] = '\0';
SEL propSel = sel_getUid(propName);
if ([self ja_blockActionsRespondsToSelector:propSel])
{
/* Check if it's a property of block type.
Does not check type signature of the block. This is possible, but
undocumented, when using Clang. For details, see
http://github.com/mikeash/MABlockClosure
*/
objc_property_t propInfo = class_getProperty([self class], propName);
if (propInfo != NULL)
{
NSString *propAttrStr = [NSString stringWithUTF8String:property_getAttributes(propInfo)];
if (propAttrStr.length > 0)
{
NSArray *propAttrs = [propAttrStr componentsSeparatedByString:@","];
for (NSString *attr in propAttrs)
{
if ([attr isEqualToString:@"T@?"])
{
return propSel;
}
}
}
}
}
return NULL;
}
- (void) ja_blockActionsForwardInvocation:(NSInvocation *)invocation
{
SEL propSel = [self ja_blockActionsPropertySelectorForActionSelector:invocation.selector];
if (propSel != NULL)
{
BlockAction action = [self performSelector:propSel];
id sender;
[invocation getArgument:&sender atIndex:2];
action(sender);
}
else
{
[self ja_blockActionsForwardInvocation:invocation];
}
}
- (BOOL) ja_blockActionsRespondsToSelector:(SEL)selector
{
BOOL exists = [self ja_blockActionsRespondsToSelector:selector];
if (!exists)
{
exists = [self ja_blockActionsPropertySelectorForActionSelector:selector] != NULL;
}
return exists;
}
- (IBAction) ja_blockActionsActionSignature:(id)sender
{
}
- (NSMethodSignature *) ja_blockActionsMethodSignatureForSelector:(SEL)selector
{
NSMethodSignature *result = [self ja_blockActionsMethodSignatureForSelector:selector];
if (result == nil && [self ja_blockActionsPropertySelectorForActionSelector:selector] != NULL)
{
result = [self ja_blockActionsMethodSignatureForSelector:@selector(ja_blockActionsActionSignature:)];
}
return result;
}
// Lifted straight from http://www.cocoadev.com/index.pl?MethodSwizzling
static void Swizzle(Class c, SEL orig, SEL new)
{
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
{
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}
else
{
method_exchangeImplementations(origMethod, newMethod);
}
}
+ (void) ja_blockActionsInit
{
if (!inited)
{
inited = YES;
Class nsobject = [NSObject class];
Swizzle(nsobject, @selector(forwardInvocation:), @selector(ja_blockActionsForwardInvocation:));
Swizzle(nsobject, @selector(respondsToSelector:), @selector(ja_blockActionsRespondsToSelector:));
Swizzle(nsobject, @selector(methodSignatureForSelector:), @selector(ja_blockActionsMethodSignatureForSelector:));
}
}
@end
@JensAyton
Copy link
Author

I’d completely forgotten about +resolveInstanceMethod:, that is indeed a better approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment