Access to downloaded video files in Swift, iOS - ios

I am trying to make an app that allows users to download videos to play offline, like downloading some videos for offline viewing in Netflix.
I am using Firebase as a server for the videos. Here comes the action for a download button:
#IBAction func btnDL(_ sender: Any) {
self.dlBtnOutlet.isEnabled = false
//Firebase
let storage = Storage.storage()
let videoRef = storage.reference(forURL: "gs://videowcoredata.appspot.com/Some video file.mp4")
//Local file system
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localURL = documentsURL.appendingPathComponent("movie.mp4")
// Download to the local filesystem
let downloadTask = videoRef.write(toFile: localURL) { (URL, error) -> Void in
if (error != nil) {
print("Uh-oh, an error occurred!")
print(error)
} else {
print("Local file URL is returned")
self.genericURL = String(localURL.path)
print(localURL.path)
//Save the link to CoreData
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let myUrl = MyFiles(context: context)
myUrl.fileURL = self.genericURL
(UIApplication.shared.delegate as! AppDelegate).saveContext()
I am using CoreData just for the persistence of the downloaded file link. Saving the links to an array, and returning the last member of the array as the downloaded link. I am trying to play the file in a new AVPlayerViewController, with the help of prepare for segue.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Fetch CoreData
let destination = segue.destination as! AVPlayerViewController
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
urlList = try context.fetch(MyFiles.fetchRequest())
}
catch {
print("Oops we have an error!)")
}
self.genericURL = (urlList.last?.fileURL)!
destination.player = AVPlayer(url: URL(fileURLWithPath: self.genericURL))
}
I define two variables in the beginning of the code to access later:
var urlList : [MyFiles] = []
var genericURL = ""
So, my questions are:
I can manage to download the file, locate the downloaded file and save its path to CoreData for persistence. I can play the local video from the local file with the code above, but when I restart the simulator I no longer have access to the file. I need to download again, which is the thing I am trying to avoid. What can be the reason for this?
I wish to do this without using CoreData, since it is about File handling. But I don't have adequate resources to learn Filemanager. I simply want to protect my videos (I don't want that user syncs them) and I want my users to have offline access if they wish, but I don't want to include the videos in the Bundle. What would be the best way for this?
Thanks in advance for help!

Related

Core Data+CloudKit not connecting to CloudKit

I am updating a pre-iOS13 Core Data app to use Core Data+CloudKit syncing to support single users on multiple devices. The syncing is supposed to occur automagically, and in an interim step in my development it did work. Now it's not working, with CloudKit telemetry not registering any activity, and I can't figure out why it's not working.
In my previous app versions I provided a small number of label strings in UserDefaults and allowed users to modify them, with the updated versions put back into UserDefaults. It was a shortcut to avoid having to manage a second Core Data entity for what would only ever be a small number of objects.
I have since realized this won't work in a multi-device implementation because the fact that the local Core Data database is empty no longer means it's the first use for that user. Instead, each new device needs to look to the cloud-based data source to find the user's in-use label strings, and only use app-provided default values if the user doesn't already have something else.
I followed Apple's instructions for Setting Up Core Data with CloudKit, and at first syncing worked fine. But then I realized the syncing behavior wasn't correct, and that instead of pre-populating from strings stored in UserDefaults I really needed to provide a pre-populated Core Data database (.sqlite files). I implemented that and the app now works fine locally after copying the bundled .sqlite files at first local launch.
But for some reason this change caused the CloudKit syncing to stop working. Now, not only do I not get any automagical updates on other devices, I get no results in the CloudKit dashboard telemetry so it appears that CloudKit synching never gets started. This is odd because locally I am getting notifications of 'remote' changes that just occurred locally (the function I list as a #selector for the notification is being called locally when I save new data locally).
I'm stumped as to what the problem/solution is. Here is my relevant code.
//In my CoreDataHelper class
lazy var context = persistentContainer.viewContext
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let appName = Bundle.main.infoDictionary!["CFBundleName"] as! String
let container = NSPersistentCloudKitContainer(name: appName)
//Pre-load my default Core Data data (Category names) on first launch
let storeUrl = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!.appendingPathComponent(appName + ".sqlite")
let storeUrlFolder = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
let seededDataUrl = Bundle.main.url(forResource: appName, withExtension: "sqlite")
let seededDataUrl2 = Bundle.main.url(forResource: appName, withExtension: "sqlite-shm")
let seededDataUrl3 = Bundle.main.url(forResource: appName, withExtension: "sqlite-wal")
try! FileManager.default.copyItem(at: seededDataUrl!, to: storeUrl)
try! FileManager.default.copyItem(at: seededDataUrl2!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-shm"))
try! FileManager.default.copyItem(at: seededDataUrl3!, to: storeUrlFolder.appendingPathComponent(appName + ".sqlite-wal"))
}
let storeDescription = NSPersistentStoreDescription(url: storeUrl)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
//In the view controllers, we'll listen for relevant remote changes
let remoteChangeKey = "NSPersistentStoreRemoteChangeNotificationOptionKey"
storeDescription.setOption(true as NSNumber, forKey: remoteChangeKey)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
//This is returning nil but I don't think it should
print(storeDescription.cloudKitContainerOptions?.containerIdentifier)
return container
}()
//In my view controller
let context = CoreDataHelper.shared.context
override func viewDidLoad() {
super.viewDidLoad()
//Do other setup stuff, removed for clarity
//enable CloudKit syncing
context.automaticallyMergesChangesFromParent = true
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(reportCKchange),
name: NSNotification.Name(
rawValue: "NSPersistentStoreRemoteChangeNotification"),
object: CoreDataHelper.shared.persistentContainer.persistentStoreCoordinator
)
updateUI()
}
#objc
fileprivate func reportCKchange() {
print("Change reported from CK")
tableView.reloadData()
}
Note: I have updated the target to be iOS13+.
I think a newly created NSPersistentStoreDescription has no cloudKitContainerOptions by default.
To set them, try:
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: <<Your CloudKit ID>>)
I discovered as well that ALL attributes of your entities must be optional, or have a default value, otherwise it just won't work. Be nice if this was clearer in the documentation...

