Customising NSAlert: accessory views

Published
2013-12-18
Tagged

For a well-documented feature, NSAlert‘s accessoryView feature takes a little bit of fiddling to implement. NSAlert is behind the standard pop-up modal alert window that’s familiar to everyone, often popping into the foreground and grabbing your attention, refusing to go away until you acknowledge its existence:

A typical self-indulgent NSAlert box. This one is strangely self-aware

NSAlert boxes can, by default, display any number of buttons (c.f. the classic “Save/Delete/Cancel” triumvirate), but advanced features require a little more jiggery-pokery. This mainly comes in the form of its accessoryView property, which allows you to add an NSView to the alert proper. At first you might be tempted to programatically create an NSView, adding the required elements in init methods and the like, but it turns out it’s actually a bit easier to use nib files to hold all of that. This is how to do so.

In XCode, create a new file, and make it an NSView nib:

Selecting “view” under the User Interface tab.

Now you can add elements to your heart’s content. I’ve set mine up with a text box and a check box, which should do for a nice-looking NSAlert:

My custom NSView, ready for inclusion into an NSAlert

Now there’s the question of how we access this nib when we need it. What we need to do is assign responsibility of loading this nib to some class in our program. It doesn’t matter which class ends up doing this: you could make it a WindowController’s job, or you could make a completely new class to “wrap” this custom NSAlert if you wanted. For purposes of this example I’ve got my NSAppDelegate subclass to do all the heavy lifting, which is a terrible idea.

The important thing is that this class gets to be the File's Owner for your custom NSView. In XCode, select “File’s Owner” from the bar on the left, then in the identity inspector on the right (⌘+⌥+3) replace the Class field with your chosen owner:

My custom view now knows its owner will be a JRAppDelegate.

Now your nib knows who its owner will be, but the owner still doesn’t know that it should own a nib. Open up the class you chose as file’s owner1 and in the header file, add an IBOutlet:

1
@interface JRAppDelegate : NSObject <NSApplicationDelegate> {
2
    IBOutlet NSView *customView;
3
    IBOutlet NSTextField *stringField;
4
    IBOutlet NSButton *checkbox;
5
}

I’ve added an IBOutlet for the view, as well as one for the string field and checkbox. I figure I’ll want to grab these values later, although you could just as easily use bindings and properties to grab said values. Whatever’s your poison.

Now back in the nib file, we link up our owner and our custom view:

Linking the owner and custom view.

Obviously, you can link up anything else you want to right now.

At this stage, finally, the owning class and the nib are sufficiently aware of each others’ existence that we can actually do something. For ease of testing, I’m placing the following code in -[JRAppDelegate applicationDidFinishLaunching:], which runs pretty much as soon as the app starts:

1
NSAlert *a = [[NSAlert alloc] init];
2
a.messageText = @"Hello!";
3
a.informativeText = @"I'm an NSAlert.";
4
[a runModal];
5
[NSApp terminate:self];

This produces the alert we saw at the top of the post. Let’s add an accessoryView:

1
NSAlert *a = [[NSAlert alloc] init];
2
a.messageText = @"Hello!";
3
a.informativeText = @"I'm an NSAlert.";
4
a.accessoryView = customView; // <--
5
[a runModal];
6
[NSApp terminate:self];

But! If you do this and run your app, you’ll find that while the alert box pops up, your view isn’t included. If I set a breakpoint on the line marked // <-- above, we can soon see that customView isn’t even assigned at this point:

customView is nil. How sad.

It turns out that simply assigning something as an IBOutlet doesn’t result in its magical instantiation at runtime: Cocoa may be magic, but it’s not that magic. Instead, we have to do the alloc/init cycle ourselves, using NSBundle:

1
NSAlert *a = [[NSAlert alloc] init];
2
a.messageText = @"Hello!";
3
a.informativeText = @"I'm an NSAlert.";
4
//Replace @"CustomView" with the name of your custom view nib
5
[NSBundle.mainBundle loadNibNamed:@"CustomView" owner:self topLevelObjects:nil];
6
a.accessoryView = customView;
7
[a runModal];
8
[NSApp terminate:self];

If we run this, finally we find our custom view occupying pride of place in the centre of our NSAlert window:

Although obviously I need to make my custom view a bit wider.

And what a sweet child it is.

More information

Apple has a handy little help file on custom alert dialogues, which at least tells you what they’re for and some basics on how to make them.


  1. Now would be a really great time to mention that you can open any file in XCode quickly by typing ⌘+⇧+O. This is the equivalent of SublimeText’s ⌘+T trick, and saves a heap of time.