Opening and saving files is part of many, if not most Mac Apps, and there are many ways of achieving this. I wanted to review my practice and hope to arrive at a clean, swifty way of file handling. In this post, I will concentrate on NSOpenPanel and NSSavePanel, rather than the filesystem end of things; but before we go there, we need to take a short trip into the land of filepaths and URLs. (For more, see this post.)

This is not the time or place to delve into the intricacies of the file system. (See this master post on why not: there’s *a lot* of it.) Very frequently your application will simply handle the url you retrieve from the open panel, or write to the location picked in the save panel, so that you do not actually have to parse it. But for those cases where you want to open a particular location, I have a companion post that simply lists the URLs for common system locations, eg the desktop, the documents folder etc. as well as a little bit on construction and the all-important discussion on filepaths vs URLs.

Apple has, unsurprisingly, opinions on how to use OpenPanels:– To open a document in a new window, present the Open panel modally relative to the app.
– To choose files or directories and use them in conjunction with an already open document or window, present the panel as a sheet attached to the corresponding window.

We’ll get to the sheet presentation later.

0) These are the default settings for your out-of-the-box NSOpenPanel:

I see a fair number of examples that set all of these settings to their default values, which serves no purpose. If you really need to remind yourself that your openPanel can only open one file at a time – and you have a good reason for that – you should a) add a comment and b) name it singleFileOpenPanel – this way, anyone (including yourself six months down the line) will think twice about changing allowMultipleSelection.

Basic OpenPanel

NSOpenPanel

1) Minimum panel

let openPanel = NSOpenPanel()
openPanel = runModal()

let panelResult = openPanel.url
if let panelResult = panelResult {

// do something with the URL

}

1a) Retrieving a result even when the user chose ‘cancel’ is, of course, not the sensible way to handle things. Thankfully, NSOpenPanel.runModal() has a return value; which is of type NSApplication.ModalResponse. This is slightly awkward, because while the openPanel only ever handles two cases – the default button and the cancel button – in a switch you need to handle the ‘default’ case as well. (We’ve seen ModalResponse in action in NSAlert).

(For common locations, like the user’s home folder, the document folder, the application’s temporary or applicationSupport directories, etc, see the Filemanager paths/URLs post.)

1c) Not ok with ‘OK’?

If you want the default button of your openPanel or savePanel to display a different string, use

openPanel.prompt = "Go Wild!"

This will still be the default button (fires when you hit return).

While I strongly discourage the use of this feature for frivolous reasons, it has a very important use case: if your apps handles multiple selection of files, it should also handle folder selection. Since this ideally involves file enumerators, we will deal with this in a separate section.

The logic is that if one or more files are selected, you handle those files; if no files are selected, you handle every (usable) file in the current folder (openPanel.directoryURL); but you might want to change the title of the button from ‘Open’ to ‘Choose’ or similar. (‘Choose’ seems to be common in many applications. It’s a good one.) It’s up to you whether you want to handle selection of a folder as ‘opening it’ (the default), or whether you allow choosing a folder and simply process all of its contents outright without making the user drill down a level. In this case, you need to set canChooseDirectories to true).

1d) Do something with the file. If you want to read the file and use its contents – the text, the image contained in them – then you’re good. If you want to further filter files by attributes (e.g. creationDate) you need to look at the URLResourceValues. An example of this usage is given under 3d) FileEnumerators.

2) Filtering types:

In my little sample app, where I’ve simply printed the title of each file, all files were valid. In the vast majority of cases, your app will want to be more selective – if you cannot handle the contents of a file, what’s the point in letting the user attempt to open it?

NSOpenPanel has an allowedFileTypes property to deal with this problem.

The strict type of allowedFileTypes is [String], but with a twist: valid values are a) file extentions ([“jpg”, “png”]) and b) UTIs ([“public.jpeg”, “public.png”]). Not only is Apple very clear about which one you should use (if both are available, always use UTIs; if this is your own type, register a UTI with the system), but the possibilities for errors are great: nothing ever conforms to “.jpg” (an easy mistake to make), and “jpg” does not allow you to open image.jpeg even though the _image_ is perfectly readable. Wrong letters, do not pass go. public.jpeg has no such problems.

If you are handling images through the medium of NSImage, you can simply use