Sharing data between an notification service extension and main app

In my scenario, I'm sharing data between the parent iOS app and notification service extension. I'm using Xcode 10.2.1, iOS Deployment Target 10.0
We have tried the NSUserDefaults and Keychain group it's working as expected. Is any other way to save the values(Store Model or datatypes) from notification service extension(TargetB) to MainApp(TargetA).
We have appended the values into the model once the app is in terminated state and save it in the keychain.
For saving to Keycahin:
NotificationAPI.shared.NotificationInstance.append(Notifications(title: messageTitle, message: messageBody,date: Date()))
let notification = NotificationAPI.shared.NotificationInstance
let value = keychain.set(try! PropertyListEncoder().encode(notification), forKey: "Notification")
For USerDefault :
var defaults = NSUserDefaults(suiteName: "group.yourappgroup.example")
I want to transfer the data from Target B to Target A. when the app is in an inactive state? Another to transfer or saving data? Please help me?
Maybe I didn't get the question correctly.
You can share data between the app through CoreData:
Here it is explained.
In general, you need to add the extension and the main app in a group and to add your xcdatamodeled file in the target of the extension.
I think UserDefaults, Keychain, CoreData, iCloud, and storing data to your backend from where the other app take it is all the options that you have.
Add below property in your VC.
var typeArray = [DataModelType]()
let path = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.ABC.app")?.appendingPathComponent("type.plist")
DataModel which you want to share
struct DataModelType: Codable {
//define properties which you wanna use.
}
Add Extention to ViewController
extension ViewController {
fileprivate func saveData() {
let encoder = PropertyListEncoder()
do{
let dataEncode = try encoder.encode(typeArray)
try dataEncode.write(to:path!)
}
catch{
print("Error")
}
}
fileprivate func loadData() {
if let data = try? Data(contentsOf: path!) {
let decoder = PropertyListDecoder()
do {
typeArray = try decoder.decode([DataModelType].self, from: data)
}
catch {
print("Error")
}
}
}
func fetchDataModel() -> DataModelType {
loadData()
return typeArray
}
}
In your extension add below code wherever you wanna access data.
let viewController = ViewController()
let currentDataModel = viewController.fetchDataModel()

UIDocument Creation on iOS 11: reader is not permitted to access the URL

