In my post on macOS Storyboard Segues I said I’d look at sheets in more details, and now is as good a time as any. I meant to look at sheet segues in this post, but then I worked through all of the permutations of NSAlert, and 3500 words later, I’ve decided to put them into a separate post at a later time. This is a long tutorial. On the other hand, it’s a really good starting point if you’re not overly familar with the Cocoa frameworks: you need next to no Swift knowledge, but I’ll go through A LOT of typical Cocoa patterns and numerous controls.

Preliminaries

Apple has a whole Sheet Programming Guide, which I found problematic: it mentions did-end and did-dismiss selectors and delegates… which is a little bit of a red herring. In fact, it’s a barrel of herrings-of-all-colours that refers to

… which was deprecated in osX 10.10. As of writing, the current version of macOS is 10.13, (even though I still use and target 10.12) and a good many people who started programming with Swift will never have seen this method (or, indeed, much Objective-C code.)

The SheetProgrammingGuide mentions two ways of creating alerts – through functions (disrecommended since 10.3, which was released in 2001, deprecated properly in 10.10) or using the NSAlert class. It does not mention the option of using a sheet segue to create a custom sheet. All of the sample code is in ObjectiveC, and unless I’m mistaken, it would not run without modifications due to its age.

You can find out what Apple thinks about Sheets in the Dialog section of the Human Interface Guide; but the summary seems to be that the use case is dialogs – a print dialog, agreeing to a user agreement, anything that needs the user’s input before continuing. (Read the guidelines; most of the advice should be a no-brainer, but it’s useful to remind oneself of them anyway.) And then read the Alert section of the HIG, too.

By design, an NSAlert object is intended for a single alert—that is, an alert with a unique combination of title, buttons, and so on—that is displayed upon a particular condition. You should create an NSAlert object for each alert dialog, creating it only when you need to display an alert, and release it when you are done.

But here we also happen across the weirdest and most infuriating aspect of NSAlert: the modal and sheet modal forms have a different lifecyle, and it’s easy to overlook that. For a modal NSAlert, interaction handling (which button was pressed, did the user tick the ‘don’t show this again’ box) happens after showing the alert; the alert is retained as long as it is within scope of the calling function.
For a sheetModal NSAlert, all handling needs to happen in the completion handler; the alert is released immediately afterwards.

When you’re switching from one style to the other, this can catch you out.handler: The completion handler that gets called when the sheet’s modal session ends should be a clue, but I admit to overlooking it for a good while.

In the documentation for the buttons of an NSAlert, under _Related Documentation_, you will findAn alert’s return values for buttons are position dependent. The following constants describe the return values for the first three buttons on an alert (assuming a language that reads left to right). Note that these constants constitute additionalNSApplication.ModalResponse
values used byrunModal()
andbeginSheetModal(for:completionHandler:)
.

Note that the ‘ButtonReturnValues’ link goes to the same page, but they’re NSApplication.ModalResponse values; more specifically the NSApplication.ModalResponse.alertFirstButtonReturn (.alertSecondButtonReturn, .alertThirdButtonReturn).

0) Before we begin, let’s have a look at the old Functional API.

While the SheetProgrammingGuide still mentions it,void NSBeginAlertSheet(NSString *title, NSString *defaultButton, NSString *alternateButton, NSString *otherButton, NSWindow *docWindow, id modalDelegate, SEL didEndSelector, SEL didDismissSelector, void *contextInfo, NSString *msgFormat, ...);
has been properly deprecated in 10.10. (It was discouraged in 10.3, which came out in 2001… a long time ago).

It’s an intereresting window into what development used to be like, and it shows one of the strengths of OOP: this might have been ‘functional’ (of sorts), but the NSAlert way is much clearer. Above is code you need to copy and fill in as well as you can, and while it’s cool that you can pass it functions (what should happen when you dismiss the sheet, who should handle the alert etc), it’s also pretty overwhelming. (Note, for instance, that you have items called ‘defaultButton’ etc which are NSStrings. Note also the weak typing in having a modalDelegate of type id, which corresponds to AnyObject.)

Basic Alerts

1a) Create a new macOS application, with Storyboards and Swift as usual. We’re simply going to create alerts in a button’s IBAction. Add that button, name it ‘Show Alert’ and create an IBAction for it in the ViewController.

