How to delete files in iOS Notification Service Extension? - ios

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.

Related

Monitor file with some kind of persistent id

My app writes files to the users shared Documents directory. I save the files path but this breaks if the user changes the documents name, moves the document to a new location etc. Is there some kind of persistent file id i can use to keep track of these documents?
i've been poking around
FileManager.default.attributesOfItem(atPath: docUrl.path)
and i find attributes like FileAttributeKey.systemFileNumber but this number seems to change every time i run the app. There has to be some way to keep track of files other than their path
You need to use url bookmarks, like the following (sketch)
if let bookmark = try? URL(fileURLWithPath: docUrl.path)
.bookmarkData(includingResourceValuesForKeys: nil,
relativeTo:nil) {
// store somewhere persistent bookmark to your file
}
...
let bookmark = // read bookmark from persisten storage
var stale = false
if let url = try? URL(resolvingBookmarkData:bookmark, bookmarkDataIsStale: &stale) {
// work with resolved url
} else {
// file disappeared - handle the situation
}

Reading files from external storage in iOS 13

I have an iOS app that is trying to read files from an external storage device without importing them into the App's sandbox.
I have followed Apple's documentations outlined here to do this --
Providing Access to Directories
I'm able to retrieve the selected directory ( which is on an external storage device connected via the Lightning port ) and enumerate the files inside the directory.
However, when I try to do something with those files as per the recommended pattern, I get a failure and basically get permission errors on the file.
let shouldStopAccessing = pickedFolderURL.startAccessingSecurityScopedResource()
defer {
if shouldStopAccessing {
pickedFolderURL.stopAccessingSecurityScopedResource()
}
}
var coordinatedError:NSError?
NSFileCoordinator().coordinate(readingItemAt: pickedFolderURL, error: &coordinatedError) { (folderURL) in
let keys : [URLResourceKey] = [.isDirectoryKey]
let fileList = FileManager.default.enumerator(at: pickedFolderURL, includingPropertiesForKeys: keys)!
for case let file as URL in fileList {
if !file.hasDirectoryPath {
do {
// Start accessing a security-scoped resource.
guard file.startAccessingSecurityScopedResource() else {
// Handle the failure here.
//THIS ALWAYS FAILS!!
return
}
// Make sure you release the security-scoped resource when you are done.
defer { file.stopAccessingSecurityScopedResource() }
I should add that this works JUST FINE if the files are on iCloud Drive via Simulator. It fails both on external devices and iCloud Drive on a real device.
Here is a full working project that demonstrates the failure.
Running on simulator accesses iCloud Drive files just fine. But running on device fails.
Running on device to access USB drive fails.
So, this seems like a documentation issue with the link posted above. When the user selects a folder, all files and folders are recursively granted access and automatically security scoped. The line guard file.startAccessingSecurityScopedResource() always returns false.
The trick to getting this work is NOT to try to security scope individual files, but to ensure that this code snippet does not run BEFORE you access files.
defer {
if shouldStopAccessing {
pickedFolderURL.stopAccessingSecurityScopedResource()
}
}
As long as you continue to access files while the pickedFolderURL is inside security scope, you will be successful.
Hope this helps somebody.

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

Turning off iCloud and remove items from the ubiquitous container

I have a UISwitch in my app for users to switch on/off iCloud. I want to achieve the following: When a user turns off iCloud, all contents in the ubiquitous container will be removed and copied to a local directory as backups. However, as soon as the files have been removed from the ubiquitous container, copies on the iCloud server are also removed. This basically clear everything on iCloud.
I have the following questions:
How can files on the ubiquitous container be removed without affecting copies on the iCloud server?
What is the best or standard practice to remove files from the ubiquitous container before disabling iCloud?
Can iCloud be disabled at all after it has been initialised/enabled?
Thank you.
After reading Apple's documents and others suggestions, here is my understanding. I am not 100% sure if they are correct. Comments and corrections are most welcome:
Anything added to or removed from the ubiquity container will be synced with the iCloud server. The app has no control of this.
Once iCloud document storage has been enabled in Settings app by the user, it cannot be disabled by the app. The app's responsibility is to provide UI (assuming a UISwitch) to let user indicate where they want their documents synced with the iCloud for the app.
If the user turns off iCloud by turning off the UISwitch in the app (not in Settings), what the app should do is to stop querying metadata, stop listening to NSMetadataQueryDidUpdateNotification, and stop accessing files in the ubiquity container (as mentioned by crizzis above). If later the user turns iCloud on again, files already in the ubiquity container will be synced with iCloud automatically, and no manual merging should be needed unless unresolved conflicts occur.
Using evictUbiquitousItem(at url:)
See point #1. I'm not sure why you would want to do that, though. Can't you just stop accessing the local copies the second the switch is off?
I don't think it can be disabled programmatically. On a positive side, if you want to avail the users of a possibility to disable iCloud, it's already there. iCloud is supposed to be disabled via the Settings app, and all you really need to do is handle that fact within the app by listening to NSUbiquityIdentityDidChangeNotification
UPDATE
Amin Negm-Awad suggested that evictUbiquitousItem(at url:) forces a reload, and so the local copy is not permanently deleted. However, I've done a little testing just out of curiosity, and haven't found that to be the case. The following test:
func runTest(ubiURL: URL) {
self.query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
self.query.predicate = NSPredicate(format: "%K like '*'", NSMetadataItemFSNameKey)
NotificationCenter.default.addObserver(self, selector: #selector(self.metadataQueryDidUpdate(_:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: self.query)
self.query.start()
self.query.enableUpdates()
let fileURL = ubiURL.appendingPathComponent("Documents/file.txt")
FileManager.default.createFile(atPath: fileURL.path, contents: "Hello".data(using: .utf8))
do {
try FileManager.default.startDownloadingUbiquitousItem(at: fileURL)
} catch {
print("startDownloadingUbiquitousItem: \(error.localizedDescription)")
}
}
var updateCount = 0
func metadataQueryDidUpdate(_ notification: Notification) {
print("######################")
print("update #\(updateCount)")
for file in query.results as! [NSMetadataItem] {
guard let fileURL = file.value(forAttribute: NSMetadataItemURLKey) as? URL, let fileStatus = file.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String else {
print("Invalid item!")
return
}
if fileStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent {
print("URL to evict: \(fileURL)")
do {
try FileManager.default.evictUbiquitousItem(at: fileURL)
print("Eviction result: successful")
} catch {
print("evictUbiquitousItem: \(error.localizedDescription)")
}
}
print("File exists at URL: \(FileManager.default.fileExists(atPath: fileURL.path))")
}
updateCount = updateCount + 1
}
Yielded:
ubiURL is file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/
######################
update #0
URL to evict: file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/Documents/file.txt
evictUbiquitousItem: The file “file.txt” couldn’t be saved in the folder “blabla”.
File exists at URL: true
######################
...
update #3
URL to evict: file:///private/var/mobile/Library/Mobile%20Documents/iCloud~com~example~blabla3/Documents/file.txt
Eviction result: successful
File exists at URL: true
######################
update #4
File exists at URL: false
(no further updates followed)
I don't think the remote file will end up on the device unless startDownloadingUbiquitousItemAtUrl: is called with the evicted file's URL. Not sure if this behavior can be relied upon, though.

Download in documents directory or move file async - 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

Resources