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?
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?
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?
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.
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).