views:

310

answers:

2

I'd like to adjust the NSApplicationIcon image that gets shown automatically in all alerts to be something different than what is in the app bundle.

I know that it's possible to set the dock icon with [NSApplication setApplicationIconImage:] -- but this only affects the dock, and nothing else.

I'm able to work around this issue some of the time: I have an NSAlert *, I can call setIcon: to display my alternate image.

Unfortunately, I have a lot of nibs that have NSImageView's with NSApplicationIcon, that I would like to affect, and it would be a hassle to create outlets and put in code to change the icon. And for any alerts that I'm bringing up with the BeginAlert... type calls (which don't give an NSAlert object to muck with), I'm completely out of luck.

Can anybody think of a reasonable way to globally (for the life of a running application) override the NSApplicationIcon that is used by AppKit, with my own image, so that I can get 100% of the alerts replaced (and make my code simpler)?

A: 

Swizzle the [NSImage imageNamed:] method? This method works at least on Snow Leopard, YMMV.

In an NSImage category:

@implementation NSImage (Magic)

+ (void)load {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // have to call imageNamed: once prior to swizzling to avoid infinite loop
    [[NSApplication sharedApplication] applicationIconImage];

    // swizzle!
    NSError *error = nil;

    if (![NSImage jr_swizzleClassMethod:@selector(imageNamed:) withClassMethod:@selector(_sensible_imageNamed:) error:&error])
        NSLog(@"couldn't swizzle imageNamed: application icons will not update: %@", error);

    [pool release];
}


+ (id)_sensible_imageNamed:(NSString *)name {
    if ([name isEqualToString:@"NSApplicationIcon"])
        return [[NSApplication sharedApplication] applicationIconImage];

    return [self _sensible_imageNamed:name];
}

@end

With this hacked up (untested, just wrote it) jr_swizzleClassMethod:... implementation:

+ (BOOL)jr_swizzleClassMethod:(SEL)origSel_ withClassMethod:(SEL)altSel_ error:(NSError**)error_ {
#if OBJC_API_VERSION >= 2
    Method origMethod = class_getClassMethod(self, origSel_);
    if (!origMethod) {
     SetNSError(error_, @"original method %@ not found for class %@", NSStringFromSelector(origSel_), [self className]);
     return NO;
    }

    Method altMethod = class_getClassMethod(self, altSel_);
    if (!altMethod) {
     SetNSError(error_, @"alternate method %@ not found for class %@", NSStringFromSelector(altSel_), [self className]);
     return NO;
    }

    id metaClass = objc_getMetaClass(class_getName(self));

    class_addMethod(metaClass,
                    origSel_,
                    class_getMethodImplementation(metaClass, origSel_),
                    method_getTypeEncoding(origMethod));
    class_addMethod(metaClass,
                    altSel_,
                    class_getMethodImplementation(metaClass, altSel_),
                    method_getTypeEncoding(altMethod));

    method_exchangeImplementations(class_getClassMethod(self, origSel_), class_getClassMethod(self, altSel_));
    return YES;
#else
    assert(0);
    return NO;
#endif
}

Then, this method to illustrate the point:

- (void)doMagic:(id)sender {
    static int i = 0;

    i = (i+1) % 2;

    if (i)
        [[NSApplication sharedApplication] setApplicationIconImage:[NSImage imageNamed:NSImageNameBonjour]];
    else
        [[NSApplication sharedApplication] setApplicationIconImage:[NSImage imageNamed:NSImageNameDotMac]];

    // any pre-populated image views have to be set to nil first, otherwise their icon won't change
    // [imageView setImage:nil];
    // [imageView setImage:[NSImage imageNamed:NSImageNameApplicationIcon]];

    NSAlert *alert = [[[NSAlert alloc] init] autorelease];
    [alert setMessageText:@"Shazam!"];
    [alert runModal];
}

A couple of caveats:

  1. Any image view already created must have setImage: called twice, as seen above to register the image changing. Don't know why.
  2. There may be a better way to force the initial imageNamed: call with @"NSApplicationIcon" than how I've done it.
Ashley Clark
A: 

Try [myImage setName:@"NSApplicationIcon"] (after setting it as the application icon image in NSApp).

Note: On 10.6 and later, you can and should use NSImageNameApplicationIcon instead of the string literal @"NSApplicationIcon".

Peter Hosey
That doesn't override an already existing named image: "If the receiver is already registered under a different name, this method unregisters the other name. If a different image is registered under the name specified in aString, this method does nothing and returns NO." Unfortunately, calling setName:nil or @"" on the preexisting app icon image doesn't appear to change things either.
Ashley Clark
Also, setApplicationIconImage: doesn't replace the existing object as one might think, it modifies the representations contained within it.Ideally, there would be a notification associated with the image rep changing that NSImageViews could be observing and redraw themselves as necessary.
Ashley Clark