NSPersistentCloudKitContainer bugs - doesn't sync objects changed offline - ios

I'm testing my implementation of CoreData + CloudKit Sync, using NSPersistentCloudKitContainer and it seems to work fine some of the time. I fixed the problem with users switching iCloud on/off in the settings as recommended by Apple, but the problem I now have is that when I'm testing my app offline, when I'm back online with an internet connection, there's no syncing. I've got some code which does correctly trigger and detect the presence of an internet connection or not.. but still it doesn't seem that setting the container options to nil and then back to the cloudkit container gets it to sync. Here's my code so far:
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?
container = NSPersistentCloudKitContainer(name: "Model")
let description = NSPersistentStoreDescription(url: applicationDocumentsDirectory()!.appendingPathComponent("Model.sqlite"))
description.setOption(true as NSNumber,forKey: NSPersistentHistoryTrackingKey)
//Check if user is first logged onto iCloud enabled/not - if not then set cloudkitcontainer options to nil to disable sync and use the local DB on device
// This was from an accepted answer here: https://forums.developer.apple.com/thread/118924
//NB This works and the code here is triggered because a switch on/off icloud in settings kills the app and makes it restart, so this code is triggered.
if FileManager.default.ubiquityIdentityToken != nil { //logged onto iCloud*/
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.my.container")
} else {
description.cloudKitContainerOptions = nil;
}
//However, turning a device to airplane mode on/off won't kill the app to trigger the above code, so have to put that check in with checking when the device is on or offline:
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.my.container")
} else {
description.cloudKitContainerOptions = nil
}
}
//ensure migration------
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
//----------------------
container!.persistentStoreDescriptions = [description]
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)")
}
})
container!.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container!.viewContext.automaticallyMergesChangesFromParent = true
return container!
}()
Edit
I just tried this from device to device and it seems to work if one device is put into offline mode momentarily without the monitor instance activated (commented that bit of the code out to test). However if the device is put into airplane mode for a prolonged period of time it seems that CloudKit updates aren't sent and it's synced from the other device rather than the latest change from the device I'm on...

Related

Firestore fails to fetch full document after listener kept firing off continuously

Ok something really weird happened. Have an app that's about to go into production. This is how the application functions
There is a Home Page which fetches the logged in user's document every time the app comes to foreground
This is not a one time fetch but a listener so it will also listen for changes as the app is in foreground
I use this mechanism to keep the user document up to date and pass it to other pages in the app
When app goes to background, I remove the listener
The user document has close to 30 fields
So I was testing another functionality in app in the simulator and everything was working. I parallely ran the app with the same user logged in on a real device and this is what happened after the build:
As the app came to foreground the listener fired without stopping. I have the code in a such a way that every time the listener fetches the document from firestore, a part of the home page animates. So technically the home page refreshed without stopping
I killed the app and re-built it and the continuous firestore fetch happened yet again
So I all together deleted the app and re-built it - Now the continuous firestore fetch stopped
Here is the PROBLEM:
The document has all
30 fields on firestore
But on the real device, it fetches only 2 fields
I tried re-installing and re-building many times but this is the state
But on the simulator with the same user logged in, it works fine.
What could be the issue? Is there some corrupted cache for this particular user? Firestore is a solid product so never encountered anything like this before.
Here is the listener code:
#objc func viewEntersForeground(){
guard let currentUid = Auth.auth().currentUser?.uid else {return}
let ref = Firestore.firestore().collection("users").document(currentUid)
userListener = ref.addSnapshotListener({ (snapshot, error) in
if let error = error{
let alertController = ErrorAlertController(errorText: "\(error.localizedDescription)", errorTitle: "Something's wrong", viewController: self)
alertController.showAlertController()
return
}
//THE BELOW DICTIONARY FETCHES ONLY TWO FIELDS
guard let dictionary = snapshot?.data() else {return}
self.userDictionary = dictionary
self.user = User(dictionary: dictionary)
self.checkIfUserExists()
if self.user?.uid == "" || self.user?.uid == nil {
return
}
self.passDataToInbox()
self.passDataToSettings()
self.checkIfFirTokenExists()
self.checkIfProfilePictureExists()
if self.user?.userState == 3{
self.isUserState3 = true
} else {
self.userState = self.user?.userState
}
})
}

SwiftUI CloudKit not refreshing view when remaining active

