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

Time:09-23

My users are able to store files locally in the Documents/ folder. Later, they can delete those files from a list. I delete the files like this:

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

Issue

After the files has been deleted, I need to update the list of files (urls). However, my app crashes on the updateURLs() call providing nothing but this error:

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

Workaround

I assume this is because the urls are updated before the file has been completely deleted. Then, the url is accessed for which the file now does not exist anymore; hence the app crashes.
When wrapping the updateURLs() call in a delay like this:

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

...it does not crash and my list updates properly. However, this isn't really a solution because

  1. it leads to bad UX – unnecessarily long waiting time when the file has been deleted quickly
  2. if the file takes longer than 1s to be deleted, it will crash again

Research

From a somewhat related SO question, I figure the .removeItem(:) method should be synchronous, not async. However, my workaround using the delay makes me guess otherwise.

Question

How do I know when the file was actually deleted so I can update my list at the right time? The .removeItem(:) method doesn't seem to have a completion handler...


Full stack trace from the crash:

(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

CodePudding user response:

The solution matt referenced from the question I linked in my original post turns out to work fine – just not in the simulator. It is:

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

...and then used like this:

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

Even with this code, the app crashes in my iOS 15.0 beta simulator, but doesn't on my iPhone with iOS 15.0 beta.

  • Related