SwiftUI + DocumentGroup在iOS/iPadOS中:如何重命名当前打开的文档

5
我的问题与SwiftUI的DocumentGroup相关:在Xcode中使用简单的基于模板的项目(使用新的多平台,文档为基础的应用程序模板),我可以创建新文档、编辑文档等。此外,“在应用程序之外”,我可以操作文档文件,例如移动、复制、重命名等。
由于默认情况下,所有新文档都会初始化为“Untitled”名称;在主应用程序入口点,我可以访问文件的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)
    }
}

第一个问题:当文档处于“打开”状态,即在ContentView的范围内运行代码时,我如何编辑/更改实际文件名? SwiftUI缺乏说明文档,这使得像这样寻找问题答案变得非常困难 - 我想我已经搜索了整个互联网,但似乎没有人遇到这些问题,而且如果他们确实遇到了这些问题,他们发布的问题也没有答案 - 我自己发了几个关于其他问题的问题,甚至没有任何评论,更不用说答案了。
我还有另一个问题,我认为与此有些相关:例如,在Files应用程序中,当选择某些文件类型时,可以在该文件的“INFORMATION”窗格下显示其他扩展信息(例如:视频文件显示像素尺寸、持续时间和编解码器信息); 我的应用程序文档包含一些值(在保存的数据中),我希望用户能够在文档选择器中“瞥见”,而不必像Files应用程序中所述的那样打开文件。
我的第二个问题是:这是否可能做到,如果可能,我至少可以从哪里开始寻找答案? 我猜这在SwiftUI本身目前不可能,因此它必须与“常规”Swift集成?
感谢您提前提供的任何指导。
3个回答

1

好的,这里有一件事:我“有点”成功地实现了我想要的目标,尽管它看起来(对我来说)不是最“正确”的方法,并且该过程仍然存在问题 - 尽管我目前将其归咎于(显然已知的)有缺陷的DocumentGroup实现,这也导致其他问题(请参见此问题以获取更多有关该问题的详细信息)。

我“有点”成功更改文件名的方法如下所示:

@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)")
                    }                    
                }
        }
    }
}

这段代码的作用是:在视图初始化后(在previousFileURL中),通过使用.onAppear修饰符将文档的初始URL“存储”到状态变量中(我之所以这样做,是因为我不知道如何获取传递给DocumentGroup闭包的file的引用)。然后,通过使用.onDisappear修饰符,使用FileManagermoveItem来分配新名称-通过简单地将文件从先前的URL移动到新生成的URL(实际上应该重命名文件);提供的示例代码使用硬编码字符串newFileName,但在我的实际代码中(在此处实际发布太长),我正在从实际文档中存储的值中提取此新文件名,该值本身是应用程序用户在打开文档时可以编辑的字符串(有意义吗?)。

问题

目前存在一个非常令人烦恼的问题:在一组特定情况下(即,当应用程序刚启动,并且使用“加号”按钮创建了一个新文档时),代码的行为符合我的预期-它打开了新文档,在那里我可以(使用“内容视图”)编辑(和存储)将成为文件名的字符串,并且当我“关闭它”(使用NavigationView上的返回按钮)时,它会适当地更新文件名,我可以通过实际查看文档浏览器中的文件来确认。

但是……如果我重新打开相同的文件,或者使用另一个文件,或者只是再次执行创建新文件等整个过程而不关闭应用程序,则显然DocumentGroup会以某种方式混淆FileManager,以至于moveItem操作实际上会复制文件(使用新名称),但不会删除或更改“旧”的文件名,因此您最终会得到两个文件:一个带有新名称,另一个带有“旧”/以前的名称。

即使我检查旧名称的文件是否存在,这种情况仍会发生:当它满足这些条件时,FileManager.default.fileExists实际上会找到以前/旧的文件,但是当将其“移动”到新名称时,它会复制而不是重命名。奇怪的是,但我认为这是因为我在上面提到的(显然)错误。希望这能指引更有经验和理解的人给出更好的答案,并在这里分享。

我遇到了你遇到的同样的问题,我几乎搜索了整个互联网,但什么都没找到。这可能是我能得到的最接近的答案了。在你的解决方案中,previousFileURL似乎是整个应用程序的全局变量。也许将其移动到ContentView级别? - fuermosi777

0

你在设备上测试过上述“hacky”解决方案吗?它在模拟器上运行良好,但由于iOS 13中新的访问权限规则, 代码会抛出"XXXXXX"无法移动,因为您没有访问"YYYYYY"的权限。

我深入挖掘并尝试覆盖XCode生成的标准Document.swift代码的标准init()FileWrapper函数定义,将所需的文件名设置为FileWrapperpreferredFilenamefilename属性:

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
    }
}

这是控制台的打印输出。我已经在方括号[]中添加了用户操作:
[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

在初始文档创建后,第一次写入(使用func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper),文件名已正确分配给FileWrapper。然而,当视图代码加载文档时,明显没有使用FileWrapper的任何文件名属性。当文档关闭(使用分配名称的FileWrapper进行写入)并再次打开时,情况相同。

这看起来像是一个错误。我不明白为什么DocumetGroup不使用FileWrapper的文件名属性,而绝对使用由同一FileWrapper提供的数据内容。

我还没有在新的SwiftUI(iOS14)上尝试过。我会尝试并回报。

更新:现在在iOS 14上测试了,也不起作用。我想是时候提交反馈了。


权限问题是可以预料的。您需要使用.startAccessingSecurityScopedResource().stopAccessingSecurityScopedResource()来解决这个问题。 - Vitor Enes
是的,我了解startAccessingSecurityScopedResource并实现了它,但仍然存在问题。如果您不介意我问一下,是否还需要其他步骤(例如权限)才能使其正常工作? - Bora Okumusoglu
不完全是这样。只需确保在正确的“对象”上调用startAccessingSecurityScopedResource - 例如:您将URL存储在变量中(let url =“whatever_url_here”),则应该这样做:url.startAccessingSecurityScopedResource();此外,不要“信任”来自文件系统的直接URL - 您需要为文件创建一个书签,并使用该书签安全地访问应用程序沙盒之外的资源,即: var secureBookmarkData = Data() secureBookmarkData = try fileURL.bookmarkData()(伪代码,请检查语法) - Vitor Enes
谢谢。我已经成功实现了书签以获得安全访问 - startAccessingSecurityScopedResource 返回 true。但是现在我遇到了 Error renaming file! Threw: “Untitled” couldn’t be moved because you don’t have permission to access “Folder Name”. 这次可能是权限问题... - Bora Okumusoglu
你是否曾经得到过在设备上完整的工作序列?我正在尝试通过这里的各种解决方案和评论来梳理它,但是我很难跟进实际起作用的内容。 - Steven Hovater
不,它确实不能按预期在设备上工作。最好的情况是,它会创建一个新文件并将旧文件复制一份留下。我放弃在我的应用程序中实现它了。 - Bora Okumusoglu

0
上述“解决方案”遇到的问题与.fileImporter修饰符存在(已确认的)错误有关,因此,尽管有些hacky,但这个“解决方案”仍然可行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接