FileManager .removeItem(at:) 需要很长时间——我怎么知道它何时完成?

FileManager .removeItem(at:) takes long – how do I know when it's done?

我的用户能够将文件本地存储在 Documents/ 文件夹中。稍后,他们可以从列表中删除这些文件。我这样删除文件:

List {
    ForEach(urls, id: \.self) { url in
        Text(url.lastPathComponent)
    }
    .onDelete { row in
        guard let i = row.first, let fileURL = urls[safe: i] else { return }
        do {
            try FileManager.default.removeItem(at: fileURL)
            updateURLs() // → crashes here
        }
        catch { print(error.localizedDescription) }
    }
}

// ...

func updateURLs() {
    urls = FileManager.default.urls(
        in: FileManager.default.documentsDirectory
            .appendingPathComponent("unuploaded-documents")
    )
}

// extension of FileManager:
extension FileManager {
    public var documentsDirectory: URL {
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: "")
    }
    
    public func urls(in directory: URL) -> [URL] {
        var fileURLs: [URL] = []
        if let enumerator = FileManager.default.enumerator(at: directory, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
            for case let fileURL as URL in enumerator {
                do {
                    let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
                    if fileAttributes.isRegularFile ?? false {
                        fileURLs.append(fileURL)
                    }
                } catch { print(error, fileURL) }
            }
        }
        return fileURLs
    }
}

问题

文件删除后,我需要更新文件列表(urls)。但是,我的应用程序在 updateURLs() 调用时崩溃,只提供此错误:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

解决方法

我认为这是因为 urls 在文件被完全删除之前更新了。然后,访问 url 文件现在不再存在;因此应用程序崩溃。
当像这样延迟包装 updateURLs() 调用时:

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    updateURLs()
}

...它不会崩溃并且我的列表会正确更新。然而,这并不是真正的解决方案,因为

  1. 它会导致糟糕的用户体验——文件被快速删除时不必要的长时间等待
  2. 如果文件删除时间超过1s,会再次崩溃

研究

开始,我认为 .removeItem(:) 方法 应该是 同步的,而不是异步的。但是,我使用延迟的解决方法让我猜测不是这样。

问题

我如何知道文件实际删除的时间,以便我可以在正确的时间更新我的列表? .removeItem(:) 方法似乎没有完成处理程序...


