Search code examples
objective-cxcodeobjective-c-runtimescripting-bridgeobjective-c-category

Extend iTunesApplication class with Categories


I am just learning how to use ScriptingBridges. I made a method that slowly fades the volume on iTunes, and would like to make it a category so I can do the following:

iTunesApplication* iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
[iTunes lowerVolume:50 speed:1];

I made another category for NSSpeechSynthesizer that works, but I can't get this one to. I keep getting the following build error:

"_OBJC_CLASS_$_iTunesApplication", referenced from:
l_OBJC_$_CATEGORY_iTunesApplication_$_iTunesApplicationAdditions in iTunesApplication.o
objc-class-ref-to-iTunesApplication in iTunesApplication.o
ld: symbol(s) not found
collect2: ld returned 1 exit status

Is there something special I can do to make it work since I can't include the symbols?

Thanks,
Ryan Pendleton

UPDATE: I only found one solution, which is below. It involves MethodSwizzling, so I'm open to better answers, but for now it's all I have.


Solution

  • The solution I found was to use the Objective-C runtime API. I'm sure there's a better way to organize this, but here's how I did it:

    Here are my .h and .m files for creating the category. Notice how lowerVolume is not an actual method, but a C function with the arguments id self, and SEL _CMD. You'll also notice a setupCategories function. We'll call that later.

    // iTunes+Volume.h
    
    #import <objc/runtime.h>
    #import "iTunes.h"
    
    void lowerVolume(id self, SEL _cmd, int dest, float speed);
    void setupCategories();
    
    @interface iTunesApplication (Volume)
    
    - (void)lowerVolume:(int)dest speed:(float)speed;
    
    @end
    
    // iTunes+Volume.m
    
    #import "iTunes+Volume.h"
    
    void lowerVolume(id self, SEL _cmd, int dest, float speed)
    {
        NSLog(@"Lower Volume: %i, %f", dest, speed);
    }
    
    void setupCategories()
    {
        id object = [[SBApplication alloc] initWithBundleIdentifier:@"com.apple.iTunes"];
        Class class = [object class];
        [object release];
    
        class_addMethod(class, @selector(lowerVolume:speed:), (IMP)lowerVolume, "@:if");
    }
    

    Now that I've made the functions, I need to actually add them to the scripting bridge class using the Objective-C runtime API. I'll do this in main.m to make sure that the methods are ready to be used when the run loop starts.

    // main.m
    
    #import <Cocoa/Cocoa.h>
    #import "iTunes+Volume.h"
    
    int main(int argc, char *argv[])
    {
        NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    
        setupCategories();
        return NSApplicationMain(argc,  (const char **) argv);
    
        [pool drain];
    }
    

    Now, I can use my method wherever I want as long as I include the header files:

    - (void)mute
    {
        iTunesApplication* iTunes = [[SBApplication alloc] initWithBundleIdentifier:@"com.apple.iTunes"];
        [iTunes lowerVolume:0 speed:1];
        [iTunes release];
    }
    

    If any of this doesn't make sense, just tell me and I'll try to explain it better.