File exporting is a common task when building apps, and it often becomes necessary to define a custom file extension. A custom file extension allows an app to be the only one recognizing, and therefore opening and manipulating a file with that extension. But in addition to that, it’s also required sometimes to make the operating system aware of who is the owner of custom file types, and which app is responsible for opening them.
That’s quite common mainly for apps that deal with documents that users can interact directly with. For instance, documents that users see on Finder and double-click to open them, such as files created with Pages, Numbers, Xcode, and so on.
We are all used to open files by double-clicking on them, without running the app responsible that handles them first. But how do we manage that for our own apps? How can we export files with custom extensions, and have the system launch our app and open selected file or files automatically, when we just interact with them in Finder, the Files app, or someplace else?
That’s what exactly this post comes to answer, presenting step-by-step the recipe of how to define a file of custom type, and how to open it with our own app directly from the system.
Getting started with the sample app
In order to keep things as simple and clear as possible, suppose that we are implementing a quite basic note-taking app on macOS. It allows to write just a title and the content, but nothing else beyond these. A button presents the system-default file dialog, allowing to choose the folder where the note file will be stored into.
The following presents the view that implements all that:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
struct ContentView: View { @State private var title = “” @State private var content = “” @State private var showFileImporter = false var body: some View { VStack { TextField(“Title”, text: $title) TextEditor(text: $content) .font(.body) } .padding() .toolbar { ToolbarItem { Button(“”, systemImage: “square.and.arrow.down”) { showFileImporter = true } } } .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.folder], allowsMultipleSelection: false) { result in switch result { case .success(let urls): guard let url = urls.first else { return } // Obtain a security scoped URL so the app can // read & write files & folders outside its sandbox. guard url.startAccessingSecurityScopedResource() else { return } defer { url.stopAccessingSecurityScopedResource() } do { // Create a new Note instance using // the given title and content. let note = Note(title: title, content: content) // Encode the note instance and write it to disk // at the selected URL. try JSONEncoder() .encode(note) .write( to: url.appending( component: “\(noteFileName).mynote” ) ) } catch { print(error) } case .failure(let error): print(error.localizedDescription) } } } var noteFileName: String { guard !title.isEmpty else { return “Untitled” } return title // Remove whitespace. .trimmingCharacters(in: .whitespacesAndNewlines) // Replace spaces with underscores. .replacingOccurrences(of: ” “, with: “_”) } } |
The fileImporter modifier triggers the appearance of the system file dialog, which is configured to allow single folder selections only here. I won’t get into more details about the file importer, as you can find a dedicated tutorial about it in this older post.
The interesting part is inside the success case of the file importer, where:
- A
Noteinstance is initialized. - It’s JSON-encoded.
- It’s written to the selected URL using an automatic file name.
The Note type shown in the above snippet is the following:
|
1 2 3 4 5 6 |
struct Note: Codable, Equatable { var title: String var content: String } |
The note is stored using the title as file name, but in a more system-friendly version. That’s what the noteFileName computed property does. If no title has been provided, the note is simply named “Untitled”.
But the interesting here is the file extension. Since this is a sample app and not a real one, the extension should be something that normally would not be used in actual apps. That’s why I chose “samplenote“, and our goal from now on is to make both the system and this app capable of opening .samplenote files when double-clicked on Finder.
There’s just one more detail to configure mandatorily. In the Project navigator, choose the project at the top, then select the app target and the Signing & Capabilities tab. Inside the App Sandbox, in the File Access Type, update the User Selected File to Read/Write. If you avoid that, the system will always deny access to the selected folder.

Running the app now allows to create a .samplenote file, but the system knows nothing about it yet. Even when right-clicking on it and selecting the Open With submenu, the system suggests other apps to open that file with:

So, that’s where the actual topic of this post is starting, as we’re about to change that in the next parts.
Making the system aware of our custom document type
When our app defines a new file extension and it’s necessary to let the system know about that, then we have to declare one or more Exported Type Identifiers, depending on the number of custom file types the app creates. That’s the first step towards opening custom files outside the app.
We declare Exported Type Identifiers by going to Project navigator > Project > Project Target > Info tab > Exported Type Identifiers section.

Expand the section, and click on the Plus (+) button. There are some mandatory fields to provide values for. Here we go:
- Description: Provide a description of the exported type. Here: SampleNote File.
- Identifier: A unique, reverse-DNS identifier for the file type. It’s usually in the form of “com.Your_Name_Or_Organization.FileExtension”. Here:
com.Your_Name_Or_Organization.sampleNote. - Extensions: That’s the place to specify the custom file extension. Here: samplenote.
- Conforms To: Not a mandatory field, but a nice-to-have so QuickLook or the system knows the type that this one conforms to. Here: public.data.
Additionally, you should provide an icon for the exported files, but that’s something we’ll just skip here.
The following image shows the Exported Type Identifiers section filled in. Remember, if your app exports multiple custom file types, then multiple matching entries should exist too:

Making the system capable of opening custom file types
The above declares a custom file type to the system, but it’s not enough to make it open with our app. What makes that possible is the addition of a new Document Type entry.
Still in the Info tab, right above the Exported Type Identifiers, there is the Document Types section. Expand it, and once again, click on the Plus (+) button to create a new entry.
There are four fields of interest here:
- Name: A name for the document type. Optionally, it could be the same to the Description field in the Exported Type Identifiers, and that’s the value to set in this example: SampleNote File.
- Identifier: This should be the exact same identifier to the Identifier field in the Exported Type Identifiers entry. Make sure to either type it in correctly, or just copy and paste it: com.Your_Name_Or_Organization.samplenote.
- Role: Make sure that the Editor value is selected.
- Handler Rank: Set the Owner value, indicating that the app owns the document type.

Running the app again and create a new note. This time, macOS suggests only our app to open the file with! However, don’t try that yet, as the app still cannot handle incoming files.

If you don’t see your app listed in the Open With submenu, then Clean Build the project first, and then run again. In the worst case, restart Xcode or even your computer, but usually that’s not necessary.
Handling an incoming file URL
When the system initiates the opening of a custom file type, the app receives the URL to the file, and not the file itself as some sort of an object. So, our next move is to make the app capable of receiving such an incoming URL, and then load the file contents appropriately.
In SwiftUI, the app receives an incoming URL with the onOpenURL modifier. One important thing to keep in mind is that onOpenURL should be added to the top-most view of a window, and not to views that appear deeper in the view hierarchy. When opening files from the system, SwiftUI may create additional windows. If the same file-opening logic or state is shared between windows, this can lead to unexpected behavior. For this reason, it’s recommended to handle incoming file URLs at the top-most view of each window.
Since this sample project has one view only, we’ll add the onOpenURL to the ContentView:
|
1 2 3 4 5 6 7 |
VStack { … } // … other modifiers … .onOpenURL { url in } |
As you can see, the onOpenURL modifier gets a closure as argument, which contains the incoming URL as a parameter value.
We’ll handle the URL and the file opening in a dedicated method, where the URL is given as argument. In it, we ensure that the file exists first, and then we load its contents as a Data object. After that, we decode from JSON to a Note object.
If everything runs smoothly, we assign the loaded title and content to the local properties:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func loadNote(at url: URL) { // Make sure that the file exists. guard FileManager.default.fileExists(atPath: url.path) else { return } do { let data = try Data(contentsOf: url) let loadedNote = try JSONDecoder().decode(Note.self, from: data) title = loadedNote.title content = loadedNote.content } catch { print(error) } } |
Note that in real-world apps, file operations, and I/O operations in general, should run in a separate task. For the sake of the simplicity we avoid that here.
With the loadNote(at:) in place, we can update the onOpenURL as shown next:
|
1 2 3 4 5 |
.onOpenURL { url in loadNote(at: url) } |
And we’re done! From now on, macOS will launch our app and present a new window with the loaded note when double-clicking a .samplenote file in Finder.
☝️ A small side note; run the app through Xcode first before opening files while still implementing. You’ll be able to debug and find out if there’s something wrong when fetching the file from the received URL.
Wrapping up
Three things to remember; exported type identifiers, document types and the onOpenURL modifier in SwiftUI. These are what you need in order to define your own custom file types and make them recognizable by the system. As for the details, they are all presented in the previous parts. I hope you found this post useful. Thanks for reading!