Download in documents directory or move file async - iOS - ios

I am building an iOS app in which the user can download different files.
I am using an URLSessionDownloadTask and an URLSession to download a file asynchronously.When the download is finished, the destination folder is by default, the tmp/ directory.
So, when the download ends, I need to move the temporary file to another directory.For a picture or a song, this takes only 1 second maybe even less. But when the file is a video for example, it can take up to 15 seconds.
The issue
To allow the user to still interact with the app, I would like to make this move asynchronous.Each time I try to do that, the file manager throws an exception.
“CFNetworkDownload_xxxxxx.tmp” couldn’t be moved to “Downloads” because either the former doesn't exist, or the folder containing the latter doesn't exist.
What have I tried
I tried to put the call to the file manager in a background thread, it throws.
I tried to remove the destination file before calling the move method, to make sure that the file doesn't already exists.
I tried to make a call to the copy function, before removing the file from the tmp/ directory.
My code
The call to the file manager looks like that.
func simpleMove(from location: URL, to dest: URL) -> Bool {
let fileManager = FileManager.default
do {
try fileManager.moveItem(at: location, to: dest)
return true
} catch {
print("\(error.localizedDescription)")
return false
}
}
When I put that in a background thread, I do it like that.
DispatchQueue.global().async {
if !simpleMove(from: location, to: dest) {
//Failure
}
}
Questions
How can I possibly move a really large file without affecting the UI?
It would be an even better solution to download the file directly in a permanent directory. How can I do that?
When I make the call to my simpleMove(from:to:) synchronously, it works perfectly.So, why the error says that the destination directory doesn't exists? (or something like that, I'm not sure of the meaning of that error)
Thanks.
Note
The code above is written in Swift 3, but if you have an Objective-C or a Swift 2 answer,feel free to share it as well!

Amusingly, the correct answer to this was posted in another question, where it was not the correct answer.
The solution is covered in Apple's Documentation where they state:
location
A file URL for the temporary file. Because the file is temporary, you must either open the file for reading or move it to a permanent location in your app’s sandbox container directory before returning from this delegate method.
If you choose to open the file for reading, you should do the actual reading in another thread to avoid blocking the delegate queue.
You are probably calling simpleMove from the success handler for the DownloadTask. When you call simpleMove on a background thread, the success handler returns and your temp file is cleaned up before simpleMove is even called.
The solution is to do as Apple says and open the file for reading:
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let file: FileHandle = try FileHandle(forReadingFrom: location)
DispatchQueue.global().async {
let data = file.readDataToEndOfFile()
FileManager().createFile(atPath: destination, contents: data, attributes: nil)
}
} catch {
// Handle error
}
}

I came across the same issue but i solve it.
First check that the file is exist in that path because i got issue because of the path extension are different of location URL. i was trying to rename audio but path extension was different(eg. mp3 to m4a)
Also in case there is any other file already exists at destination path
this issue arise.
So first try to check file exists at location where you by using
let fileManager = FileManager.default
if fileManager.fileExists(atPath: location.path) {
do {
try fileManager.moveItem(at: location, to: dest)
return true
} catch {
print("\(error.localizedDescription)")
return false
}
}
Hope this will help you

Related

Alternative UIActivityViewController save dialog for Mac Catalyst or solution for UIDocumentPickerViewController throwing error code 260

I am looking for an alternative export menu other then UIActivityViewController for a Mac Catalyst app. While this works, the user can not choose where they want to save the file (the file is a JSON file of all the items in a list) and I would like the user to be able to choose the directory they want to save the JSON to. I have tried the following code but it gives the error "Error Domain=NSCocoaErrorDomain Code=260 'The file 'name.json' couldn’t be opened because there is no such file'" when you try to save a file.
The Code:
let fileManager = FileManager.default
do {
let fileURL2 = fileManager.temporaryDirectory.appendingPathComponent("\(detailedList.lname!).json")
// Write the data out into the file
try jsonData.write(to: fileURL2)
// Present the save controller. We've set it to `exportToService` in order
// to export the data -- OLD COMMENT
let controller = UIDocumentPickerViewController(url: fileURL2, in: UIDocumentPickerMode.exportToService)
present(controller, animated: true) {
// Once we're done, delete the temporary file
try? fileManager.removeItem(at: fileURL2)
}
} catch {
print("error creating file")
}
I have tried Googling other ways or ways to get this to work but I cannot find anything that will work on Mac Catalyst. I know you can do this because I have seen other apps and examples do it but nothing works for me. So what would be a possible alternative way of doing this or a solution to this code?
The problem is that you are removing the file you wish to save before the user has a chance to choose where they want to save it.
The completion handler where you call try? fileManager.removeItem(at: fileURL2) is called as soon as the document picker is displayed.
The proper solution is to delete the file in the UIDocumentPickerDelegate methods, not when the picker is presented.

How to delete files in iOS Notification Service Extension?

