Files and persistence
Let the user choose a file
Use fileImporter for user-selected files and keep sandbox access scoped to the URL the user approved.
12 minute lesson
Sandboxed Mac apps cannot read arbitrary files, and that is by design. Your app sees its own container and nothing else. The way to reach the user’s files is user intent: the user picks a file in a system dialog, and that choice is the permission grant.
This is the piece that surprises people coming from other platforms. There is no “files permission” to request up front. Access arrives one URL at a time, attached to an explicit user action.
SwiftUI exposes the open panel through the fileImporter modifier. Suppose we let users import a text file into their notes:
struct ImportButton: View {
@EnvironmentObject private var model: NotesModel
@State private var showImporter = false
var body: some View {
Button("Import Text File…") {
showImporter = true
}
.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [.plainText]
) { result in
if let url = try? result.get() {
model.importNote(from: url)
}
}
}
}
The panel that appears is the real system open panel, running outside your process. Your app never sees the filesystem during browsing — it receives only the final, approved URL.
That URL is security-scoped. Before reading it you must start access, and you must stop when done:
func importNote(from url: URL) {
guard url.startAccessingSecurityScopedResource() else { return }
defer { url.stopAccessingSecurityScopedResource() }
if let text = try? String(contentsOf: url, encoding: .utf8) {
notes.append(Note(id: UUID(), title: url.lastPathComponent, body: text))
}
}
defer guarantees the stop call runs however the function exits. Keep the window between start and stop short — do the read, then get out.
The grant does not survive a relaunch. If your app needs to reopen the same file tomorrow, store a security-scoped bookmark:
let bookmark = try url.bookmarkData(
options: .withSecurityScope
)
// persist the Data, then later:
var stale = false
let restored = try URL(
resolvingBookmarkData: bookmark,
options: .withSecurityScope,
bookmarkDataIsStale: &stale
)
The bookmark encodes the permission itself, so resolving it after relaunch restores access without asking the user again.
The classic mistake: saving url.path as a string and trying to read it on the next launch. The path is correct, the file exists, and the read fails with a permission error anyway, because the string carried none of the grant. If a file works until the app restarts, this is almost always what happened.
Verify both halves: import a file from your Desktop and see the note appear, then relaunch and confirm the bookmark-restored URL still reads.
Lesson completed