I am developing a macOS and iOS app with SwiftUI. Both are using CoreData and iCloudKit to sync data between both platforms. It is indeed working very well with the same iCloud Container.
I am facing a problem that the iCloud background update is not being triggered when staying in the application. If I make changes on both systems, the changes are being pushed, however not visible on the other device.
I need to reload the app, close the app and open again or lose focus in my Mac app and come back to it. Then my List is going to be refreshed. I am not sure why it is not working, while staying inside the app without losing focus.
I am read several threads here in Stackoverflow, however they are not working for me. This is my simple View in iOS
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
#FetchRequest(entity: Person.entity(), sortDescriptors: []) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons, id:\.self) { person in
Text(person.firstName + (self.refreshing ? "" : ""))
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle(Text("Person"))
}
}
}
In this example I am already using a workaround, with Asperi described in a different question. However, that isn't working for me either. The list is not being refreshed.
In the logs I can see that it is not pinging the iCloud for refreshing. Only when I reopen the app. Why is background modes not working? I have activate everything properly and set up my AppDelegate.
lazy 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.
*/
container.persistentStoreDescriptions.forEach { storeDesc in
storeDesc.shouldMigrateStoreAutomatically = true
storeDesc.shouldInferMappingModelAutomatically = true
}
//let container = NSPersistentCloudKitContainer(name: "NAME")
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)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
UIApplication.shared.registerForRemoteNotifications()
return container
}()
Edit:
My iOS app only keeps fetching records from iCloud, when the app is being reopened. See this gif:
So apart from my comments and without more information, I suspect you have not set up your project correctly.
Under Signings and Capabilities, your project should look similar to this...
As mentioned I suspect a lot of the code in your ContentView view is unnecessary. Try removing the notifications and simplifying your view code, for example...
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Person.entity(),
sortDescriptors: []
) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons) { person in
Text(person.firstName)
}
}
.navigationBarTitle(Text("Person"))
}
}
}
With your project correctly setup, CloudKit should handle the necessary notifications and the #FetchRequest property wrapper will update your data set.
Also, because each Core Data entity is by default Identifiable, there is no need to reference id:\.self in your ForEach statement, so instead of...
ForEach(self.persons, id:\.self) { person in
you should be able to use...
ForEach(self.persons) { person in
As mentioned in the comments, you have included unnecessary code in your var persistentContainer. It should work as this...
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
For anyone looking at this lately, the fix for me after a LOT of searching was simply adding container.viewContext.automaticallyMergesChangesFromParent = true inside the init(inMemory) method in Persistence.swift (all stock from Apple for a SwiftUI in Xcode 12.5.1. As soon as I added that and rebuilt the 2 simulators, everything sync'd w/in 5-15 seconds as it should.
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "StoreName")
container.viewContext.automaticallyMergesChangesFromParent = true

Trouble with Firebase persistence feature

I have a small app in which users can upload local files to the server.
I am trying to handle unexpected situation like losing Internet connection. I want to use persistence feature which I have declared in AppDelegate.
Database.database().isPersistenceEnabled = true
At first, I export file to the server and if it succeeds then database reference is created.
let uploadTask = ref.putData(contents as Data, metadata: myMetadata, completion: { (metadata, error) in
if error != nil {
...
} else {
DataService.instance.usersRef.observeSingleEvent(of: .value) { (snapshot: DataSnapshot) in
...
}
}
)}
However, testing the solution (with turning airplane mode on and off) while exporting the file, the transfer is not restored. I am not sure if it's implementation issue or I'm missing key point of persistence feature.
Thanks in advance!

App Extension Programming Guide on sharing Core Data context with the main app

There is no documentation or sample code explaining if we can share the viewContext with the app extension or not.
AFAK, the app and the extension run in different processes and we should NOT share moc with another process/thread. I should not share the viewContext the containing app is using with the app extension.
So should we create another viewContext to use in app extension(? but NSPersistentContainer only provides one viewContext) or use a background context in app extension(???)
While an extension is running, it communicates directly only with the host app. There is no direct communication between a running extension and its containing app; typically, the containing app isn’t even running while its extension is running. In addition, the containing app and the host app don’t communicate at all.
So since they all run in different processes, so maybe (???) I can come to the conclusion that when the app extension ask for the viewContext, and when the containing app ask for the viewContext, the 2 viewContext(s) are actually distinct instances?
class Master {
static let shared: Master = Master()
lazy var persistentContainer: CCPersistentContainer = {
let container = CCPersistentContainer(name: "xt")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
private var _backgroundContext: NSManagedObjectContext!
var backgroundContext: NSManagedObjectContext {
if _backgroundContext == nil {
_backgroundContext = persistentContainer.newBackgroundContext()
}
return _backgroundContext
}
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
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)")
}
}
}
}
---- More On How To Sync Data Across Processes ----
1. WWDC 15: 224_hd_app_extension_best_practices
This WWDC session talks about how to post notification x processes.
2. NSMergePolicy
A policy object that you use to resolve conflicts between the persistent store and in-memory versions of managed objects.
3. WWDC 17: 210_hd_whats_new_in_core_data
4. UserDefaults(suiteName: AppGroups.primary)!
You can't share viewContext between the app and an extension. I don't mean you shouldn't, I mean it's actually, literally impossible even if you wanted to do so. An app and its extension are two separate processes. The viewContext object is created at run time by a process. There's no way for an app to hand off a variable in memory to a different process on iOS, so there's no possibility of using the same instance in both. You also have two different persistent container objects, again because they're created when the app or extension runs.
These two containers or view contexts might well use the same persistent store file. That's not unusual, and it allows the app and extension to access the same data.

Update data model on Apple Watch when phone app is in background

I have written an iOS app that refreshes its data model when a push notification is received whilst the app is in the foreground, once the data is retrieved from the server I send that information to the watch kit app using:
// This code resides in ErrorsViewController.swift
func updateWatchContext() {
do {
let messages = convertParseObjectsToJSON(tasks)
try session?.updateApplicationContext(["messages" : messages])
} catch let error as NSError {
NSLog("Updating the context failed: " + error.localizedDescription)
}
}
func convertParseObjectsToJSON(data:[PFObject])->[[String : AnyObject]]
{
var data = [[String:AnyObject]]()
for var i = 0; i < tasks.count; i++
{
let object = tasks[i]
data.append([
"createDate" : object["createDate"],
"errorMessage" : object["errorCode"]
])
}
return data
}
This works fine when the application is in the foreground, the data model gets updated on the watch as expected. However in the scenario that the phone is running in the background, how can I make a background fetch, parse the data and send it to the watchkit app without waking the iPhone, using watch connectivity?
I was thinking about trying to add the code in AppDelegate, but I don't believe that will work. I'd like to note that I do not want to make any network requests directly from the watch itself due to the limited CPU power; it would be un-necessary to handle the data parsing there.

Resources