Get URL from Open dialog of standard Swift document-based application

1.4k Views Asked by At

I am trying to extract the URL of the document a user has selected in the default "Open" dialogue of my document based macOS application. I understand, a fileWrapper is passed to the init method but is there a way of extracting the path/URL from said wrapper?

Thanks,

Lars

5

There are 5 best solutions below

1
nonresidentalien On BEST ANSWER

DocumentGroup just needs a binding to the document to initialize the ContentView with, so have a func on the document grab the url & return the binding:

App:

import SwiftUI

@main
struct FileOpenApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: FileOpenDocument()) { file in
            ContentView(document: file.document.setSourceURL(file))
        }
    }
}

Document:

struct FileOpenDocument: FileDocument {
    var sourceURL: URL?
    
    init() {
    }

    // needs to be mutating to avoid "self is immutable" error
    mutating func setSourceURL(_ config: FileDocumentConfiguration< FileOpenDocument >) -> Binding<FileOpenDocument> {
        sourceURL = config.fileURL
        return config.$document
    }
} 
2
Blindy On

The FileWrapper has a filename field, so you'd presumably use that.

3
Swift Dev Journal On

The open panel gives you the URL if someone clicks the Open (OK) button. NSOpenPanel has a urls property that contains the URLs of the selected files.

SwiftUI file importers give you a URL if the open was successful.

.fileImporter(isPresented: $isImporting, allowedContentTypes: 
    [.png, .jpeg, .tiff], onCompletion: { result in
    
    switch result {
        case .success(let url):
            // Use the URL to do something with the file.
        case .failure(let error):
            print(error.localizedDescription)
    }
})

UPDATE

SwiftUI's document opening panel works differently than the file importer. You could try working with NSOpenPanel directly. The folllowing article should help:

Save And Open Panels In SwiftUI-Based macOS Apps

3
dang On

The accepted answer from nonresident alien can be simplified, the DocumentGroup closure needs a binding to the document to initialize the ContentView with, so declare a func on the document that grabs the source URL & returns the config, which can then supply the document binding:

struct FileOpenDocument: FileDocument {
    var sourceURL: URL?
    
    mutating func setSourceURL(config: FileDocumentConfiguration<FileOpenDocument>) -> FileDocumentConfiguration<FileOpenDocument> {
        sourceURL = config.fileURL
        return config
    }

} 

The DocumentGroup initializer then becomes:

@main
struct FileOpenApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: FileOpenDocument()) { file in
            ContentView(document: file.document.setSourceURL(file).$document)
        }
    }
}

No modifications to ContentView necessary.

1
Angelo On

You can't declare your FileDocument as a struct with a mutating method. Doing this will trigger the dreaded Publishing changes from within view updates is not allowed, this will cause undefined behavior issue. Until Apples solves this issue we should stick to declaring the FileDocument as a class instead of a Struct. This is to circumvent the issue, not to solve it, since it is desirable to work with structs most of the time.

My solution was to declare my file document as a class. This is from an experiment I am working on:

class MyDocument: FileDocument {
    var pdfURL: URL?
    var pdfFile: PDFDocument?

    init(pdfDocument: PDFDocument = PDFDocument()) {
        pdfFile = pdfDocument
    }

    static var readableContentTypes: [UTType] { [.pdf] }

    required init(configuration: ReadConfiguration) throws {
        guard let data = configuration.file.regularFileContents
        else {
            throw CocoaError(.fileReadCorruptFile)
        }
        pdfFile = PDFDocument(data: data)
    }

    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        return .init()
    }
    
    func setSourceURL(config: FileDocumentConfiguration<GPTFileDialogDocument>) -> FileDocumentConfiguration<GPTFileDialogDocument> {
        pdfURL = config.fileURL
        return config
    }
}

This way you can keep the same DocumentGroup initializer as @dang proposed:

@main
struct FileOpenApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: FileOpenDocument()) { file in
            ContentView(document: file.document.setSourceURL(file).$document)
        }
    }
}