Migrating Data to App Groups Disables iCloud Syncing - swift

I am adding a Today Extension to my existing app. I have added the an App Group and used this post to successfully migrate my Core Data's data to the App Group's store. My app uses both a NSPersistentCloudKitContainer (when iCloud is toggled on) and a NSPersistentContainer (iCloud toggled off). While the data in both containers migrate successfully, I am no longer able to sync between my devices when using NSPersistentCloudKitContainer. In the console I get these two errors:
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(837): : Failed to set up CloudKit integration for store: (URL: file://path/name.sqlite)
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
The path from the first error message is the path of the oldURL before switching to App Groups. So I believe I just need to tell iCloud to not try to integrate CloudKit at that store location and use the App Group's store location.
But I can not figure out how to do this. Can anyone help?
Core Data code:
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
//Get the correct container
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "App")
} else {
containerToUse = NSPersistentContainer(name: "App")
}
guard let container = containerToUse else {
fatalError("Couldn't get a container")
}
//Set the storeDescription
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.App")!.appendingPathComponent("\(container.name).sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
//migrate from old url to use app groups
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
print("Hey Listen! Error migrating persistent store")
print(error.localizedDescription)
}
// delete old store
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print("Hey Listen! Error deleting old persistent store")
print(error.localizedDescription)
}
})
}
}
}
return container
}
}

