Three view modifiers cover file management: fileImporter picks a file, fileExporter saves a
FileDocument, and fileMover relocates an existing file. Each one presents the system dialog
from a boolean binding.
Import
fileImporter handles the dialog and folder navigation. Restrict what the user can pick with
allowedContentTypes, and read the chosen URL in the completion closure.
Button("Import") { importing = true }
.fileImporter(
isPresented: $importing,
allowedContentTypes: [.plainText]
) { result in
switch result {
case .success(let file): print(file.absoluteString)
case .failure(let error): print(error.localizedDescription)
}
}
Export
Exporting needs a document type conforming to FileDocument: declare its content types, read
from a ReadConfiguration, and write through a FileWrapper.
struct TextDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
var text = ""
init(text: String) { self.text = text }
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
text = String(decoding: data, as: UTF8.self)
} else {
text = ""
}
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
FileWrapper(regularFileWithContents: Data(text.utf8))
}
}
fileExporter then takes the document and its content type, plus the presentation binding.
Button("Export") { exporting = true }
.fileExporter(
isPresented: $exporting,
document: document,
contentType: .plainText
) { result in
// .success(URL) or .failure(Error)
}
Moving
fileMover presents the move dialog for a file URL you already hold and reports the new
location in its completion closure.
Button("Move files") { moving = true }
.fileMover(isPresented: $moving, file: file) { result in
// .success(URL) or .failure(Error)
}