Working with Open and Save Panels in Mac Apps

My short article on changing the button title for open and save panels gave me the idea to write a longer article on working with open and save panels.

You May Not Need to Deal with Open and Save Panels

Apple’s document architecture gives you a lot of default behavior for free. You can write an app that opens and saves files without having to write any code for the open and save panels. The most common case of having to write code that involves open and save panels is importing and exporting data. For example an image editor would use a save panel to export the image to common file formats, such as GIF, JPEG, and PNG.

Creating the Panel

Creating the open or save panel is the easiest step. Call the constructor for NSOpenPanel or NSSavePanel.

let savePanel = NSSavePanel()

Customizing the Panel’s Appearance

When you create an open or save panel, you get the default appearance, which works well most of the time. But if you’re using a save panel to export a file, you want the button to say Export instead of Save because you’re exporting a file. AppKit provides several ways to customize the appearance of open and save panels. Some of the most popular ways include the following:

  • The prompt property lets you change the text of the default button.
  • The title property lets you change the title of the panel.
  • The nameFieldLabel property lets you change the label text in front of the filename text field.
  • The nameFieldStringValue property lets you set the filename currently shown in the filename text field.
  • The directoryURL property lets you set the current directory for the panel.
  • The accessoryView property lets you add an accessory view. A common use of an accessory view is to show a popup button with a list of file types.
  • The allowedFileTypes property lets you limit the file types you can open or save.

Custom Appearance Example

As an example, I’m going to share the code for a custom save panel for an app I’m developing. In this project I’m using a save panel to publish an EPUB book.

func buildPublishSavePanel() -> NSSavePanel {
    let savePanel = NSSavePanel()
    savePanel.title = "Publish Book"
    savePanel.nameFieldLabel = "Book Name:"
    savePanel.nameFieldStringValue = self.displayName
    savePanel.prompt = "Publish"
    savePanel.allowedFileTypes = ["epub"]
    return savePanel
}

Because the save panel is for publishing books, I changed the panel’s title, name field label, and prompt (button text) to let people know this panel is for publishing books. I’m publishing EPUB books so I’m limiting the file types to files with the pub extension.

The most confusing line of code is the line that sets the text field value to self.displayName. The display name is the filename of the document that I’m publishing.

Showing the Panel

There are two ways to show an open or save panel. The easiest way is to show a panel as a modeless window. Call the panel’s begin method and supply a closure.

savePanel.begin { (result: NSApplication.ModalResponse) -> Void in
    // Code to come later
}

The other way is to show the panel as a sheet inside the document window. Call the panel’s beginSheetModal method. Supply the window and a closure.

savePanel.beginSheetModal(for: window) { 
    (result: NSApplication.ModalResponse) -> Void in {
    // Put your code here
}

Handling the Default Button Click

After showing the panel, you must handle the clicking of the default button (Open for an open panel, Save for a save panel). What you must do is check if the result of the panel running is NSFileHandlingPanelOKButton, which occurs when someone clicks the default button. Usually you will call a function that performs the task you want to do. For example if you were exporting a file, you would call a function that exports the file.

The Full Example

The following code shows the full example of using a save panel to publish a book:

func showPublishSavePanel() {
    let savePanel = buildPublishSavePanel()
        
    savePanel.begin { (result: NSApplication.ModalResponse) -> Void in
        if result.rawValue == NSFileHandlingPanelOKButton {
            if let panelURL = savePanel.url {
                // Replace this code with 
                // a call to a function you wrote.
                publishEPUB(location: panelURL)
            }
        }
    } // End of closure
}

The code begins by calling the buildPublishSavePanel function I wrote earlier to create the save panel and customize how it looks.

The next line of code calls the save panel’s begin method to show the save panel. The closure in the begin method uses a variable, result that stores the result of someone interacting with the save panel.

The first if statement checks if the person clicks the default (Publish) button. If so, the second if statement runs.

The second if statement gets the URL of the location the person chose to store the published book and calls a function to publish an EPUB book at the supplied URL. In your app you would call a function you wrote that creates (examples: importing or exporting a file) a file at the given URL.