openPanel.allowedFileTypes = NSImage.imageTypes

(there is also an NSImage.imageUnfilteredTypes; my understanding – which may be wrong – is that imageUnfilteredTypes is the more restrictive; imageTypes includes user-registered NSImageReps; Apple uses .imageTypes in their example.)

The resulting array starts with"com.adobe.pdf", "com.apple.pict", "com.adobe.encapsulated-postscript", "public.jpeg", "public.png" and contains more than a dozen instances of UTIs like “com.pentax.raw-image” – in other words, every camera manufacturer has their own implementation of the RAW format, and Apple handles A LOT of them. (Won’t say ‘all’. I’m superstitious that way.)
As I understand it, this usage is preferred over [“public.image”], though in a brief test (common file formats as they hung out on my hard drive) there seems to be no practical difference. Being more precise avoids the disappointment of ‘hey, this app can read my- no, it can’t actually read my file after all.’

The most useful resource is probably the listing of System-Declared UTIs which lets you find the most commonly useful ones.

Of interest may be

public.text
public.rtf
public.html
public.audio
public.folder

but do look through the list; there are lots, some of them oddly specific, and the listing gives the base type as well as the file extensions.

While openPanel.allowedFileTypes can be nil – all files are allowed – an empty array throws an exception. (Thankfully, a helpful one: file types can never be empty). Setting it to a string that is neither a valid extension nor a valid UTI, on the other hand, just stops you from opening any files at all: public.unicorn gives you no results. If you name a file myFile.unicorn and use openPanel.allowedFileTypes = [“unicorn”], you can read your file – but please don’t do this; declare a UTI. (And if you’re renaming files in the Finder to test this, be aware that the Finder will display it as myFile.unicorn, but the actual filename will be myFile.unicorn.jpg, or whatever it started out as. You need to bring up the file info and confirm that you want a different extension.)

File Enumerators

File Enumerators

3) Above I’ve mentioned file enumerators. The task of doing something with every file in a folder is not an uncommon one – many image manipulation applications allow you to either import all files in a folder – but it comes with a potential high cost, particularly if you allow subfolders: since the macOS filesystem has no limitations, your user could be importing all files on an 8TB drive, which your app will never be able to handle. For this reason – because users eventually can and will try anything, deliberately or out of absent-mindedness – you need to protect yourself and the system against this scenario. (I once opened 7GB of RAW images instead of creating a new folder for them. I have to say that Preview is absolutely marvellous: I thought this attempt would tie my computer up for ages; it did not. I shall never again set any other application as default for opening RAW images.)

3a) The first thing to mention is that enumerators don’t work on single files. You need to make sure that you really *are* handling a directory before you put any vital code into this branch.

3b) NSFileEnumerator inherits from NSEnumerator, and here I hit the first snag: while NSEnumerator exists, Swift handles enumeration/iteration very differently from Objective-C. While NSArray, NSSet, and NSDictionary have NSEnumerator properties, swift uses the sequence and iterator protocols. I’m not convinced you even can use NSEnumerator with Swift: it exposes only a standard initializer, and no way to set its content.

The first FileEnumerator’s I have come across were using String-based paths, and thus proving why Stringly-typed resources are hell:let enumerator = FileManager.default.enumerator(atPath: panelResult.absoluteString)

does not create an enumerator, just as FileManager.default.contents(atPath: panelResult.absoluteString) (not a property you should use) reports that there are zero items in my selected folder. The suspicion is that the ‘path’ these methods want does not equal the string provided by URL.absolutePath, but since all of the examples assume that you have a valid path without once saying what that valid path actually looks like, I got really stuck at this point. (filepath has been deprecated in 10.6…) It took me some time to find the .path property (absoluteString begins with file://; path drops this prefix).

All the compiler knows that the string is a string. You could ask it for the contents of “kittens and rainbows” and it would equally fail; but here the compiler cannot offer any guidance, which leads to great frustration.

(This, obviously, ends up with the resultLabel showing only the creation date of the last item in the directory)

You can find a full listing of keys in this listing (nil means ‘get all’); but I have to add a caveat: some keys work out of the box (whether you use nil or request them explicitly), while others – like .thumbnailDictionaryKey always return empty dictionaries for me.

(thumbnail bugs me especially badly: my life would be so, so much better if I could simply get a thumbnail from a URL, so I could store URLs in my app, read in and display thumbnails, and load the image proper only when it is needed.)

4) Ways of presenting open/save panels

