4

I'm working on making a plugin for Mail.app. I'd like the plugin to add a button to Mail's toolbar. To do this, I decided the best approach would be to call the function to add this button in the MessageViewer's initialize method (MessageViewer is the class for Mail.app's FirstResponder). The code I'm modifying seems to work nicely:

+ (void) initialize
{  
[super initialize];

//  class_setSuperclass([self class], NSClassFromString(@"MVMailBundle"));
//  [ArchiveMailBundle registerBundle];


// Add a couple methods to the MessageViewer class.
Class MessageViewer = NSClassFromString(@"MessageViewer");

// swizzleSuccess should be NO if any of the following three calls fail
BOOL swizzleSuccess = YES;
swizzleSuccess &= [[self class] copyMethod:@selector(_specialValidateMenuItem:) 
                                 fromClass:[self class] 
                                   toClass:MessageViewer];
swizzleSuccess &= [[self class] copyMethod:@selector(unsubscribeSelectedMessages:) 
                                 fromClass:[self class] 
                                   toClass:MessageViewer];

However, when I try to do the same, it doesn't work:

//  //Copy the method to the original MessageViewer
swizzleSuccess &= [[self class] copyMethod:@selector(_specialInitMessageViewer:)
                                 fromClass:[self class]
                                   toClass:MessageViewer];  

Here are the swizzling methods:

+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel inClass:(Class)cls
{
// For class (cls), swizzle the original selector with the new selector.
    //debug lines to try to figure out why swizzling is failing.
//  if (!cls  ||  !origSel) {
    //          NSLog(@"Something was null.  Uh oh.");
    //}
Method origMethod = class_getInstanceMethod(cls, origSel);

if (!origMethod) {
    NSLog(@"Swizzler -- original method %@ not found for class %@", NSStringFromSelector(origSel), 
          [cls className]);
    return NO;
}
    //if (!altSel) NSLog(@"altSel null.  :(");
Method altMethod = class_getInstanceMethod(cls, altSel);
if (!altMethod) {
    NSLog(@"Swizzler -- alternate method %@ not found for class %@", NSStringFromSelector(altSel), 
          [cls className]);
    return NO;
}

method_exchangeImplementations(origMethod, altMethod);

return YES;

}


+ (BOOL) copyMethod:(SEL)sel fromClass:(Class)fromCls toClass:(Class)toCls
{
    // copy a method from one class to another.
    Method method = class_getInstanceMethod(fromCls, sel);
    if (!method)
    {
        NSLog(@"copyMethod-- method %@ could not be found in class %@", NSStringFromSelector(sel),
              [fromCls className]);
        return NO;
    }
    class_addMethod(toCls, sel, 
                    class_getMethodImplementation(fromCls, sel), 
                    method_getTypeEncoding(method));
    return YES;
}

It seems to be failing in the call to class_getInstanceMethod, because I get an error in the log. This happens for both the method inside of my own class, as well as for the MessageViewer's initialize method.

Are there some gotchas I'm not taking into account here?

Jacob Relkin
  • 161,348
  • 33
  • 346
  • 320
Aaron
  • 879
  • 7
  • 18
  • Your code doesn't format properly and looks to be a bit of a mess. You need each code line to be intended by at least 4 spaces for it to not get mangled. – Shaggy Frog Dec 08 '10 at 02:10

2 Answers2

9

If you want to swizzle class methods, then why are you using the class_getInstanceMethod() function instead of class_getClassMethod()?

NSResponder
  • 16,861
  • 7
  • 32
  • 46
5

Instead of trying to implement swizzling manually, you should just use JRSwizzle. One word of caution, JRSwizzle's implementation of +jr_swizzleClassMethod:withClassMethod:error: is just a stub, but you can simply call +jr_swizzleMethod:withMethod:error on the class's metaclass instead (e.g. just pass klass->isa instead of klass (where klass is the class you're swizzling)).

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • Thanks, Kevin, the JRSwizzle package looks awesome! Now, am I still responsible for copying my class method into the class in which I want to swizzle, or is there a way I can indicate this in the jr_swizzleClassMethod call? (my apologies if it's obvious; I'm very new to ObjC) – Aaron Dec 08 '10 at 18:17
  • `jr_swizzleMethod:withMethod:error:` takes care of everything for you. It copies your method into the target class and swaps the IMP with the method you're trying to override. – Lily Ballard Dec 08 '10 at 21:11
  • Okay, so suppose I have a method newMethod in class Foo and I want to swizzle it with method origMethod in class Bar. How do I notate which methods in which classes I'm switching? – Aaron Dec 09 '10 at 01:05
  • 1
    I'm sorry, that was a bit misleading. Copying only occurs if you're overriding a method from a superclass. Typically when swizzling methods, you write your new method in a category on the original class. Trying to copy methods between classes doesn't make any sense, as the compiler will compile the original method with the wrong class in mind. You should just write your `newMethod` on a category of class Bar, then swizzle it. – Lily Ballard Dec 09 '10 at 01:26
  • Okay, that makes much more sense. I know it's weird to be copying a method to another class altogether but I'm writing a plugin for Mail.app so I lack options since it doesn't have a proper plugin architecture. I'll try this. Thanks again! – Aaron Dec 09 '10 at 03:23
  • If your problem is you can't write a category on the class directly because you can't link against that class directly, you could try writing your category on NSObject (or on another class in the superclass chain that you do have access to). – Lily Ballard Dec 09 '10 at 12:46
  • Hi Kevin, I returned to trying to swizzle this method and it's still a bit of a struggle. I seem to be able to swizzle if I create a category of NSResponder, which is a superclass of the MessageViewer class. However, I need to touch instance variables of the MessageViewer itself (namely, the toolbar instance variable). Do you know if there exists a clever way I could pull that off? – Aaron Dec 15 '10 at 20:00
  • There are runtime methods you can use to get at the instance variables, declared in ``. `object_getIvar` and `object_setIvar` are the newer methods (available in 10.5 and later), and `object_getInstanceVariable`/`object_setInstanceVariable` are the older ones. – Lily Ballard Jan 01 '11 at 22:07