崩溃的完整堆栈跟踪:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x00007fff5b5e19f3 SwiftUI`SwiftUI.SystemListDataSource.contextForItem(index: (Swift.Int, Swift.Int)) -> SwiftUI._RowVisitationContext<SwiftUI.SystemListDataSource<τ_0_0>> + 304
    frame #1: 0x00007fff5b5e1a5d SwiftUI`protocol witness for SwiftUI.ListCoreDataSource.contextForItem(index: (τ_0_0.SectionIDs.Index, τ_0_0.RowIDs.Index)) -> SwiftUI._RowVisitationContext<τ_0_0> in conformance SwiftUI.SystemListDataSource<τ_0_0> : SwiftUI.ListCoreDataSource in SwiftUI + 15
    frame #2: 0x00007fff5b783db6 SwiftUI`SwiftUI.ShadowListDataSource.contextForItem(index: (τ_0_0.SectionIDs.Index, τ_0_0.RowIDs.Index)) -> SwiftUI._RowVisitationContext<SwiftUI.ShadowListDataSource<τ_0_0>> + 752
    frame #3: 0x00007fff5b783f0e SwiftUI`protocol witness for SwiftUI.ListCoreDataSource.contextForItem(index: (τ_0_0.SectionIDs.Index, τ_0_0.RowIDs.Index)) -> SwiftUI._RowVisitationContext<τ_0_0> in conformance SwiftUI.ShadowListDataSource<τ_0_0> : SwiftUI.ListCoreDataSource in SwiftUI + 9
    frame #4: 0x00007fff5b91910b SwiftUI`SwiftUI.ListCoreDataSource.visitRowAt<τ_0_0>(_: (τ_0_0.SectionIDs.Index, τ_0_0.RowIDs.Index), visitor: (SwiftUI._RowVisitationContext<τ_0_0>) -> τ_1_0) -> τ_1_0 + 506
    frame #5: 0x00007fff5b917e73 SwiftUI`SwiftUI.ListCoreDataSource.visitContent<τ_0_0>(atRow: Foundation.IndexPath, visitor: (SwiftUI._RowVisitationContext<τ_0_0>) -> τ_1_0) -> τ_1_0 + 344
    frame #6: 0x00007fff5bb0d053 SwiftUI`SwiftUI.UITableViewListCoordinator.tableView(_: __C.UITableView, canEditRowAt: Foundation.IndexPath) -> Swift.Bool + 785
    frame #7: 0x00007fff5bb114da SwiftUI`merged @objc SwiftUI.UITableViewListCoordinator.tableView(_: __C.UITableView, canEditRowAt: Foundation.IndexPath) -> Swift.Bool + 122
    frame #8: 0x00007fff2516143d UIKitCore`-[UITableView _canEditRowAtIndexPath:] + 96
    frame #9: 0x00007fff2518fbbb UIKitCore`-[UITableView _setupCell:forEditing:atIndexPath:animated:updateSeparators:] + 139
    frame #10: 0x00007fff2517b6e2 UIKitCore`-[UITableView _setEditing:animated:forced:] + 1415
    frame #11: 0x00007fff2517b125 UIKitCore`-[UITableView willMoveToSuperview:] + 126
    frame #12: 0x00007fff254c81b4 UIKitCore`__UIViewWillBeRemovedFromSuperview + 548
    frame #13: 0x00007fff254c7e4c UIKitCore`-[UIView(Hierarchy) removeFromSuperview] + 92
    frame #14: 0x00007fff254481e4 UIKitCore`-[UIScrollView removeFromSuperview] + 62
    frame #15: 0x00007fff254af574 UIKitCore`-[UIView dealloc] + 435
    frame #16: 0x00007fff202f45f7 CoreFoundation`__RELEASE_OBJECTS_IN_THE_ARRAY__ + 115
    frame #17: 0x00007fff202f453d CoreFoundation`-[__NSArrayM dealloc] + 275
    frame #18: 0x00007fff2019a9f7 libobjc.A.dylib`objc_object::sidetable_release(bool, bool) + 177
    frame #19: 0x00007fff2019c175 libobjc.A.dylib`AutoreleasePoolPage::releaseUntil(objc_object**) + 175
    frame #20: 0x00007fff2019c064 libobjc.A.dylib`objc_autoreleasePoolPop + 185
    frame #21: 0x00007fff2877283a QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double, double*) + 712
    frame #22: 0x00007fff287aa007 QuartzCore`CA::Transaction::commit() + 699
    frame #23: 0x00007fff287ab324 QuartzCore`CA::Transaction::flush_as_runloop_observer(bool) + 60
    frame #24: 0x00007fff20362a0d CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    frame #25: 0x00007fff2035d242 CoreFoundation`__CFRunLoopDoObservers + 541
    frame #26: 0x00007fff2035d7f2 CoreFoundation`__CFRunLoopRun + 1126
    frame #27: 0x00007fff2035cea9 CoreFoundation`CFRunLoopRunSpecific + 567
    frame #28: 0x00007fff2c951cd3 GraphicsServices`GSEventRunModal + 139
    frame #29: 0x00007fff24f2f72a UIKitCore`-[UIApplication _run] + 915
    frame #30: 0x00007fff24f34192 UIKitCore`UIApplicationMain + 101
    frame #31: 0x00007fff5b96a40f SwiftUI`closure #1 (Swift.UnsafeMutablePointer<Swift.Optional<Swift.UnsafeMutablePointer<Swift.Int8>>>) -> Swift.Never in SwiftUI.KitRendererCommon(Swift.AnyObject.Type) -> Swift.Never + 196
    frame #32: 0x00007fff5b96a349 SwiftUI`SwiftUI.runApp<τ_0_0 where τ_0_0: SwiftUI.App>(τ_0_0) -> Swift.Never + 148
    frame #33: 0x00007fff5b3b06c1 SwiftUI`static SwiftUI.App.main() -> () + 61
  * frame #34: 0x0000000104fdba0e FMP Dokumente`static FMP_DokumenteApp.$main(self=FMP_Dokumente.FMP_DokumenteApp) at FMP_DokumenteApp.swift:10:1
    frame #35: 0x0000000104fdbab9 FMP Dokumente`main at FMP_DokumenteApp.swift:0
    frame #36: 0x0000000105c0ce1e dyld_sim`start_sim + 10
    frame #37: 0x000000010b04a4d5 dyld`start + 421

我在原始 post 中链接的问题中引用的解决方案 matt 结果证明工作正常 - 只是不在模拟器中。它是:

extension FileManager {
    public func removeItem(at url: URL, completion: @escaping (Bool, Error?) -> ()) {
        DispatchQueue.global(qos: .utility).async {
            do {
                try self.removeItem(at: url)
            } catch {
                DispatchQueue.main.async {
                    completion(false, error)
                }
            }
            
            DispatchQueue.main.async {
                completion(true, nil)
            }
        }
    }
}

...然后像这样使用:

FileManager.default.removeItem(at: fileURL) { success, error in
    updateURLs()
}

即使使用此代码,该应用程序在我的 iOS 15.0 beta 模拟器中崩溃,但在我的 iPhone 和 iOS 15.0 beta 中不会崩溃。