danwood
danwood

Reputation: 1512

Changing NSApplicationIcon across a running application?

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)?

Upvotes: 3

Views: 2143

Answers (2)

Peter Hosey
Peter Hosey

Reputation: 96373

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".

Upvotes: 1

Ashley Clark
Ashley Clark

Reputation: 8823

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.

Upvotes: 2

Related Questions