@IBAction func showAlert(_ sender: Any) {

let alert = NSAlert()
alert.runModal()
}

Build and run.

It’s not the most informative alert in the history of computing, but it works. (Sort of: Autolayout does not love me.) But this is the default implementation of NSAlert, and you will see that it already contains a button to dismiss it. Unlike a sheetSegue, you will always be able to get rid of this alert.Basic customisations: Icon, Text

1b) Let’s start to customise it a bit.

PROTIP: Create an alert, customise it, then run it. If you do the customisation after showing the alert, you won’t see the effect. (Ask me how I know…). It is assumed that unless I specifically state that you should add code at the top or bottom of this function, all customisations will be between let alert = NSAlert() and alert.runModal()

There are three styles of alert: .informational, .warning, and .critical. Let’s try the other two (.informational is default)

(Yeah, me too. Currently, there is no visual difference between informational and warning alerts sez the NSAlert.Style documentation; which does not mean that there never will be.)

The icon automatically uses your application icon, so I’ve added a very simple one to the AssetCatalogue (using AppIconTool from the App Store. The ability to upload a single image to Xcode and have it auto-generate icons is definitely on my wishlist.)

Yeah. Better.

alert.alertStyle = .critical

With and without a custom app icon. This will – see above – look better if you use an icon that isn’t just square, but has some transparent pixels.

If you want to use a custom icon, simply use

alert.icon = #imageLiteral(resourceName: "bluebell")
(replace with your favourite method of instantiating images; these days I prefer adding an image to the AssetCatalogue and using image literals over NSImage(named:)

1c) We move back to .informational (I simply like it better, these alerts do not _need_ my attention), and customise the text.

addalert.messageText = "This is a standard alert"
alert.informativeText = "It does nothing extraordinary, but like so many alerts, it wants to be confirmed."
This resolves the autolayout issue: Autolayout does not like an empty string for the informativeText.

Text restrictions: Informative Text can be very, very long. Here I’ve pasted two paragraphs from the Human Interface Guidelines; the alert expands downwards to acommodate them. (This is not a good idea, but it’s good to know where the breaking point is.)

At around this point, I’m happy to stop complaining about Autolayout:

I was definitely expecting setting the same two paragraphs as the message text to completely break the alert, it does not. Not that you’d ever want this, but I was expecting anything other than a full line of text to break things somewhat. Instead it’s smooth. Bravo, Apple.

Buttons

2a) Now we’re getting to buttons. All buttons dismiss the alert; we will handle which button was clicked slightly later, after we’ve been through the layout stuff.

alert.addButton(withTitle: "OK")
shows no visual difference to the default. (There is one. These are buttons added to a buttons array. The one provided by default is its own thing.)

If you change that to

alert.addButton(withTitle: "First")

the button you add has the same characteristics – it sits in the bottom right corner, is the default button (reacting to Return or Enter).Moar Buttons

This button placement is automatic; the default button on the right, its counterpart to the left of it, and the opposite action set apart on the left. If you think of them as ‘don’t save, cancel, save’, the pattern becomes clearer.

If the second or third (but not the first!) button is titled ‘Cancel’ (but not ‘cancel’), it will respond to command-period or the Escape key.

