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...
Related
Very rarely (<1% of users), a user will inform me that they open my app, and all their data is gone. The problem I'm having is that I am unable to replicate the issue because it happens so rarely. I don't even know how to begin fixing this problem. It seems like this does not happen while a user is using the app. They seem to just open it one day and the data is no longer there.
On one occasion, a user was able to recover the data by turning off and on the iCloud switch for my app (in the phone settings). However, this did not work for another user.
I was reading that iOS may delete the cache when the storage is full. However, to my understanding, the default location for the container is in Application Support.
Here is my Core Data implementation in App Delegate:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "Bench")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
context.perform {
do {
try context.save()
} catch {
let nserror = error as NSError
print(nserror.localizedDescription)
}
}
}
}
I access it globally like this:
let ad = UIApplication.shared.delegate as! AppDelegate
let context = ad.persistentContainer.viewContext
I have a CoreData setup with a Local store and a Cloud store, only the latter of which syncs with CloudKit. The sync itself works well.
I'm trying to listen for remote change notifications so that I can trigger some processes on device when a remote change from CloudKit comes in. I've set up the NSPersistentStoreRemoteChangeNotificationOptionKey key in my persistence controller, and a corresponding observer that listens for this.
The observer correctly triggers for remote change notifications, however my problem is that is also triggers for any CoreData changes that originate on-device once the view context is saved.
If this is expected behaviour, I was hoping there would at least be some useful information in the notification itself so I could tell apart whether the change was triggered by an on-device action or synced from CloudKit.
Is there something I'm doing wrong, or is there any other easy way to get a notification when a remote change comes in?
Persistence Controller
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "MyContainerName")
let storeDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
// Local
let localUrl = storeDirectory.appendingPathComponent("local.store")
let local = NSPersistentStoreDescription(url: localUrl)
local.configuration = "Local"
// Cloud
let cloudUrl = storeDirectory.appendingPathComponent("cloud.store")
let cloud = NSPersistentStoreDescription(url: cloudUrl)
cloud.configuration = "Cloud"
cloud.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "my.container.identifier")
cloud.setOption(true as NSNumber, forKey: "NSPersistentStoreRemoteChangeNotificationOptionKey")
container.persistentStoreDescriptions = [local, cloud]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
App Delegate
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: PersistenceController.shared.container.persistentStoreCoordinator
)
return true
}
#objc
func storeRemoteChange(_ notification: Notification) {
precondition(notification.name == NSNotification.Name.NSPersistentStoreRemoteChange)
print(notification.userInfo)
}
}
Output from print statement
Optional([AnyHashable("NSStoreUUID"): C2D121212-421B-003F-9819-956411111169, AnyHashable("storeURL"): file:///var/mobile/Containers/Data/Application/3234FH#-DACF-493I-AD24-143E4CDSFE2/Library/Application%20Support/cloud.store])
If this is expected behaviour, I was hoping there would at least be some useful information in the notification itself so I could tell apart whether the change was triggered by an on-device action or synced from CloudKit.
Yes this is expected behaviour, because whether local on device or change from the cloud this is recorded as change in your store/database.
So in order to process changes from the cloud you can subscribe for database changes, read here
EDIT *********
7-09-2020 1:39 PM PST
I've got what I believe will suffice as a minimal working reproducible version of the app available at:
https://github.com/Rattletrap99/penny-game-test
EDIT *********
I'm building a game in which users create coins as rewards for various achievements. The coins are saved as managed objects in Core Data, with their various attributes. They are retrieved for various reasons, have their attributes modified, etc., during game play.
Everything saves and retrieves perfectly until I quit and relaunch, at which point no data is present in the persistent store. This is the case whether using a simulator or a device.
My usual means of saving to Core Data is:
func saveIt(){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
appDelegate.saveContext()
}
Which calls:
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
savedFlag += 1
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
let savedCoins = try context.fetch(coinFetchRequest) as! [Coin]
print("In appDelegate, saveContext, after context.save, there are \(savedCoins.count) coins.")
print("Successfully saved in appDelegate \(String(describing: savedFlag)) times")
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Every print() statement I put in the code confirms the save, but when I retrieve (upon relaunch), via code similar to:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let issuerFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Issuer")
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
do {
let issuers = try context.fetch(issuerFetchRequest) as! [Issuer]
print(" ### Upon startup, there are \(issuers.count) Issuers in CD")
let coins = try context.fetch(coinFetchRequest) as! [Coin]
print(" #### Upon startup, there are \(coins.count) Coins in CD")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
I get:
### Upon startup, there are 0 Issuers in CD
#### Upon startup, there are 0 Coins in CD
Also, I try to save in applicationWillTerminate to be certain the data is saved before quitting:
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground.
// Saves changes in the application's managed object context before the application terminates.
self.saveContext()
let issuerFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Issuer")
let coinFetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Coin")
do {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let issuers = try context.fetch(issuerFetchRequest) as! [Issuer]
let coins = try context.fetch(coinFetchRequest) as! [Coin]
print("\\\\ Upon quit, there are \(issuers.count) Issuers in CD")
print("\\\\ Upon quit, there are \(coins.count) Coins in CD")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
However, the print() statements are not printed, leading me to believe applicationWillTerminate is never fired.
I should mention that Issuer has a to many relationship with Coin, and I ensure that there are Issuers present before creating and saving a Coin.
Any help greatly appreciated!
After beating myself half to death for longer than I care to admit, I found the fault in these three lines in AppDelegate.swift:
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
Once these were removed, everything went back to normal. I'd like to say I understand why, but honestly, removing these lines were an act of desperation. And the irony is that I had added these lines in an attempt to rectify an earlier problem with fetching from Core Data.
Many thanks to all who contributed!
Remember to load up the persistentContainer in your AppDelegate. You can create a project in Xcode with CoreData built-in.
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "CoreDataModelName")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
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()
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!