Converting directory URL to file URL

I'm trying to use the URL that a SwiftUI file exporter returns to create a PDF. I use the URL to create a PDF context, but the context does not get created, and I get the following error message in Xcode's debug console:

CGDataConsumerCreateWithFilename: failed to open `/path/to/MyPDF.pdf' for writing: Is a directory.

The URL the file exporter returns is a directory URL. How do I convert the directory URL to a file URL so I can create the PDF context?

Given a directory URL, just append the file name to it like so:

let folder = URL(fileURLWithPath: "path/to/directory")
let file = folder.appendingPathComponent("file.pdf")

When I append the filename to the URL the file exporter returns,

// url is the URL the file exporter returns
case .success(let url):
    let filename = url.lastPathComponent
    let folder = URL(fileURLWithPath: url.path)
    let file = folder.appendingPathComponent(filename)
    publishPDF(location: file)

I can create the PDF context, but I get the following error when closing the PDF:

NSURLConnection finished with error - code -1002

And I wind up with an empty PDF.

When I remove the last path component and add the filename,

case .success(let url):
    let filename = url.lastPathComponent
    let directoryURL = url.deletingLastPathComponent()
    let folder = URL(fileURLWithPath: directoryURL.path)
    let file = folder.appendingPathComponent(filename)
    publishPDF(location: file)

I get the same error message about the URL being a directory.

I don't know what "file exporter" is and what "publishPDF" does exactly.

print the url you are getting from file exporter,

if you are given a file URL like "file://path/to/directory/file.pdf" - then use it directly.
if you are getting a folder URL: "file://path/to/directory" - then append a file name to it.
is it "file" scheme url to begin with?

The following doesn't make much sense:

// assuming url = file://path/to/directory/file.pdf
let filename = url.lastPathComponent // file.pdf
let folder = URL(fileURLWithPath: url.path) // file://path/to/directory/file.pdf
let file = folder.appendingPathComponent(filename) // file://path/to/directory/file.pdf/file.pdf

// assuming url = file://path/to/directory
let filename = url.lastPathComponent // directory
let folder = URL(fileURLWithPath: url.path) // file://path/to/directory
let file = folder.appendingPathComponent(filename) // file://path/to/directory/directory/directory

and in here you are not changing url (assuming it was file url initially):

// assuming url = file://path/to/directory/file.pdf
let filename = url.lastPathComponent // file.pdf
let directoryURL = url.deletingLastPathComponent() // file://path/to/directory
let folder = URL(fileURLWithPath: directoryURL.path) // file://path/to/directory
let file = folder.appendingPathComponent(filename) // file://path/to/directory/file.pdf

// assuming url = path/to/directory
let filename = url.lastPathComponent // directory
let directoryURL = url.deletingLastPathComponent() // path/to
let folder = URL(fileURLWithPath: directoryURL.path) // path/to
let file = folder.appendingPathComponent(filename) // path/to/directory

Also important to know what "publishPDF" expects. a file url? a folder url? a url pointing to a file that's already created? a url pointing to a file that should not be already there?

BTW, -1002 is this:

NSURLErrorUnsupportedURL = 			-1002,

if you are getting `/path/to/MyPDF.pdf' for writing: Is a directory." – worth checking if there's indeed a directory called "MyPDF.pdf", that would explain the error.

The file exporter is SwiftUI's version of a Save panel.

The problem is that the URL the file exporter returns looks like a file URL.

file://path/to/directory/file.pdf

But Core Graphics thinks the URL is a directory so I can't create a PDF context with the URL.

The problem is the system thinks file://path/to/directory/file.pdf is a directory. How do I get the system to treat file://path/to/directory/file.pdf as a fie?

@SwiftDevJournal Maybe you can share all code that you mention here so that we can compile/run it and take a look ourselves?

If you want to reproduce the problem, add the .fileExporter modifier to a view in a document-based SwiftUI app. Set the content type argument for the file exporter to .pdf. Take the URL the file exporter returns and create a PDF context with the URL.

let pdfContext = CGContext(url as CFURL, mediaBox: nil, nil)

You are getting back an error stating that /path/to/MyPDF.pdf is a folder - it might be true and there is indeed a folder named MyPDF.pdf at that location, worth checking.

Try putting these lines before "CGContext(url as CFURL, ...)":

var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
precondition(!exists || !isDirectory.boolValue)
let pdfContext = CGContext(url as CFURL, mediaBox: nil, nil)

i.e. url file should not exist at all or if exists it should be a file not a folder - if exists the file will get overridden.

I placed the lines you suggested before the line that creates the PDF context.

The exists value is true. The value of isDirectory is also true.

I get to the line that creates the PDF context. When I execute the line, I get the same error message in Xcode's debug console that I mentioned in the original question.

CGDataConsumerCreateWithFilename: failed to open `/path/to/MyPDF.pdf' for writing: Is a directory.

I can work around the problem on Mac by using NSSavePanel instead of SwiftUI's file exporter. But I can't use NSSavePanel on iOS.

How did that strange directory MyPDF.pdf, with a file-name-like name, get created in the first place? Can you delete that directory and try again?

How did that strange directory MyPDF.pdf , with a file-name-like name, get created in the first place?

That's what SwiftUI's file exporter returns when someone chooses the location to store the PDF file. The only reason I can think why this occurs is that SwiftUI documents use file wrappers to save the document. Because the document is a file wrapper, SwiftUI decides to export the document as a file wrapper too when I want a PDF file.

Can you delete that directory and try again?

How do I do that? I tried removing the last path component that contains the filename and appending the filename again. But when I do that I still get the error about the URL being a directory.

It was probably created by an earlier bug in your code. Just temporarily delete that folder from your app using FileManager APIs in the next run.

The following sample doesn't show the issue you are describing – the url created points to a file, not a directory and thus happily passes the precondition(!exists || !isDirectory.boolValue) test. For simplicity this example writes png instead of pdf:

Minimal working sample app
import SwiftUI
import UniformTypeIdentifiers

struct Document: FileDocument {
    static var readableContentTypes: [UTType] { [.png] }
    static var writableContentTypes: [UTType] { [.png] }
    
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        let image = UIImage(systemName: "graduationcap.fill")!
        let data = image.pngData()!
        let wrapper = FileWrapper(regularFileWithContents: data)
        wrapper.filename = "FileName.png"
        return wrapper
    }
    
    init() {}
    init(configuration: ReadConfiguration) throws {
        fatalError()
    }
}

struct TestView: View {
    @State var presented: Bool = false
    
    var body: some View {
        Button("Show File Dialog") {
            presented = true
        }
        .fileExporter(isPresented: $presented, document: Document(), contentType: .png) { result in
            switch result {
            case .success(let url):
                var isDirectory = ObjCBool(false)
                let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
                precondition(!exists || !isDirectory.boolValue)
                print(url.path)
            case .failure:
                break
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        TestView()
    }
}

@main
struct TheApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

This question is not about Swift language itself and you'll have better answers elsewhere (Apple Dev Forums, stackoverflow or via filing a DTS incident).