2d) What about the number of buttons? The HIG says An alert can include up to three buttons but who ever listened to that? (Further down, the HIG becomes more explicit: Generally, use two-button alerts. Two-button alerts provide an easy choice between two alternatives. Single-button alerts inform, but give no control over the situation. Alerts with three or more buttons create complexity though I would argue that in many cases – see ‘save’ dialog – you want the ‘do this’ ‘don’t do this’ ‘let me go back to the document and reconsider’ triad.

(these are methods of NSButton, the array builds – as we have seen – from right to left.)

A you can see, marking a button as the default overrides any removal of highlighting. On a tangent, I found that you also cannot use a button’s action to set the button’s highlight, because the automatic management – highlight-while-being-clicked – overrides setting the highlight state manually.

So if the default marking of a button is determined by setting its key equivalent to ‘return’, all we need to do is set it for the second button and –

alert.buttons[1].keyEquivalent = "\r"

Well, not quite yet. NSAlert automatically adds the same to the first button, so they’re now both our default buttons. Which is a bad idea. (It’s not _very_ obvious, but if you compare this image with the one above, you see that the highlight state – while also blue with a light text – is subtly different from the default button state. Past incarnations of macOS were more drastic, future incarnations (or skins?) might do the same again, which is another excellent reason not to try and indicate a default button – the one you should choose if you want to be safe/do the thing you meant to – using the unrelated highlight.

You need to specifically set alert.buttons[0].keyEquivalent = "" to make the second button the default.

What happens when you set several buttons to the same key equivalent? In this example, the rightmost button always won, if it was a non-default button, the leftmost button got logged; I did not manage to fire the middle button. This behaviour seems to be consistent even when you choose a different key equivalent (here: “p” which is triggered by pressing the ‘p’ key) – if the rightmost button is part of the group, the rightmostbutton fires, otherwise, it’s the button on the left. I only tried this with three buttons, for reasons that will become obvious later.

Key codes and key masks (basically, the option to specify command-p or option-a) are beyond the scope of this post, so here you’ll only get single letters. Anyway. Give each button a different key equivalent, and let’s move on.

Supression Button

3) Add a supression button:

alert.showsSuppressionButton = true

This is the default text; and since it’s called SupressionButton the implications are pretty strong: you should use this only for this purpose, even if it’s tempting to appropriate. If you want your own control, you should design one.

Supplementary View

4) Alerts can have accessory views. You could create this in code, but I don’t like creating views in code.How to set up a supplementary view

4a) Create a new NSView subclass in your app, name it AlertAccessoryView. Drag a customView from the object library into the bar at the top of your scene (which now should have objects for AlertViewController, FirstResponder, and your custom view) and set its class to AlertAccessoryView. It will appear as a non-resiable or movable view above your AlertViewController scene.
Under 10.12.6 and Xcode 9, I’ve managed to get a resize handle to widen this by dragging a wider object into my view, and had no luck with the ability to resize the height. Setting either in the Size Inspector works fine, though.
Drag a button and a label into the view; details don’t matter right now. For items like this, I like to use a Box, send it to the back, and set its fill colour, which is easier than messing with other ways of colouring a view.

Create an outlet for your custom view @IBOutlet var accessoryView: AlertAccessoryView! which now allows you to setalert.accessoryView = accessoryView

And hey presto, you’ve got the opportunity to do anything you like in the usual fashion. One good use for the accessory view would be to have two views and a disclosure button – add a ‘show more’ button and tell the user everything they need to know about the action they’re about to perform in one handy place.

Help Button

5) The help button is the last item that you can place in an alert. You can use this in two ways: setting a helpAnchor (but doing so is beyond the scope of this tutorial and not very high on my list of priorities) or by implementing an NSAlertDelegate, which is how we’ll handle things here. You show it by setting

alert.showsHelp = true

Help? Help.

5a) First, declare that your ViewController will conform to the NSAlertDelegate protocol, then add

which is lousy interface design – two modal alerts on the screen at the same time – but had to be done. In this function, you can implement anything you like. You could even create a code branch: either do a custom thing and return true from the alertShowHelp function, OR set a helpAnchor and return false to redirect to the help system.

Handling Responses

6) So far, we’ve just gone through all of the options of NSAlert without taking any note whatsoever of which button the user clicked. For informational alerts – with a single ‘ok’ button – that is perfectly fine. For anything else, you need NSApplication.ModalResponse.Using ModalResponse in Modal and sheet Modal Alerts

6a) So far, we have used alert.runModal() to show our alert on screen, and for purely informational, single-button alerts that is just fine.

It is easy to overlook that runModal() returns a value, the above-mentioned NSApplication.ModalResponse. Replace that line with

6b) If you look at the documentation, you can see why ‘default’ is necessary: this is not an enum, but a bunch of static variables; so you will never get the complete set. But what of the other two, OK and cancel?

to your switch statement, comment out the three button-related ones, and make sure that you add only

alert.addButton(withTitle: "OK")
alert.addButton(withTitle: "Cancel")

to your NSAlert. Also remove the key equivalent code – when experimenting, it’s very easy to not add enough buttons to the buttons array and create an out-of-bounds error.

Build, run, and be disappointed: this catches only the default clause.

Remove all the buttons that you have added to the alert so you only get the default OK button that NSAlert adds by default.

