Recently, I noticed that the newly introduced fileImporter
and fileExporter
view modifiers in SwiftUI won’t work inside a Menu
view. It’s not a big deal to put the file modifiers outside of the menu, but it took quite a while for me to pin down the problem.
Here is an example of the issue:
First of all I’ve created a simple text file document type as it is shown here:
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 |
import SwiftUI import UniformTypeIdentifiers struct TextFile: FileDocument { // tell the system we support only plain text static var readableContentTypes = [UTType.plainText] // by default our document is empty var text = "" // a simple initializer that creates new, empty documents init(initialText: String = "") { text = initialText } // this initializer loads data that has been saved previously init(configuration: ReadConfiguration) throws { if let data = configuration.file.regularFileContents { text = String(decoding: data, as: UTF8.self) } } // this will be called when the system wants to write our data to disk func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = Data(text.utf8) return FileWrapper(regularFileWithContents: data) } } |
With this I’ve created my FileExporterView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import SwiftUI struct ExportFileView: View { @State private var showExportPanel = false private let myDocument = TextFile(initialText: "My text document") var body: some View { Button(action: { showExportPanel = true }, label: { Text("Export File") }) .fileExporter(isPresented: $showExportPanel, document: myDocument, contentType: .plainText) { result in switch result { case .success(let url): print("Saved to \(url)") case .failure(let error): print(error.localizedDescription) } } } } |
For completeness, I’ve also implemented my FileImporterView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import SwiftUI struct ImportFileView: View { @State private var showImportPanel = false var body: some View { Button(action: { showImportPanel = true }, label: { Text("Import File") }) .fileImporter(isPresented: $showImportPanel, allowedContentTypes: [.plainText]) { result in switch result { case .success(let url): print("Read from \(url)") case .failure(let error): print(error.localizedDescription) } } } } |
Now, let’s wrap it up to demonstrate the problem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import SwiftUI struct ContentView: View { var body: some View { VStack { ImportFileView() ExportFileView() Menu(content: { ImportFileView() ExportFileView() }, label: { Text("My Menu") }) } .padding() } } |
The separate commands in the VStack
work as expected but the ones in the Menu
won’t. The panels just won’t show up.
The solution is quite simple: Just put the fileImporter
/ fileExporter
view modifiers outside the Menu
. Here are the modified views:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import SwiftUI struct ExportFileView2: View { @Binding var showExportPanel: Bool var body: some View { Button(action: { showExportPanel = true }, label: { Text("Export File") }) } } |
and
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import SwiftUI struct ImportFileView2: View { @Binding var showImportPanel: Bool var body: some View { Button(action: { showImportPanel = true }, label: { Text("Import File") }) } } |
and the usage in the Menu
:
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 |
import SwiftUI struct ContentView: View { @State private var showImportPanel = false @State private var showExportPanel = false private let myDocument = TextFile(initialText: "My text document") var body: some View { VStack { ImportFileView() ExportFileView() Menu(content: { ImportFileView() ExportFileView() }, label: { Text("My Menu") }) Menu(content: { ImportFileView2(showImportPanel: $showImportPanel) ExportFileView2(showExportPanel: $showExportPanel) }, label: { Text("My Second Menu") }) } .padding() .fileImporter(isPresented: $showImportPanel, allowedContentTypes: [.plainText]) { result in switch result { case .success(let url): print("Read from \(url)") case .failure(let error): print(error.localizedDescription) } } .fileExporter(isPresented: $showExportPanel, document: myDocument, contentType: .plainText) { result in switch result { case .success(let url): print("Saved to \(url)") case .failure(let error): print(error.localizedDescription) } } } } |
A little bit ugly but it works.