2

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

Duncan C
  • 128,072
  • 22
  • 173
  • 272

5 Answers5

3

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
    }
} 
  • I changed this to get rid of spinning beach ball problem on FileDocument-derived documents, please see my answer below. Change is to return config itself instead of binding from setSourceURL, and get binding from that. – dang Oct 21 '22 at 21:03
2

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

Swift Dev Journal
  • 19,282
  • 4
  • 56
  • 66
  • Thank you for your explanations. However, in a SwiftUI Document-based App, a newly opened document is only handed a filewrapper as part of the Readconfiguration in `init(configuration: Readconfiguration)`, and the preceding fileImporter values seem not accessible to me. Is there a way to access those? – nonresidentalien Feb 10 '22 at 15:29
  • I don't know of any way to get the file URL from a SwiftUI read configuration. I updated my answer with an article link that may help you. – Swift Dev Journal Feb 10 '22 at 18:50
  • Thanks for your response and the link! – nonresidentalien Feb 11 '22 at 18:35
1

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

Blindy
  • 65,249
  • 10
  • 91
  • 131
0

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.

dang
  • 587
  • 1
  • 5
  • 10
  • Hi dang, thanks for the elegant approach. Changed the setSourceURL() parameter label to optional. Unfortunately, getting a spinning beachball after opening a file or choosing new document. – nonresidentalien Oct 20 '22 at 16:32
  • Yes, I discovered that myself on FileDocument-derived documents (not a problem with ReferenceFileDocument descendants). Changing setSourceURL to return the FileDocumentConfiguration instead of the binding (and then grabbing the binding from the returned config) solved the problem for me. I updated the update, do you still have problems? – dang Oct 21 '22 at 20:56
  • I implemented the changes but the program still enters an infinite loop after presenting the document window. – nonresidentalien Oct 24 '22 at 15:16
0

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)
        }
    }
}
Angelo
  • 51
  • 1
  • 5
  • 1
    According to the [docs](https://developer.apple.com/documentation/swiftui/filedocument), you should use [`ReferenceFileDocument`](https://developer.apple.com/documentation/swiftui/referencefiledocument) instead – amq May 20 '23 at 17:29