Build and Run. This returns NSApplication.ModalResponse.cancel, and at this point I have no idea whether that’s wanted behaviour or a bug. (I would have expected .OK) and I have no idea what will return that.
This default button does not get added to the buttons array, which is consistent with not returning .alertFirstButtonReturn

Sheet Modal Alerts

7) So far, we have only run the NSAlert modally, which means it appears in its own window. Press the help button again if you need a reminder of the behaviour: only the modal window is responsive and can get focus, any other windows can be moved, but not interacted with or dismissed.
NSAlert has a second mode: ‘sheetModal’, which means that it will appear as a drop-down sheet attached to a specific window. This is particularly useful for document-based applications, since the rest of the application will function as normal.

7a) Return the alert to the state where you add three buttons yourself.

In examples, I have frequently found NSApp.keyWindow instead of view.window – the currently active window – but ‘window for this view’ seems a better fit. I have not tested this with a multi-window application, so I just want to flag this up here: it may be that the choice of window needs to be made differently. For this example app, both work.

Build and Run.

The alert attaches to the menu bar, and everything works just fine.

7b) beginSheetModal does not return anything, so our switch statement above is useless.

If you only have an informational alert, and don’t need a response, you can call this as alert.beginSheetModal(for: window), but I found it useful to have the reminder that there IS provision for a completion handler useful.

The signature tells you that you will get a ModalResponse and don’t need to return anything, and in the meantime you can do anything you like (which usually will correspond to the switch statement above). This isn’t the place to go into closure syntax in detail, but the full code is

Where the function signature gives you the response type, here you provide a local variable name, and then you write code just as you would normally, just making certain you balance the brackets properly. (I frequently write closures first and assign them to a variable, and then simply pass in alertClosure as a parameter, but that’s a personal preference.)

Build and run: apart from the sheet dropping down, you should see no difference to the modal alert.

Don’t show this again

8) We’re nearly at the end of this tutorial, and you may be slightly fed up with seeing the same alert again and again, so we’ll restore the supression button and this time, make use of it.

This is a little complex, so I’ll run through the logic before we add the code to our function. First, we’ll add a reference to user defaults and define a key for the value that determines whether we show the alert or not. Next, we write a switch statement: if the preference key returns false, we show the alert (wrapping all the code we’ve written so far in that branch), if it’s true and the user is fed up, we do nothing.
Finally, we read the value of the supressionButton and set the preference key accordingly, so that the text time the code is run, it will be shown – or not shown – as desired.Getting rid of alerts

This code goes either inside the completion handler for a sheetModal alert, or after let result = alert.runModal() for a modal alert.
(This was the ‘different life cycle’ bit mentioned at the start of this article. You cannot run this after beginSheetModal. While the modal alert is retained until the end of the function, the sheetModal is released earlier.)

8d) Close the switch statement with the ‘true’ branch:

case true:
print("User was bored")
}

Build and run. Show the alert, dismiss it with any button, show it again, and this time, tick the supression box.

Click on the ‘Show Alert’ button ONE LAST TIME and rejoice as nothing happens.

And more…

Usage Example

See the Human Interface Guidelines for Dialogues and Alerts: important warnings, print dialogues, asking users whether they want to save. Use sparingly.
Alerts can also be a useful debugging tool if you don’t want to interrupt the flow of working inside the app: you can sample data, display it in human-readable form, maybe even change it, and dismiss it easily.

The other use for NSAlert is to display errors. NSAlert has a special initialiser for this: init(error:), which extracts the localized error description, recovery suggestion, and recovery options from the error parameter and uses them as the alert’s message text, informative text, and button titles, respectively. I wonder whether this – and the new Swift error handling system – is responsible for the occasional encounter with ‘An error occurred’. (OK).

Alternatives

Sheet segues, which I will write up another day, and a gem from the HIG: when a Mail server connection has been lost, Mail displays a warning indicator in the sidebar. Users can click the warning indicator if they want more information about the situation. – instead of showing an alert that must be interacted with, the app simply shows an error indicator that users can seek out if they’re interested. For things that often go wrong (the internet connection was lost) this is the better option – dropping access happens so frequently that the only time readers need to know is if the condition persists or queuing up actions is no loger possible.

Extensions

Since you can drop in an auxiliary view: anything you fancy, really. If you wanted to use four or five buttons (please don’t), you’d have to add a target and action to those buttons. So, ok, one more: