The curious case of NSFileCoordinator in Swift
Asked Answered
A

1

6

It seems that NSFileCoordinator, despite being a pretty important class, has not been given any Swiftification. It has not been given any async alternative; you have to provide a completion handler. Moreover, it takes an NSErrorPointer rather than being marked throws, and the pointee cannot be referred to inside the completion handler:

let url = URL(fileURLWithPath: "fakepath")
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { url in
    let theError = error
    print(theError as Any)
}

This results in an error: "Simultaneous accesses to error, but modification requires exclusive access."

If I can't access the error from inside the block, what use is it? How am I supposed to access it? Surely not after the block; the completion handler is called asynchronously (because we have to wait until coordinated access is possible), so after the block, the error will always be nil even if there was in fact an error.

(I notice that Apple's own example code skirts this issue by never checking the value of its error variable at all.)

Albanian answered 18/9, 2022 at 15:45 Comment(0)
A
2

Apparently this method is even weirder than I thought. According to the docs, if there is an error, the completion handler is never called. Thus, checking error for a non-nil value after the completion handler might actually be somewhat useful.

Moreover, the docs also say that you are expected to set a separate "sentinel" value to indicate failure initially and then set it to indicate success within the completion handler. So the canonical architecture would be:

let url = URL(fileURLWithPath: "fakepath")
var failure = true
var error: NSError?
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { url in
    failure = false
}
if failure {
    print(error as Any)
}

I've never actually seen this architecture used, but that appears to be what you're supposed to do.

Albanian answered 18/9, 2022 at 15:45 Comment(3)
You say in your question "the completion handler is called asynchronously". It's actually not asynchronous; the closure parameter is non-escaping, so it must be called synchronously. This is why it's fine to check the value of error after the call to coordinate(readingItemAt: ...). (Also, I don't know why Apple suggests to use a sentinel value when you can just check if the error is nil or not. Maybe it's possible for error to still be nil if the coordination fails for some obscure reason?)Solingen
I think you're right; maybe the call just blocks, plain and simple, and then either calls the handler or skips it, and we proceed to the next lines. That might be a good reason to run on a background thread. You might want to give that as answer. The documentation is astoundingly hard to understand. I tried but failed to find good examples, even though we are told to always use a file coordinator in a UIDocumentPickerControllerDelegate (which is how this arose for me).Albanian
Wondering if at least should be doing something like ``` extension NSFileCoordinator { func coordinate(writingTo: URL, onSuccess: (URL) -> Void) throws { var error: NSError? = nil coordinate(writingItemAt: writingTo, error: &error, byAccessor: onSuccess) if let error { throw error } } } ``` But probably if this does block there is some much nicer approach with modern Swift.Whereat

© 2022 - 2024 — McMap. All rights reserved.