Since iOS 11 I have encountered the following error every time I am creating a new document using UIDocument API:
[ERROR] Could not get attribute values for item /var/mobile/Containers/Data/Application/XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX/Documents/myDoc-XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX.myFile (n).
Error: Error Domain=NSFileProviderInternalErrorDomain Code=1
"The reader is not permitted to access the URL."
UserInfo={NSLocalizedDescription=The reader is not permitted to access the URL.}
Unlike similar questions (1, 2, 3) on SO on this, I am not using UIDocumentBrowserViewController. I am simply creating a UIDocument and call save() to the Documents directory myself. The closest question I found uses UIManagedDocument. However, in my case, the file is still created and written successfully despite the error message.
Here's the gist of my save routine:
#IBAction func createDoc(_ sender: Any) {
let uuid = UUID().uuidString
let doc = Document(baseName: "myDoc-\(uuid)")
doc.save(to: doc.fileURL, for: .forCreating) { (completed) in
if (completed) {
doc.close(completionHandler: nil)
self.verifyNumberOfFiles()
}
}
}
My UIDocument subclass is also almost blank for simplicity of this question:
class Document: UIDocument {
let fileExtension = "myFile"
override init(fileURL url: URL) {
super.init(fileURL: url)
}
/// Convenience method for `init(fileURL:)`
convenience init(baseName: String) {
self.init(fileURL: documentsDirectory.appendingPathComponent(baseName).appendingPathExtension(Document.fileExtension))
}
override func contents(forType typeName: String) throws -> Any {
return NSData()
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
}
}
I'm always writing to Documents folder, and my lookup routine can verify that my files are successfully created:
public var documentsDirectory: URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
}
func loadFileURLs(from dirURL: URL) -> [URL]? {
return try? FileManager().contentsOfDirectory(at: dirURL, includingPropertiesForKeys: nil)
}
What I have discovered so far:
The error appears even when I set up my UTI already. See this and this.
I can verify that my UTI works when I send a "myFile" to my device over AirDrop and it correctly triggers my app to open.
The error appears on iOS 11 only. The same code doesn't reproduce the error on iOS 10, like in the question above.
I tried adding UISupportsDocumentBrowser key although I'm not using the browser but it's not dissolve the error.
What is happening? Is this just a "noise" error message on iOS 11?
Here's 🔨 my GitHub code online if anyone is interested.
A workaround for this is to create the file by saving its data to the disk, and then open it as you would with an existing file.
Your createDoc(_:) method would then like this:
#IBAction func createDoc(_ sender: Any) {
let uuid = UUID().uuidString
let baseName = "myDoc-\(uuid)"
let url = documentsDirectory
.appendingPathComponent(baseName)
.appendingPathExtension(Document.fileExtension)
do {
let emptyFileData = Data()
try emptyFileData.write(to: url)
let document = Document(fileURL: url)
document.open() { completed in
guard completed else {
// handle error
return
}
doc.close(completionHandler: nil)
self.verifyNumberOfFiles()
}
} catch {
// handle error
}
}
In Xcode 9.3 it is possible to specify a new item in info.plist:
Supports Document Browser (YES)
This enables access to the application's documents directory (for example, /var/mobile/Containers/Data/Application/3C21358B-9E7F-4AA8-85A6-A8B901E028F5/Documents on a device). Apple Developer doc here.

Swift 3 Photo Share Extension with Firebase Database Not Working After First Send

