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
- it leads to bad UX – unnecessarily long waiting time when the file has been deleted quickly
- 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.