If you are still facing the same problem, you should add the following line for the storeDescription
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourapp.identifier")
Source: https://developer.apple.com/videos/play/wwdc2019/202/
Following is my CoreDataStack:
import CoreData
class CoreDataStack {
// MARK: - Core Data stack
static var persistentContainer: NSPersistentCloudKitContainer = {
/*
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 = NSPersistentCloudKitContainer(name: "data")
let storeURL = URL.storeURL(for: "group.com.myapp", databaseName: "data")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.myapp")
container.persistentStoreDescriptions = [storeDescription]
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
static 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)")
}
}
}
}
public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}

Related

Fetch data from CloudKit to app when app is in background

I have an app that uses Core Data with CloudKit to store data. When I am in the app (using the app) and create some new data on an other device, the data is fetched in a few seconds and I see it immediately, however if I am not using the app and create some new data on an other device, I have to enter the app and then the data starts fetching, is there a way to fetch data even if I am not using the app.
Here is my core data stack:
import CoreData
import Combine
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init() {
container = NSPersistentCloudKitContainer(name: "CoreData")
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "APP_GROUP_NAME")?.appendingPathComponent("CoreData.sqlite") else {
fatalError("Shared file container could not be created.")
}
let storeDescription = NSPersistentStoreDescription(url: fileContainer)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "Container_Identifier")
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
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
}
}

Preload sqlite to core data

I am trying to preload data to core data at the first run with a sqlite file. I first created a xcdatamodeld file, and then use python to produce the sqlite database, which has exactly the same data structure with the xcdatamodeld I stated.
#main
struct ArgosApp: App {
static var isFirstLaunch = isAppAlreadyLaunchedOnce()
// MARK:- Initialise Core Data Stack
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Argos")
// if it is, load data from sql
if ArgosApp.isFirstLaunch {
guard let seededDataURL = Bundle.main.url(forResource: "karate", withExtension: "sqlite") else {
fatalError("Fail to find")
}
let storeUrl = ArgosApp.getDocumentsDirectory().appendingPathComponent("karate.sqlite")
if !FileManager.default.fileExists(atPath: (storeUrl.path)) {
try! FileManager.default.copyItem(at: seededDataURL, to: storeUrl)
}
let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.url = storeUrl
description.type = "sqlite"
container.persistentStoreDescriptions = [description]
// ERROR came in here: Thread 1: "Unsupported store type."
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}
// else, load from core data
else {
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
// TODO:- More decent error handling
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
return container
}()
var body: some Scene {
WindowGroup {
let context = persistentContainer.viewContext
let categories = Category.fetchAllCategories(context: context) ?? []
// Pass the persistent container to the view as environment object
ChallengeChooser(categories: categories)
.environment(\.managedObjectContext
,context)
}
}
// Detect if it's the first-launch
static func isAppAlreadyLaunchedOnce() -> Bool {
let defaults = UserDefaults.standard
if let _ = defaults.string(forKey: "isAppAlreadyLaunchedOnce") {
print("App already launched")
return false
} else {
defaults.set(true, forKey: "isAppAlreadyLaunchedOnce")
print("App launched first time")
return true
}
}
func saveContext (context: NSManagedObjectContext) {
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
static func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
}
The error came in when I tried to load the database saying
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unsupported store type.'
Here is the code I ran with sqlite3, I add some test data on it so the difficulty and explanation column are all NULL.
cur.execute("""
CREATE TABLE category (
name VARCHAR(40) PRIMARY KEY
);
""")
cur.execute("""
CREATE TABLE video (
url VARCHAR(100) PRIMARY KEY,
explanation VARCHAR(300),
difficulty INT,
category VARCHAR(40),
FOREIGN KEY(category) REFERENCES category(name)
ON DELETE CASCADE
ON UPDATE CASCADE
);
""")
And this is what my xcdatamodeld looks like, and I set name and url to be non optional.

Core Data and CloudKit sync is inconsistent

I have an app that uses Core Data and CloudKit using the public database. The problem is that deletes never seem to sync and additions and changes don't show up until the app enters the background and then returns to the foreground, and even those results are inconsistent.
The example app is just the default app you get when specifying SwiftUI and Core Data. I modified the Schema in the CloudKit dashboard to add the two indexes recordName and modifiedAt.
The following is the Persistence.swift file, so to reproduce, create a new project, chose Core Data and Use CloudKit, Add Capability for CloudKit and Remote Notifications, then replace the Persistence.swift contents with the above.
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestCKSink")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
guard let description = container.persistentStoreDescriptions.first else
{
fatalError()
}
//description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
container.viewContext.transactionAuthor = "Me"
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
I have tried this with and without the history tracking option turned on. As I said, this works some of the time. I also realize that I need to add code to force refresh the display, but even checking the CloudKit dashboard and querying the records, some do not don't show there at all or take a long time to arrive (18 minutes).
Any suggestions would be welcomed.
I experienced similar issues some time ago, try the following setting when initializing your managed object context:
managedObjectContext.automaticallyMergesChangesFromParent = true

Using CloudKit Containers Properly

I have an app that uses NSPersistentCloudKitContainer for iCloud Sync to users who want it and has worked for all platforms (iPhone/iPad/Mac). But now, trying to add Apple Watch support, I realize that I might have implemented CloudKit wrong this whole time.
Code:
final class CoreDataManager {
static let sharedManager = CoreDataManager()
private var observer: NSKeyValueObservation?
lazy var persistentContainer: NSPersistentContainer = {
setupContainer()
}()
private func setupContainer() -> NSPersistentContainer {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
#if os(watchOS)
useCloudSync = true
#endif
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "app")
} else {
containerToUse = NSPersistentContainer(name: "app")
}
guard let container = containerToUse, let description = container.persistentStoreDescriptions.first else {
fatalError("Could not get a container!")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in }
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = "app"
container.viewContext.automaticallyMergesChangesFromParent = true
NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}
}
//MARK: - History token
private lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private var lastHistoryToken: NSPersistentHistoryToken? = nil {
didSet {
guard let token = lastHistoryToken, let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { return }
do {
try data.write(to: tokenFile)
} catch {
print("###\(#function): Failed to write token data. Error = \(error)")
}
}
}
private lazy var tokenFile: URL = {
let url = NSPersistentCloudKitContainer.defaultDirectoryURL().appendingPathComponent("app", isDirectory: true)
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: URL, withIntermediateDirectories: true, attributes: nil)
} catch {
print("###\(#function): Failed to create persistent container URL. Error = \(error)")
}
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
init() {
// Load the last token from the token file.
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
}
//MARK: - Process History
#objc func storeRemoteChange(_ notification: Notification) {
// Process persistent history to merge changes from other coordinators.
historyQueue.addOperation {
self.processPersistentHistory()
}
}
func processPersistentHistory() {
let backContext = persistentContainer.newBackgroundContext()
backContext.performAndWait {
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
let result = (try? backContext.execute(request)) as? NSPersistentHistoryResult
guard let transactions = result?.result as? [NSPersistentHistoryTransaction], !transactions.isEmpty else {
print("No transactions from persistent history")
return
}
// Update the history token using the last transaction.
if let lastToken = transactions.last?.token {
lastHistoryToken = lastToken
}
}
}
What I've noticed:
I only have 10 items on my test device. When I boot up the watch app and look at the console, it looks like it's going through the entire history of every addition and deletion of items I've ever done, making it take a long time to download the 10 items that I actually have left.
I looked in my iCloud storage and found out that my app is taking up a lot of space (48 MB) when the 10 items are just entities with a few strings attached to them
Research:
I've done a lot of research and found that it could be from setting NSPersistentHistoryTrackingKey only when the user is not using iCloud Sync. But when I enable NSPersistentHistoryTrackingKey for iCloud users too, the watch app still takes a very long time to download the 10 items.
I know Core Data can be tricky, and changing persistentStoreDescriptions or other attributes of the container can be an app-breaking bug to existing users. So I need something that works for new and existing users.
Ask:
Does anyone know how to fix this problem or have had similar issues? I've been trying to figure this out for almost a week now, and any help would be greatly appreciated!

Swift - Using Core Data With Cocoa

I'm playing with Core Data for an OS X application. The language is Swift. There's something odd about the way it works for Cocoa. The following is a shorter version of what Xcode creates.
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
// MARK: - Core Data stack
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
let fileManager = NSFileManager.defaultManager()
var failError: NSError? = nil
var shouldFail = false
var failureReason = "There was an error creating or loading the application's saved data."
// Make sure the application files directory is there
do {
let properties = try self.applicationDocumentsDirectory.resourceValuesForKeys([NSURLIsDirectoryKey])
if !properties[NSURLIsDirectoryKey]!.boolValue {
failureReason = "Expected a folder to store application data, found a file \(self.applicationDocumentsDirectory.path)."
shouldFail = true
}
} catch {
let nserror = error as NSError
if nserror.code == NSFileReadNoSuchFileError {
do {
try fileManager.createDirectoryAtPath(self.applicationDocumentsDirectory.path!, withIntermediateDirectories: true, attributes: nil)
} catch {
failError = nserror
}
} else {
failError = nserror
}
}
}()
}
And an SQLite file for Core Data is nowhere to be found after I add a new record to an entity. There's no pointer to an SQLite file in the code above. The following is an iOS counterpart.
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Core Data stack
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added 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.
// Create the coordinator and store
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() 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.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
}
The code above points to an SQLite file. So I don't have trouble inserting a record to an entity.
I'm using Xcode 7.2.1 Am I doing something wrong? Or is this an Xcode bug?
The code you have posted here is only part of a functional Core Data stack, and is insufficient to initialise Core Data.
This is Apple's current example code for initialising a Core Data Stack:
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = NSBundle.mainBundle().URLForResource("DataModel", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOfURL: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.URLByAppendingPathComponent("DataModel.sqlite")
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
}
}
See Initialising the Core Data Stack from Apple.
The Swift 3 version of #DuncanBabbage solution:
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
override init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = Bundle.main.url(forResource: "DataModel", withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.appendingPathComponent("DataModel.sqlite")
do {
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
}
}