Reputation: 6977
I have a basic iOS app that shows a list of documents. I'm trying to delete a document, but noticed that the code below fails with "No such file or directory" if the document has not yet been downloaded from iCloud to the device.
Documents can be quite large (40MB) and I'd like to avoid downloading the document only to delete it (this takes time and bandwidth out of the user's data plan). Is this possible at all?
[[[NSFileCoordinator alloc] initWithFilePresenter:nil]
coordinateWritingItemAtURL:documentURL
options:NSFileCoordinatorWritingForDeleting
writingItemAtURL:previewURL
options:NSFileCoordinatorWritingForDeleting
error:&error
byAccessor:^(NSURL *newDocumentURL, NSURL *newPreviewURL){
// Fails with "No such file" error if not yet downloaded from iCloud:
[[NSFileManager defaultManager] removeItemAtURL:newDocumentURL error:&error];
[[NSFileManager defaultManager] removeItemAtURL:newPreviewURL error:&error];
}];
The full error:
Error Domain=NSCocoaErrorDomain Code=4 "The operation couldn’t be completed.
(Cocoa error 4.)" UserInfo=0x14e82930 {NSUnderlyingError=0x14e69220
"The operation couldn’t be completed. No such file or directory",
Upvotes: 4
Views: 1863
Reputation: 24247
note: Apple used to have some sample code to illustrate this but alas it has been removed.
As has been pointed out in the answers you would need to call this method in a loop since you need a separate NSFileCoordinator
for each file you would like to delete.
Something that I missed was that you have to call fileManager.removeItemAtURL
with the URL of the NSFileAccessIntent
object that you create for deleting and not the normal URL
that you get access to from your NSMetadataQueryItem
.
func removeFile(at url: URL, completionHandler: ((Error?) -> Void)? = nil) {
// `url` may be a security scoped resource.
let successfulSecurityScopedResourceAccess = url.startAccessingSecurityScopedResource()
let fileCoordinator = NSFileCoordinator()
let writingIntent = NSFileAccessIntent.writingIntent(with: url, options: .forDeleting)
fileCoordinator.coordinate(with: [writingIntent], queue: backgroundQueue) { (accessError) in
if accessError != nil {
completionHandler?(accessError)
return
}
let fileManager = FileManager()
var error: Error?
do {
try fileManager.removeItem(at: writingIntent.url)
}
catch let fileError {
error = fileError
}
if successfulSecurityScopedResourceAccess {
url.stopAccessingSecurityScopedResource()
}
completionHandler?(error)
}
}
If you wanted to delete multiple items:
for url in urlsToDelete {
removeFile(at: url)
}
Upvotes: 6
Reputation: 1194
If you just need to delete a file, use the other NSFileCoordinator coordinateWritingItemAtURL ( the one with a single newURL parameter in the accessor block ).
If you need to batch delete, then create an array of NSFileAccessIntent and use NSFileCoordinator's coordinateAccessWithIntents.
Example:
- ( void )deleteItemsAtURLs: ( NSArray * )urls queue: ( NSOperationQueue * )queue
{
//assuming urls is an array of urls to be deleted
NSFileCoordinator * coordinator;
NSMutableArray * writingIntents;
NSURL * url;
writingIntents = [ NSMutableArray arrayWithCapacity: urls.count ];
for( url in urls )
{
[ writingIntents addObject: [ NSFileAccessIntent writingIntentWithURL: url options: NSFileCoordinatorWritingForDeleting ] ];
}
coordinator = [ [ NSFileCoordinator alloc ] initWithFilePresenter: nil ];
[ coordinator coordinateAccessWithIntents: writingIntents
queue: queue
byAccessor: ^( NSError * error )
{
if( error )
{
//handle
return;
}
NSFileAccessIntent * intent;
error = nil;
for( intent in writingIntents )
{
[ [ NSFileManager defaultManager ] removeItemAtURL: intent.URL error: &error ];
if( error )
{
//handle
}
}
}];
}
Upvotes: 2