How to solve SwiftUI + DocumentGroup in iOS/iPadOS: How to rename the currently open document?
My question is about SwiftUI's DocumentGroup: Using a simple, template-based project in Xcode (using the new multi-platform, document-based application template), I can create new documents, edit them, etc. Also, "outside" the application, I can manipulate the document file like this - move it, copy it, rename it, etc.
By default the name "Untitled" ; at the main application entry point, I can access the file 's URL:
var body: some Scene {
DocumentGroup(newDocument: ShowPLAYrDocument()) { file in
// For example,this gives back the actual doc file URL:
let theURL = file.fileURL
ContentView(document: file.$document)
}
}
First question: How do I edit/change the actual filename after the document is "opened" , i.e. when the code is running in ContentView
scope? The lack of SwiftUI documentation makes finding answers to such questions very difficult - I think I've scoured the entire internet raging on, but no one seems to be having this type of problem, and if they have, their posted questions have no answers - I've posted a couple of questions myself on other questions that don't even have any comments, let alone answers .
I have another question, which I think is somewhat related: For example, I've seen in the Files app that certain file types, when selected, can display additional extended information under the Info pane for that file (eg: video file with dimensions in pixels, duration and codec information ); my app's document contains several values (in saved data) and I want the user to be able to "browse" in the document picker without opening the file itself, in a manner similar to that described in the Files application.
My second question is: is this possible, and if so, where can I at least start looking for an answer? My guess is that this isn't "possible" for SwiftUI now, it has to be integrated with "regular" Swift?
Thanks in advance for your guidance.
Solution
Ok, so here's the thing: I "sort of" managed to achieve what I'm after, although it doesn't look like (to me) the most "correct" way to do it, and there are still issues with the process - although for now I'm going to It's blamed on an (apparently known) flawed DocumentGroup
implementation that caused other problems as well (see this question for more details on that).
The way I "sort of" managed to change the filename looks like this:
@main
struct TestApp: App {
@State var previousFileURL: String = ""
var body: some Scene {
DocumentGroup(newDocument: TestDocument()) { file in
ContentView(document: file.$document)
.onAppear() {
previousFileURL = file.fileURL!.path
}
.onDisappear() {
let newFileName = "TheNewFileName.testDocument"
let oldFileName = URL(fileURLWithPath: previousFileURL).lastPathComponent
var newURL = URL(fileURLWithPath: previousFileURL).deletingLastPathComponent()
newURL.appendPathComponent(newFileName)
do {
try FileManager.default.moveItem(atPath: oldURL.path,toPath: newURL.path)
} catch {
print("Error renaming file! Threw: \(error.localizedDescription)")
}
}
}
}
}
What it does is: after the view is initialized (in previousFileURL
), it .onAppear
"stores" it in a state variable by assigning the document's initial URL in the decorator (I did this because I don't know how to get access to the closure in file
the passed DocumentGroup
by reference). Then, by using .onDisappear
modifiers, which I use FileManager
to moveItem
assign the new name - just "move" the file from the previous URL to the newly generated URL (which should actually rename the file); the sample code provided uses hardcoded strings, newFileName
but In my actual code (it's actually too long to post here) I extract this new filename from a value stored in the actual document, which is again a string that the app user can change when the document is opened EDIT (makes sense?).
question
This is currently having a very annoying problem: in one set of circumstances (i.e. when the app is just launched, and a new document is created using the "plus" button), the code behaves as I would expect to - it opens new document, I can (using the "content view") edit (and store) the string that will become the filename, and when I "close it" (using the back button on the navigation view), it will update the filename accordingly , which I can confirm by actually viewing the file in the documentation browser.
BUT... if I reopen the same file, or use another file, or just go through the whole process of creating a new file again without closing the app, then obviously somehow that will mess up DocumentGroup
the FileManager
operation moveItem
actually To the extent that the file is copied (with the new name) but the "old" file is not deleted or actually renamed, so you end up with two files: one with the new name and one with the "old"/previous name.
This happens even if I check for the existence of the old file: when it hits these conditions, it FileManager.default.fileExists
actually finds the previous/old file, but when it "moves" to the new name it copies it instead of renaming it. Weird, but I'm assuming it's because of the (obvious) bug I mentioned in the link above.
Hopefully this leads to a better answer for someone with more experience and understanding, who will (hopefully) share it here.
,
The problem encountered with the "solution" above .fileImporter
is related to the (confirmed) bug with modifiers - so this "works", nonetheless.
,
Have you tested the "hacky" solution above on your device? It works fine on simulator, but due to new access permission rules in iOS 13 , the code throws “XXXXXX” couldn’t be moved because you don’t have permission to access “YYYYYY”.
I've dug a little deeper and tried overriding the standard and function definitions init()
of the standard code generated by XCode , setting the required filename as a property of and :FileWrapper
Document.swift
preferredFilename
filename
FileWrapper
struct SomeDocument: FileDocument,Decodable,Encodable {
static var readableContentTypes: [UTType] { [.SomeDocument] }
var someData: SomeCodableDataType
init() {
self.someData = SomeCodableDataType()
print("Creating.\n")
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
let savedPreferredName = configuration.file.preferredFilename
let savedName = configuration.file.preferredFilename
let fileRep = try JSONDecoder().decode(Self.self,from: data)
self.someData = fileRep.someData
print("Loading.\n Filename: \(savedPreferredName ?? "none") or \(savedName ?? "none")\n")
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
do {
let fileRep = try JSONEncoder().encode(self)
let fileWrapper = FileWrapper.init(regularFileWithContents: fileRep)
fileWrapper.preferredFilename = fileName()
fileWrapper.filename = fileName()
print("Writing.\n Filename \(fileWrapper.preferredFilename ?? "none") or \(fileWrapper.filename ?? "none").\n")
return fileWrapper
} catch {
throw CocoaError(.fileReadCorruptFile)
}
}
func fileName() -> String {
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "yyMMdd'-'HH:mm"
let timeStamp = timeFormatter.string(from: Date())
let extention = ".ext"
let newFileName = timeStamp + "-\(someData.someUniqueValue())" + extention
return newFileName
}
}
This is what the console prints out. I'm in square brackets []:
Added user action in
[CREATE DOC BY TAPPING +]
Creating.
[AUTOMATIC WRITE]
Writing.
Filename 210628-16:49-SomeUniqueValue.ext or 210628-16:49-SomeUniqueValue.ext.
[AUTOMATIC LOAD]
Loading.
Filename: none or none
FileURL: /Users/bora/Library/Developer/CoreSimulator/Devices/F126086A-A752-4A71-B589-1B37DFC02746/data/Containers/Data/Application/D81C9D76-7986-4C0D-BA2C-1FDF69703875/Documents/Untitled 2.ext
isEditable: true
[CLOSING DOC]
Writing.
Filename 210628-16:49-SomeUniqueValue.ext or 210628-16:49-SomeUniqueValue.ext.
[REOPENING DOC]
Loading.
Filename: none or none
FileURL: /Users/bora/Library/Developer/CoreSimulator/Devices/F126086A-A752-4A71-B589-1B37DFC02746/data/Containers/Data/Application/D81C9D76-7986-4C0D-BA2C-1FDF69703875/Documents/Untitled 2.ext
isEditable: true
So after the initial document creation, the first write (use func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper
), the filename is correctly assigned to FileWrapper
. However, when the view code loads the document, it's clear that there is no FileWrapper
filename name attribute used. FileWrapper
The same thing repeats when the document is closed (with write and given a name) and opened again.
This looks like a bug. I don't understand why DocumetGroup doesn't use FileWrapper
the filename attribute, but definitely uses the same for FileWrapper
the provided data content.
I haven't tried this on the new SwiftUI (iOS14). I'll do it and report back.
UPDATE: Now tested on iOS 14, doesn't work there either. I think it's time to start the radar.