简体   繁体   中英

SwiftUI + DocumentGroup in iOS/iPadOS: how to rename the currently open document

My question is regarding SwiftUI's DocumentGroup: using a simple, template-based project in Xcode (using the new multi-platform, document-based app template), I can create new documents, edit them, etc. Also, "outside" the app, I can manipulate the document file as such - move it around, copy it, rename it, etc.

As by default, all new documents are initialised with "Untitled" name; in the main app point of entry, 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 can I edit/change the actual file name once the document is "open", ie, when the code is running on the scope of ContentView ? The lack of documentation for SwiftUI is making looking for answers to problems like this very hard - I think I've scourged the entire Internet but it seems no one is having these kinds of issues, and if they do, their posted questions have no answers - I have posted a couple of questions myself on other issues, and haven't got any comments even, let alone answers.

I have another question, that I think is somewhat related: I have seen in the Files app, for example, that some file types, when selected, can display additional extended information under the "INFORMATION" pane for that file (for example: video files show dimensions in pixels, duration and codec info); my app's documents contain a couple of values (within the saved data) that I would like the user to be able to "glance" in the document picker without having to open the file per se, in a similar fashion as described for the Files app.

My second question is then: is this possible to do, and if so, where can I at least start looking for answers? My guess is this is not "possible" with SwiftUI as such right now, so it would have to be an integration with "regular" Swift?

Thanks in advance for any guidance.

Ok, so here's the thing: I "sort of" managed to achieve what I was after, albeit it doesn't look (to me) like the most "correct" way to do this, AND there is still a problem with the process - although at the moment I am blaming it on a (apparently known) buggy DocumentGroup implementation that is also causing other problems (please see this question for more details on the issue).

The way I "sort of" managed to change the file name was as in the following code:

@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 this does is: it "stores" the initial URL for the document in the state variable right after the view is initialised (in previousFileURL ) by assigning it within the .onAppear modifier (I did this because I have no idea how to get a reference to the file passed in the DocumentGroup closure). THEN, by using the .onDisappear modifier, I use FileManager 's moveItem to assign the new name - by simply "moving" the file from the previous URL to the newly generated one (which in effect SHOULD rename the file); the provided sample code is using a hard-coded string newFileName , but in my actual code (it would be too long to post here practically) I am extracting this new filename from a value that is stored in the actual document, which in turn is a string that the app user can edit when the document is open (makes sense?).

ISSUES

This has currently a very annoying issue: under a set of circumstances (that is, when the app is freshly launched, and a new document has been created by using the "plus" button), the code behaves as I expect it to - it opens the new document, where I can (using the "content view") edit (and store) the string that will become the file name, and when I "close it" (using the back button on the NavigationView), it updates the file name appropriately, which I can confirm by actually looking at the file in the document browser.

BUT... if I re-open the same file, or work with another one file, or simply do the whole process of creating a new file, etc. again WITHOUT closing the app, then apparently DocumentGroup somehow messes up FileManager to a point where the moveItem operation actually COPIES the file (with the new name) but does not remove or actually rename the "old" one, 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 old-named file being there: when it gets to these conditions, FileManager.default.fileExists actually finds the previous/old file, but when "moving" it to the new name it then copies it instead of renaming it. Odd, but I am assuming it is because of the (apparent) bugs I mentioned in the link above.

Hope this points someone with more experience and understanding to a better answer, that they will (hopefully) share here.

The issues experienced with the above "solution" were related to a (confirmed) bug with the .fileImporter modifier - so, this "works", hacky as it is.

Have you tested the above "hacky" solution on device? It runs well 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 have dug deeper and tried overwriting standard init() and FileWrapper function definitions of the Document.swift standard code generated by XCode, to set the desired file name to preferredFilename and filename properties of 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
    }
}

Here is the console print out. I have added user actions in brackets []:

[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 initial document creation, the first write (with func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper ), the file name is properly assigned to FileWrapper . However, when the view code loads the document, it is clear that none of filename name properties of FileWrapper has been used. The same repeats when the document is closed (writing with FileWrapper with names assigned) and opened again.

This looks like a bug. I don't understand why the DocumetGroup doesn't use the file name properties of the FileWrapper , whilst definitely using the data content provided by the same FileWrapper .

I haven't tried this on the new SwiftUI (iOS14) yet. I will do and report back.

UPDATE: Tested on iOS 14 now, does not work there either. Time to radar I suppose.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM