My app uses remote notifications with a NotificationService Extension in which I edit the notification before displaying it.
I would like to let the user upload a custom sound file which should be played instead of the default sound. For this I use an shared AppGroup, which the app and the extension have access to.
The uploaded sound files are stored in the "Library/Sounds" directory as follows (my code for testing, without much error handling):
.....
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.xxx.xxx")
let soundsURL = containerURL!.appendingPathComponent("Library/Sounds/", isDirectory: true)
if !FileManager.default.fileExists(atPath: soundsURL.path) {
try! FileManager.default.createDirectory(atPath: soundsURL.path, withIntermediateDirectories: true)
}
if FileManager.default.fileExists(atPath: soundsURL.path) {
do {
try FileManager.default.copyItem(at: sourceURL, to: soundsURL.appendingPathComponent(sourceURL.lastPathComponent))
} catch {
// Exception
}
}
In the Notification Extension I change the sound of the notification to the name of the uploaded file:
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "test.wav"))
This is working fine as long as the iPhone is not locked. But if the iPhone is locked, there is no vibration and no sound is played (also no default sound). But I don't know why - according to apples documentation UNNotificationSound looks in "Library/Sounds" of the app shared group container directories. If I store the file directly in the main bundle, it works.
Does anyone have any idea what could be causing this?
Ok, now i figured out what the problem is.
My files were created with "NSFileProtectionComplete" data protection by default.
"NSFileProtectionComplete — The file is only accessible while the device is unlocked."
After changing the data protection of my sound files to "NSFileProtectionNone", it finally works!
Related
I have an iOS app and I need to be able to pick a file on my iCloud Drive, modify the file, and save the modified file with a new extension. I've tried lots of things but I still can't write the new file. Here is my latest code:
documentPickerViewController = DocumentPickerViewController(documentTypes: ["public.item"], in: .open)
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let fileURL = urls[0]
do {
// Read the file
_ = fileURL.startAccessingSecurityScopedResource()
let fileData = try Data(contentsOf: fileURL, options: .uncached)
fileURL.stopAccessingSecurityScopedResource()
// write the file
let fileCopyURL = fileURL.appendingPathExtension("copy")
_ = fileCopyURL.startAccessingSecurityScopedResource()
try fileData.write(to: fileCopyURL)
fileCopyURL.stopAccessingSecurityScopedResource()
}
catch {
print(error.localizedDescription)
}
}
When I pick a file on my iCloud Drive I get the following error:
You don’t have permission to save the file “TestFile.txt.copy” in the folder “Test Files”.
How can a save the modified file?
First, I think you should make sure that you have the correct entitlements/permissions to write to iCloud Drive:
If this still doesn't work, try updating the CFBundleVersion or Build Number of your app and make sure that your Bundle Identifier (in the first section of Signing & Capabilities) is correctly registered on your Apple Developer account.
According to your code, what you're trying to accomplish is feasible but the issue comes from elsewhere.
Let me know if you have an issue following these steps. Good Luck!
I would like to share a file (gpx) via the Gmail client app on the iPhone.
The problem is that the created mail does not contain the "shared" .gpx file.
Code for creating the share request:
let itemProver = GPXItemProvider(itemInformation) // Subclass of UIActivityItemProvider
let shareController = UIActivityViewController(activityItems:[itemProver], applicationActivities: nil)
present(controller: shareController)
The code itself does work nevertheless because if I try to export the gpx file into the default mail client on the device, everything works fine.
Do I miss something which requires the GMail app to contain the file as the attachement?
This topic covers a similar problem. A third-party library is used there though, which is not an option in my case:
Send an iphone attachment through email programmatically
Thank you.
You can write the GPX file to a temporary directory and share the URL instead of your custom item.
let data = ...
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("test.gpx")
try data.write(to: url)
let shareController = UIActivityViewController(activityItems:[url], applicationActivities: nil)
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.
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.
I'm saving a video on my iPad with this code on swift:
let paths = NSSearchPathForDirectoriesInDomains(
.DocumentDirectory, .UserDomainMask, true)
let documentsDirectory = paths[0] as String
var filePath:String? = nil
var fileNamePostfix = 0
do {
filePath =
"\(documentsDirectory)/\(dateTimePrefix)-\(fileNamePostfix++).mp4"
} while (NSFileManager.defaultManager().fileExistsAtPath(filePath))
let fileUrl = NSURL(fileURLWithPath: filePath)
self.fileOutput.startRecordingToOutputFileURL( fileUrl , recordingDelegate: delegate)
But I can't see if my video is saving because I can't open path var/mobile/media...
There are any form to save pictures on photos folder?
Thanks!!
You can't save assets to the Media Library — the stuff that appears in the Music, Videos, and Podcasts apps. The only way to get things into there is by syncing from iTunes on the desktop or downloading from the iTunes Store.
If you want to save a video so that it appears in the Photos app, use the Photos framework in iOS 8:
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(url)
}, completionHandler: { success, error in
if !success { NSLog("Failed to create video: %#", error) }
})
(In iOS 7 and earlier, use the AssetsLibrary framework instead.)
If you just want to see if the files are getting into the documents folder on your device then you can see the container in the Devices window of Xcode 6.
Just Window > Devices then select your device. Then select your app, click the gear button and tell it if you want to view, download, or replace the container for the app.
Note that you can also save the videos into the Saved Photos album, assuming they are in the right format. Check out this method in the ALAssetsLibrary Class:
writeVideoAtPathToSavedPhotosAlbum:completionBlock:
You can't access the photo system folder directly. However if you just want to see the files you've saved, you don't have to. Just enable document sharing by adding UIFileSharingEnabled to your Info.plist.
When you plug your device in and open iTunes, it will show your documents folder like so:
More info on filesharing here.