modal/completionHandler/sheet

So far, we’ve used NSOpenPanel modally; with runModal(). You can also run open panels with a completion handler, which allows the rest of the application to remain responsive.

(this is the slightly longer form since the function signature is NSModalResponse -> (). _result in_ works just as well, but sometimes I find that writing out the full signature for a completion handler and then deleting the unnecessary bits helps me with getting them right first time.)

In the other direction, you can run your openPanel as a sheet, attached to a particular window (without blocking other windows).

Be aware that there’s a beginSheet method coming up in autocomplete. It’s misleading – this is a method inherited from NSWindow, which – as the documentation says, Starts a document-modal session – here in my sandboxed app it removes the titlebar and the rounded corners from the bottom of the window, but does not present an openPanel.

5) Saving Stuff

NSSavePanel

Disclaimer: I am not actually talking about the act of saving files here, just the NSSavePanel part. Saving Files a subject for other posts, because there’s a lot of it, touching, in turn, on serialisation (Codable/NSCoding) and creating your own file types (UTI).

(As before, I don’t think customising the prompt is a good idea unless it’s to distinguish, e.g., ‘export’ from ‘save’ (one a common file format, the other your proprietary and files containing more bells and whistles); title and nameFieldLabel will depend on your app (but remember localisation). nameFieldStringValue – the name under which the file will be saved – is important; ‘Untitled’ is rarely a great choice.

You can also add savePanel.message which displays a longer String in case you need a more complicated explanation.

5c) Saving placeholder

(assuming a string we want to write as a file, since that is the easiest item to demonstrate; we will use the .unicorn extension and pretend we’ve properly registered the UTI. If your app is sandboxed, ensure that you have ‘read and write’ permission enabled for files; if this is none or read only, your app will crash when you call up the savePanel).

– savePanel.allowedFileTypes = [“unicorn”]
– retrieve savePanel.url (this is automatically composed from the current active folder and savePanel.nameFieldStringValue
– check whether the file already exists, in which case you need to ask the user whether they want to overwrite it or save the current content under a different name
– write the file (with appropriate code branches for different filetypes)

The relevant code is:

guard let saveURL = savePanel.url else {return}

let myString = "Have you heard about rainbow unicorn Torah?"
//placeholder so we have something to write to file
// see https://twitter.com/TheRaDR/status/965231302055211009 if you haven't
do {
try myString.write(to: saveURL, atomically: true, encoding: .utf8)
// most basic encoding; used here to allow the file to be read more easily by almost any text processing app
} catch {
print(error)
}

We do not need to check whether the file already exists; NSSavePanel does this automatically, and as far as I can see (but don’t take my word for it), the FileManager handles the correct writing/overwriting of files without needing further intervention from us.

5d) Hiding Extensions
The default behaviour is that extensions are hidden, which annoys me much more often than not; I want to know which custom extension an app uses (helps with recognition); and I may want to change it to another allowed filetype.

Whereas NSOpenPanel allows and parses UTIs (“public.image” lets you open all supported image formats, of which there are a lot), NSSavePanel is a little fussier.

The first item in this array is the default extention; typing anything not in the array of allowed extensions leads to automatic hiding of extension – you think it worked –

but you would be quite wrong. As you see when you unhide the the extension. (Because this is automatic, and because the hide/unhide button is at the bottom of the panel, this is easy for the user to overlook.)

If you allow more than one filetype as an option, you should provide a popup in an accessory view (see 6) to make it clear what is and isn’t allowed. While adding “public.image” to your list of allowed fileTypes lets you type any image extension you can think of (if you type .gif, the filename really will be New File.gif), you still need to parse the chosen filetype from the extension and export the image accordingly, and in the case of obscure proprietary RAW formats, that won’t happen.
Under some circumstances, allowsOtherFiletypes is a useful thing to do – if you have a text editor, users might want to edit text formats like .html, .css, and others you didn’t think of, and it’s annoying when you have to save it as .txt and then rename it in the Finder. This can, of course, go very wrong – name your image .txt and see what happens – but I feel that one should warn users and then let them do whatever they want. Sometimes, we have good reasons. The same goes for allowing files to be opened as text; I’ve had to force-extract text content from old wordprocessing files that no good filters exist for anymore.

