macosswiftuimenuitemswiftdata

Implementing Export/Import menu commands?


I have an import (and export) button(s) that works fine placed in a view.

var body: some View {
    Button(action: {
        isImporting = true
    }, label: {
        Label("Import", systemImage: "square.and.arrow.down")
    })
    .fileImporter(
        isPresented: $isImporting,
        allowedContentTypes: [UTType.log, UTType.text, UTType.xml, UTType.zip],
        allowsMultipleSelection: true
    ) { result in
        switch result {
        case let .success(files):
            files.forEach { file in
                guard file.startAccessingSecurityScopedResource() else { return }
                    .../...
                file.stopAccessingSecurityScopedResource()
            }
        case let .failure(error):
            print(error)
        }
    }
}

But I also want import/export commands available via the File menu in the macOS application.

struct ImportExportCommands: Commands {
    @MainActor
    var body: some Commands {
        CommandGroup(replacing: .importExport) {
            Section {
                ImportButton()
                    .modelContainer(mdContainer.sharedModelContainer)
                ExportButton()
                    .modelContainer(mdContainer.sharedModelContainer)
            }
        }
    }
}

The action code is executed, but no file dialog appears.

I didn't find anything about this so I suppose that is a very stupid thing.

How can I have fileImporter/fileExporter dialog pop up on the screen from a menu command?


Solution

  • I had this problem for a while and never find anything about it. But finally I have found this page:

    ".fileExporter and .fileImporter Won’t Work inside a Menu in SwiftUI" https://fpposchmann.de/fileexporter-and-fileimporter-wont-work-inside-a-menu-in-swiftui/ Thanks to Frank-Peter for the idea.

    I have reworked my code and now commands in the CommandGroup works as it should.

    private struct FactorizedIExportButton: View {
        @Binding var isExporting: Bool
    
        var body: some View {
            Button(action: {
                isExporting = true
            }, label: {
                Label("Export…", systemImage: "square.and.arrow.up")
            })
            .keyboardShortcut("E")
        }
    }
    
    struct ImportButton: View {
        @State var isImporting: Bool = false
    
        var body: some View {
            FactorizedImportButton(isImporting: $isImporting)
                .fileImporter(
                    isPresented: $isImporting,
                    allowedContentTypes: [.text, .kmz],
                    allowsMultipleSelection: true
                ) { result in importFiles(result) }
        }
    }
    
    struct ExportButton: View {
        @State var isExporting: Bool = false
        var document = TextDocument()
    
        var body: some View {
            FactorizedIExportButton(isExporting: $isExporting)
                .fileExporter(
                    isPresented: $isExporting,
                    document: document,
                    contentTypes: [.gpx],
                    defaultFilename: "Export"
                ) { result in exportFile(result) }
        }
    }
    
    struct ImportExportCommands: Commands {
        @State var isImporting: Bool = false
        @State var isExporting: Bool = false
        var document = TextDocument()
    
        @MainActor
        var body: some Commands {
            CommandGroup(replacing: .importExport) {
                Section {
                    FactorizedImportButton(isImporting: $isImporting)
                    FactorizedIExportButton(isExporting: $isExporting)
                }
                .fileImporter(
                    isPresented: $isImporting,
                    allowedContentTypes: [.text, .kmz],
                    allowsMultipleSelection: true
                ) { result in importFiles(result) }
                .fileExporter(
                    isPresented: $isExporting,
                    document: document,
                    contentTypes: [.gpx],
                    defaultFilename: "Export"
                ) { result in exportFile(result) }
            }
        }
    }