I have a UNNotificationServiceExtension that downloads videos and images to the Documents directory for use by classes that adopt UNNotificationContentExtension. I want to delete the media files that are no longer being used by any notifications. I am not sure how to go about doing this.
I tried to delete the files in my AppDelegate, but I believe the UNNotificationServiceExtension has its own Documents directory per the "Sharing Data With Your Containing App" section of this document: https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html, so I cannot access these files from my main app. They are in a different container.
I don't want to create an App Group to share the data between the app and the extension just so that I can delete the unused files.
I don't want to delete the unused files in the UNNotificationServiceExtension, because the extension has a limited amount of time in which to complete its work, and if I try to download files and delete other files, it may time out.
I think the best option is to check to see which files are needed by any delivered notifications and to delete the unneeded files in the Notification Service Extension's Documents directory. My concern with this is that the UNNotificationServiceExtension is only given a short period of time during which it must complete all of its work, after which it will time out.
So, my question is, "Is this the right way to clean up unused files from a Notification Service Extension, or is there a better way?"
Thanks to manishsharma93, I was able to implement a good solution. I am now storing the files in a directory shared by the main app and the notification service extension. I first had to set up a shared App Group using the information found here: https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html#//apple_ref/doc/uid/TP40011195-CH4-SW19
Then in my AppDelegate, I added this private function, which I call at the end of the applicationDidFinishLaunching(_:) method:
// I call this at the end of the AppDelegate.applicationDidFinishLaunching(_:) method
private func clearNotificationMedia() {
// Check to see if there are any delivered notifications. If there are, don't delete the media yet,
// because the notifications may be using them. If you wanted to be more fine-grained here,
// you could individually check to see which files the notifications are using, and delete everything else.
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
guard notifications.isEmpty else { return }
let fileManager = FileManager.default
guard let mediaCacheUrl = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourGroupHere")?.appendingPathComponent("media_cache", isDirectory: true) else { return }
// Check to see if the directory exists. If it doesn't, we have nothing to do here.
var isDirectory: ObjCBool = false
let directoryExists = FileManager.default.fileExists(atPath: mediaCacheUrl.path, isDirectory: &isDirectory)
guard directoryExists && isDirectory.boolValue else {
print("No media_cache directory to delete.", terminator: "\n")
return
}
// The directory exists and there aren't any notifications using media stored there,
// so go ahead and delete it. Use a lock to make sure that there isn't data corruption,
// since the directory is shared.
let lock = NSLock()
lock.lock()
do {
try FileManager.default.removeItem(at: mediaCacheUrl)
DebugLog("Successfully deleted media_cache directory.")
} catch let error as NSError {
DebugLog("Error: \(error.localizedDescription). Failed to delete media_cache directory.")
}
lock.unlock()
}
}
It works like a charm. Thanks again for pointing me in the right direction manishsharma93.

NSFileProvider importDocument provides fileURL of empty file when saving new document from MSOffice apps