Shorter version: use an accessory view, an array of extensions you can actually handle in your app, and a popup in the accessory view that allows users to choose rather than guess.

6) Accessory views

Customisation with accessory views

You can attach an accessoryView to your open and save panels using the accessoryView property. A common use is to provide a set of file formats for the user to choose in a save panel. The easiest way to do this is to drag a customView into your viewController, create an outlet for it, and hook up any outlets you choose.

openPanel.accessoryView = myView

This gives you an ‘options’ button in the bottom lefthand corner, which expands (or not) the view; you handle it via openPanel.isAccessoryViewDisclosed

For savePanels, the accessoryView is always visible.

So far, so wonderful. However. While you can in theory change the savePanel’s nameFieldStringValue, I (and other people) have found this unreliable – under some circumstances, it does not change the display (I’ve always ended up with the correct value in the end, but if the user sees ‘file.unicorn’, they do not expect the file to be saved under ‘file.jpg’.)

Setting allowedFileTypes to only the chosen type seems to reliably update the extension displayed to the user.

In order to change it from an accessory view, you need to declare an outlet for the savePanel and configure the panel before presenting it to the user.

For demonstration purposes, I am using a simple checkbox (popup menus are more code) and only two states. Since my accessoryView is already declared as an outlet in the viewController, I simply use the following:

With this method, you lose the option for users to simply type the extension they want, but the convenience of not having to guess which file formats are allowed outweighs that in all but text files, in my opinion. (For those, I would – see above – enable allowsOtherFiletypes.)

7) Return to the Sandbox

Sandbox and Delegate Protocol

Sandboxing is what Apple wants developers to do. Personally, I find working with the sandbox frustrating, slow, and very annoying, but I understand the considerable safety implications, and I follow the Apple whenever I can, so I write my apps for the sandbox.

– the responder chain is different. In sandboxed Apps, NSSavePanel inherits directly from NSObject. This explains a) the occasional appearance of private subclasses and b) why you cannot safely subclass NSSavePanel or NSOpenPanel in sandboxed apps.
– you cannot programmatically mimic things like ‘user clicked the ok button’ or ‘user chose a file to overwrite’. The security implications of that are clear. Given that I only discovered the NSSavePanel.ok(sender:) and .cancel(sender:) methods today – I have never seen them in example code – that’s probably not a great loss, and the whole point of the panels is to let users control choices.
– there’s a whole section on related files and how to handle them; I will not go deeper into them right now, but it’s worth keeping in mind that there are additional requirements for doing things like ‘save the same file with different extension’.

I’m mentioning this here without going into details; it seems to be a protocol that duplicates a lot of functionality of NSSavePanel/NSOpenPanel; maybe allowing for a little more finely-grained responses. panel(shouldEnable:) for instance, validates URLs – a lot of that functionality is provided by openPanel.allowedFileTypes. Most of the examples I can think of offhand – app does not handle that colour depth/file size – are options that have been outpaced by modern memory and disk space developments, but you could use this, for instance, if you’ve prompted your user to upload an avatar with a minimum size of 500×500 px: images that are smaller than that would simply not be openable. This may be preferable to allowing users to allow all images, only to open one and display the sad message of ‘this is too small, we cannot use it, please try again’.

According to this stackoverflow question,NSOpenSavePanelDelegate method’s don’t have access to the filesystem in sandboxed applications which makes the question of whether one should use NSOpenSavePanelDelegate rather pointless.

And more…

Usage Example

Any time users want to open/save files, import/export resources, and when you determine the default location for files. I run a dual boot system: do not assume that your users can save large amounts of data in their ApplicationSupport folders (network users often can’t, for instance). Give us a choice, dammit.

Alternatives

None. Particularly not in sandboxed apps.

Extensions

More customisation through accessory views. Most ‘extensions’ for this are in using settings to allow users to set up panels as they want them – hiding or showing tags in save panels, letting users set a default open and a default save folder (have an image processing app? That’s a marvellous feature; saves a lot of time and clicking).