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()
Related
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...
I have a NSManagedObject called Event that is shared between the host app and today extension. (In Target Membership, both the main app and the widget are checked).
The host app and widget have the same App Group identifier and both share Data Model(In Target Membership, both the main app and the widget are checked).
When I launch(run) the widget in Xcode, it shows all of the app events (Event) that are already saved in the host app. However, when I add a new event, it appears in the host app but NOT in today-widget. If I relaunch the widget, all the events are shown including the last event that previously was not.
This is the method that fetches events. It is defined in TodayViewController of the widget.
private func fetchEvents(date: Date) {
let predicates = NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "date = %#",Date().startOfDay as CVarArg),
NSPredicate(format: "startTime >= %#", Date() as CVarArg)
])
if let ev = try? TPEvent.fetchAll(predicates: predicates, in: persistentManager.context) {
events = ev
}
}
This event is called in viewWillAppear and widgetPerformUpdate.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchEvents(date: Date())
self.tableView.reloadData()
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
self.fetchEvents(date: Date() )
self.tableView.reloadData()
completionHandler(NCUpdateResult.newData)
}
persistentManaged.context is PersistentManager.shared.context (see code below).
By the way, both of the methods above are called when I view today-widget. I have a lot of time figuring out this issue but could not do so.
What could be the issue and how to fix it?
Please just comment should you need more info or have any question.
Update
I have a singleton PersistentManager. Use viewContext both in the host app and widget.
public final class PersistentManager {
init() {}
public static let shared = PersistentManager()
public lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "Event")
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.event.data") else {
fatalError("Shared file container could not be created.")
}
let storeURL = fileContainer.appendingPathComponent("Event.sqlite")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
}
return container
}()
public lazy var context = persistentContainer.viewContext
// MARK: - Core Data Saving support
public func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
The issue is that the main app and the app extension work as two different processes on iOS.
CoreData works with NotificationCenter which sends notifications only within the main app process. Thus, you have to send interprocess notification here.
One hidden way to send interprocess notification on iOS is to use KVO on the UserDefaults object.
In NSUserDefaults.h header file Apple states that
/*!
NSUserDefaultsDidChangeNotification is posted whenever any user defaults changed within the current process, but is not posted when ubiquitous defaults change, or when an outside process changes defaults. Using key-value observing to register observers for the specific keys of interest will inform you of all updates, regardless of where they're from.
*/
Having this specified, one can assume that by using KVO on the particular key of UserDefaults, the value change will be propagated from the app to the extension, and vice versa.
So, the approach can be that on each change in the main app you save the current timestamp of the change into the UserDefaults:
/// When the change is made in the main app:
let defaults = UserDefaults(suiteName: "group.<your bundle id>")
defaults["LastChangeTimestamp"] = Date()
defaults.synchronize()
In the app extension:
let defaults = UserDefaults(suiteName: "group.<your bundle id>")
func subscribeForChangesObservation() {
defaults?.addObserver(self, forKeyPath: "LastChangeTimestamp", options: [.new, .initial], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// Process your changes here.
}
deinit {
defaults?.removeObserver(self, forKeyPath: "LastChangeTimestamp")
}
I'm working on an application which uses a shared Core Data database between itself and a Notification Service Extension. Both the application and the extension are able to read and write to the same Core Data database.
The application however needs to update the displayed information as soon as the corresponding fields change in the database. Is there an efficient way for it to be notified of the changes the extension makes to the database? I assume the application and the extension use different managed contexts to access the database. Or am I wrong?
Using SwiftEventBus this is pretty straight forward
Controller.swift
let yourObj = YourObject()
SwiftEventBus.post("EventName", sender: yourObj)
Extension.swift
let yourObj = YourObject()
SwiftEventBus.post("EventName", sender: yourObj)
AppDelegate.swift
SwiftEventBus.onMainThread(self, name: "EventName") { (result) in
if let yourObject = result.object as? YourObject {
// Queue or write the data as per your need
}
}
I found a solution to the problem I described after being pointed towards using notification by #CerlinBoss. It is possible to send a notification from the extension to the application (or vice versa). This can be done in iOS using a Darwin notification center. The limitation however is that you can't use the notification to send custom data to your application.
After reading many articles I decided that I'd avoid making changes to the Core Data database from two different processes and using multiple managed contexts. Instead, I queue the data I need to communicate to the application inside a key in the UserDefaults and once the application is notified of the changes, I'd dequeue them and update the Core Data context.
Common Code
Swift 4.1
import os
import Foundation
open class UserDefaultsManager {
// MARK: - Properties
static let applicationGroupName = "group.com.organization.Application"
// MARK: - Alert Queue Functions
public static func queue(notification: [AnyHashable : Any]) {
guard let userDefaults = UserDefaults(suiteName: applicationGroupName) else {
return
}
// Retrieve the already queued notifications.
var alerts = [[AnyHashable : Any]]()
if let data = userDefaults.data(forKey: "Notifications"),
let items = NSKeyedUnarchiver.unarchiveObject(with: data) as? [[AnyHashable : Any]] {
alerts.append(contentsOf: items)
}
// Add the new notification to the queue.
alerts.append(notification)
// Re-archive the new queue.
let data = NSKeyedArchiver.archivedData(withRootObject: alerts)
userDefaults.set(data, forKey: "Notifications")
}
public static func dequeue() -> [[AnyHashable : Any]] {
var notifications = [[AnyHashable : Any]]()
// Retrieve the queued notifications.
if let userDefaults = UserDefaults(suiteName: applicationGroupName),
let data = userDefaults.data(forKey: "Notifications"),
let items = NSKeyedUnarchiver.unarchiveObject(with: data) as? [[AnyHashable : Any]] {
notifications.append(contentsOf: items)
// Remove the dequeued notifications from the archive.
userDefaults.removeObject(forKey: "Notifications")
}
return notifications
}
}
Extension:
Swift 4.1
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: #escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
os_log("New notification received! [%{public}#]", bestAttemptContent.body)
// Modify the notification content here...
// Queue the notification and notify the application to process it
UserDefaultsManager.queue(notification: bestAttemptContent.userInfo)
notifyApplication()
contentHandler(bestAttemptContent)
}
}
func notifyApplication() {
let name: CFNotificationName = CFNotificationName.init("mutableNotificationReceived" as CFString)
if let center = CFNotificationCenterGetDarwinNotifyCenter() {
CFNotificationCenterPostNotification(center, name, nil, nil, true)
os_log("Application notified!")
}
}
Application:
Swift 4.1
// Subscribe to the mutableNotificationReceived notifications from the extension.
if let center = CFNotificationCenterGetDarwinNotifyCenter() {
let name = "mutableNotificationReceived" as CFString
let suspensionBehavior = CFNotificationSuspensionBehavior.deliverImmediately
CFNotificationCenterAddObserver(center, nil, mutableNotificationReceivedCallback, name, nil, suspensionBehavior)
}
let mutableNotificationReceivedCallback: CFNotificationCallback = { center, observer, name, object, userInfo in
let notifications = UserDefaultsManager.dequeue()
for notification in notifications {
// Update your Core Data contexts from here...
}
print("Processed \(notifications.count) dequeued notifications.")
}
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()
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!