I am developing an iOS photo sharing extension in Swift 3 that captures a user-selected photo in the iOS Photos app along with a user-entered caption and stores it in Firebase. The photo is stored in Firebase storage. The caption and the path of the photo in Firebase Storage are stored in Firebase Realtime Database.
The problem I'm encountering is that the share extension stops working after first send. The strange thing is, if I do a similar approach in a regular View Controller in the iOS app, the code works. I noticed two issues with the Share Extension:
Issue #1: The share extension view isn't dismissed completely. In a normal situation, the view would return to a "gallery" mode of Photos. However, the share extension view is going away but the share menu with the complete list of apps that you can use to share is not dismissing.
Screenshot of what the Photos view looks like after the second send
Issue #2: the data isn't being sent up to Firebase storage or Firebase database.
Below please find my ShareViewController code:
import UIKit
import Social
import Firebase
import MobileCoreServices
class ShareViewController: SLComposeServiceViewController {
var ref: DatabaseReference!
var storageRef: StorageReference!
override func isContentValid() -> Bool {
// Do validation of contentText and/or NSExtensionContext attachments here
return true
}
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
FirebaseApp.configure()
ref = Database.database().reference()
storageRef = Storage.storage().reference()
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
if let item = self.extensionContext?.inputItems[0] as? NSExtensionItem {
for ele in item.attachments!{
let itemProvider = ele as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier("public.jpeg"){
itemProvider.loadItem(forTypeIdentifier: "public.jpeg", options: nil, completionHandler: { (item, error) in
do {
var imgData: Data!
if let url = item as? URL{
imgData = try Data(contentsOf: url)
}
if let img = item as? UIImage{
imgData = UIImagePNGRepresentation(img)
}
var updateRef = self.ref.child("demo_group").child("demo_patient").child("demo_updates").childByAutoId()
var updateStorageRef = self.storageRef.child("demo_photos" + "/\(Double(Date.timeIntervalSinceReferenceDate * 1000)).jpg")
updateRef.child("event_name").setValue(self.contentText)
updateRef.child("sender").setValue("demo_ff")
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
updateStorageRef.putData(imgData, metadata: metadata) { (metadata, error) in
if let error = error {
print("Error uploading: \(error)")
return
}
// use sendMessage to add imageURL to database
updateRef.child("photos").childByAutoId().setValue(metadata?.path)
}
} catch let err{
print(err)
}
})
}
}
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return []
}
}
Please let me know if there's anything I can do to fix this! I would be greatly appreciated.
FirebaseApp.configure() will crash your extension if call multiple times in a row because the system does not fully deallocate the firebase app instance created for the extension.
My solution was to do:
if let _ = FirebaseApp.app() {...} else {
FirebaseApp.configure()
}
This should fix your problem.
Edit: Also note that you should call extensionContext.complete after your storage & database updates have completed... Not before
So, you will need to use the methods for setValue that take a completion block.
1- Upload to storage, when the task completes
2- Update the database reference,
3- When that completion block is called... Then you call extensionContext.completeWith() or .cancel()

iOS Action Extension share from GarageBand

I am building an action extension for GarageBand on iOS which transforms and uploads audio but no matter what I try, I just could not get to the exported file.
Let’s consider the following code — it should:
find and load shared audio from extensionContext
initialise audio player
play the sound
It works if I run the extension in Voice Memos.app — the url to the file looks like this: file:///tmp/com.apple.VoiceMemos/New%20Recording%202.m4a
Now, If I run the code in GarageBand.app, the url points to (what I presume) is GarageBand’s app container, as the url looks something like /var/…/Containers/…/Project.band/audio/Project.m4a, and the audio will not be loaded and cannot therefore be manipulated in any way.
// edit: If I try to load contents of the audio file, it looks like the data only contains aac header (?) but the rest of the file is empty
What is interesting is this: The extension renders a react-native view and if I pass the view the fileUrl (/var/…Project.band/audio/Project.m4a) and then pass it down to XMLHTTPRequest, the file gets uploaded. So it looks like the file can be accessed in some way?
I’m new to Swift/iOS development so this is kind of frustrating for me, I feel like I tried just about everything.
The code:
override func viewDidLoad() {
super.viewDidLoad()
var audioFound :Bool = false
for inputItem: Any in self.extensionContext!.inputItems {
let extensionItem = inputItem as! NSExtensionItem
for attachment: Any in extensionItem.attachments! {
print("attachment = \(attachment)")
let itemProvider = attachment as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeAudio as String) {
itemProvider.loadItem(forTypeIdentifier: kUTTypeAudio as String,
options: nil, completionHandler: { (audioURL, error) in
OperationQueue.main.addOperation {
if let audioURL = audioURL as? NSURL {
print("audioUrl = \(audioURL)")
// in our sample code we just present and play the audio in our app extension
let theAVPlayer :AVPlayer = AVPlayer(url: audioURL as URL)
let theAVPlayerViewController :AVPlayerViewController = AVPlayerViewController()
theAVPlayerViewController.player = theAVPlayer
self.present(theAVPlayerViewController, animated: true) {
theAVPlayerViewController.player!.play()
}
}
}
})
audioFound = true
break
}
}
if (audioFound) {
break
}
}
}

Resources