I'm trying to create a new document in Word.app and save to my app via FileProvider extension. My implementation of appropriate method is:
override func importDocument(at fileURL: URL,
toParentItemIdentifier parentItemIdentifier: NSFileProviderItemIdentifier,
completionHandler: #escaping (NSFileProviderItem?, Error?) -> Void)
{
let internalUrl = NSFileProviderManager.default.documentStorageURL.appendingPathComponent(fileURL.lastPathComponent, isDirectory: false)
guard fileURL.startAccessingSecurityScopedResource() else { fatalError() }
try! FileManager.default.copyItem(at: fileURL, to: internalUrl) // breakpoint here
fileURL.stopAccessingSecurityScopedResource()
// update local db, whatever
completionHandler(TemporaryItem(forImporting: internalUrl, into: parentItemIdentifier), nil)
}
Apparently, when I'm putting the breakpoint and inspecting file attributes via po FileManager.default.attributesOfItem(forPath: fileURL.path) command, value for NSFileSize is 0.
Command po FileManager.default.contents(atPath: fileURL.path) returns me 0 byte Data with 0x000000000000bad0 pointer.
The file being written to internalUrl is empty as well.
The strangest thing is that this situation happens only with MS Word, Excel and PowerPoint apps. Same code for files saved from PSPDFKit, Files or Photos works perfectly. On the other side, Word correctly saves files to other file providers like Dropbox, so the problem should not be there.
I've tried to do that with file coordinator, but that didn't help. I've verified that every startAccessingSecurityScopedResource() has it's stopAccessingSecurityScopedResource(). I've tested on two iOS11.3 devices - same bahavior. I've even found other open source application which does same operations.
What am I doing wrong except expecting iOS app extension to work?
Because the the Word app will trigger several times of importDocument...
At first importDocument calling, it tries to create empty file on file provider extension. That's why the size of imported file is 0.
If you handle it well, the Word app will get the saved file path and update the file on it.
And then it will trigger next itemChangedAtURL: api with the file path it just got.

iOS: flush all output files

Is there an iOS method to flush all open output files-- so that when the call returns, all pending buffered data is written to persistent storage? I mean this in general terms-- where I don't have access to the specific file handles. I've tried sync() from Swift (a POSIX call) but that appears not guaranteed to actually make sure the data hits persistent storage before it returns (see http://pubs.opengroup.org/onlinepubs/009696899/functions/sync.html).
Not sure if this solves the problem in general, but it's addressing my need. Here's what I'm doing:
// Write to file (in my case, this is a log file where I don't have access to the file handle).
// I'm using logging to a file with SwiftyBeaver. https://github.com/SwiftyBeaver/SwiftyBeaver
DispatchQueue.main.async {
// Read from the file-- it has the flushed contents. I'm doing:
do {
let fileContents = try String(contentsOfFile: path)
} catch (let error) {
}
}
It looks like files are flushed at the end of the event loop.

Cannot access/import contents of iCloud file picked in iOS 11 document browser

Besides opening and saving its own custom documents, my iOS 11 document browser-based app should be able to import an appropriately formatted text file and convert it into a document of its own.
However, when picking a text file in the document browser, the app is able to access its contents (and then perform the conversion) only if the selected file is in local storage, but fails to access the contents when the file is stored in iCloud.
A simplified, commented version of the code I am using for the documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) delegate method is provided below:
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) {
guard let sourceURL = documentURLs.first else { return }
// Check extension to verify if it is a custom document
if sourceURL.pathExtension == "mydoc" {
// If it is a custom document, we present it directly
// (the presentDocument method is a standard implementation from Apple docs)
presentDocument(at: sourceURL)
} else {
// Otherwise, we suppose the file is a text file to be imported
// We first create a new document, then try to import the contents of the text file
let url = FileManager().temporaryDirectory.appendingPathComponent("New Document").appendingPathExtension("mydoc")
let doc = MyDocument(fileURL: url)
do {
let textFileContents = try String(contentsOf: sourceURL)
// It works fine if the text file is in local storage
// Here the document model is updated by converting the contents of the text file
// (code omitted, it is actually a method in the MyDocument class)
// ...
} catch {
// Produce error message: cannot access text file
// The error message is always produced if the text file is in iCloud storage!
}
// Save and close the document with standard code, e.g.:
doc.save(to: url, for: .forCreating) { (saveSuccess) in
guard saveSuccess else { return }
doc.close(completionHandler: { (closeSuccess) in
guard closeSuccess else { return }
})
// Reveal and import the document
self.revealDocument(at: url, importIfNeeded: true) { (revealedDocumentURL, error) in
if let error = error {
// Handle the error appropriately
return
}
// Present the Document View Controller for the revealed URL
self.presentDocument(at: revealedDocumentURL!)
}
}
}
The info.plist file declares both the custom document and public.text as Document Types (with the appropriate Role and Handler rank), and the custom document as Exported UTI. If the file picked is a custom document, everything works fine, no matter if the file is local or in iCloud.
As the importing works when the file is in local storage, I thought it may be an issue of iCloud permissions, even if Apple docs for UIDocumentBrowserViewController state:
The system automatically handles access to iCloud for you; you don't
need to enable your app’s iCloud capabilities.
However, trying to add iCloud capabilities to my app's entitlements did not solve the problem (and actually made it worse, in the sense that the exact same code was now saving newly created documents in the default iCloud container for the app, which is not the same iCloud Drive location used by the document browser, so that all documents became "invisible" to the browser - but I digress...).
Another "fun" element is that I also implemented a UIDocumentPickerViewController in the app to import additional content from a text file into an already open custom document. The code I use is basically the same as above... and it works perfectly, independently of whether the text file is in local storage or iCloud! This could reinforce the view that the problem is linked to a specific permission issue with UIDocumentBrowserViewController.
So, in summary: what could I do to access the content of text files stored in iCloud and convert them to custom documents of my app? Thank you in advance for any suggestions (and please be gentle if there are issues with the formulation of the question - this is my very first stackoverflow question after months of lurking!).
I’m not 100% sure but I believe you need to call startAccessingSecurityScopedResource on that URL to consume the sandbox extension vended to you by the document browser. Don’t forget to call stopAccessing once you are done in order not to leak kernel resources. Also, if you are not really editing that document, you might be looking for a UIDocumentPickerViewController instead? It’s weird for the user to open a file and then end up editing another. Isn’t it a better UX to start a new document, and then import the text file into it with the picker?
small drop-in snippet: (Swift 5.x)
public func documentBrowser(_ controller: UIDocumentBrowserViewController,
didPickDocumentsAt documentURLs: [URL]) {
dismiss(animated: true, completion: nil)
self.importAll(urls: documentURLs)
}
final func importAll(urls: [URL]) {
//TODO: use timer/loop/async stuff..
// for multiple files..
urls.forEach { (url: URL) in
let secured = url.startAccessingSecurityScopedResource()
= myImportInSessionFrom(url: url)
if secured { url.stopAccessingSecurityScopedResource() }
}
}

Resources