SwiftUI CloudKit not refreshing view when remaining active - ios

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

Related

SwiftIU/Firebase: How to update lists of users from Firebase in real time and have it reflect in the UI?

https://www.loom.com/share/de410c2626644dd796ad407fcee7e5c7
^^^ I've attached a loom video demonstrating the bug im facing as well as the code I currently have.
The problem is that the UI doesn't update right away and may confuse users. All the code in terms of updating the backend function correctly (its the updating of the UI thats not working properly), I'm pretty sure it has to do with the way i'm either calling the functions or the function itself.
Any help would be greatly appreciated!
#Published var userRequestInboxUsers = [User]()
#Published var emergencyContactUsers = [User]()
// function to fetch the user requests
func fetchTheUsersRequests() {
guard let uid = user.id else { return }
let query = COLLECTION_FOLLOWERS.document(uid).collection("inbox").whereField(
"currentStatus", isEqualTo: "isPending")
query.addSnapshotListener(includeMetadataChanges: true) { snapshot, error in
if let error = error {
print("There was an error querying the inbox requests: \(error.localizedDescription)")
} else {
for request in snapshot!.documents {
COLLECTION_USERS.document(request.documentID).getDocument { snapshot, error in
if let error = error {
print("There was an error fetching the user data: \(error)")
} else {
DispatchQueue.main.async {
guard let userRequestInInbox = try? snapshot?.data(as: User.self) else { return }
self.userRequestInboxUsers.append(userRequestInInbox)
}
}
}
}
}
}
}
//function that fetches the users contacts (request that have been approved)
func fetchTheUsersContacts() {
guard let uid = user.id else { return }
let query = COLLECTION_FOLLOWERS.document(uid).collection("inbox").whereField(
"currentStatus", isEqualTo: "emergencyContact")
query.addSnapshotListener(includeMetadataChanges: true) { snapshot, error in
if let error = error {
print("There was an error querying the emergency contacts: \(error.localizedDescription)")
} else {
for userContact in snapshot!.documents {
COLLECTION_USERS.document(userContact.documentID).getDocument { snapshot, error in
if let error = error {
print("There was an error fetching the user data: \(error)")
} else {
DispatchQueue.main.async {
guard let userEmergencyContactsInInbox = try? snapshot?.data(as: User.self) else {
return
}
self.emergencyContactUsers.append(userEmergencyContactsInInbox)
}
}
}
}
}
}
}
I've tried calling the function every time the view appears but that leads to duplicated results.
I'm currently using snapshot listeners to get real time access but even then this doesn't work.
I've structured my backend to have a contacts sub collection and a requests sub collection but I get the same problem with much more lines of code...
I've thought of switching to async/await but i would prefer my app be compatible to ios 14+ rather than just 15 and up.
I could try using strictly Combine rather than call backs but I don't think that would be effective in attacking the problem head on.
The problem is that you're appending the documents to the published property. This will lead to duplicating the entries.
Instead, just assign all of the mapped documents to the emergencyContactUsers property.
Check out Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese for more details about this. I've also got a number of other posts about Firestore and SwiftUI on my blog that might be useful.
As for the "duplicate IDs" warning you see - that might actually also contribute to the issue. SwiftUI lists require all list items to have a unique ID, and it seems like your user objects might not have unique IDs. This might either be due to the fact you have multiple copies of the same user in the published properties, or that your User struct is not identifiable.
I noticed that you're using DispatchQueue.async to make sure you're on the main thread. This is not necessary, as Firestore will make sure to call your code on the main thread. See https://twitter.com/peterfriese/status/1489683949014196226 for an explanation.
Also - I am curious about what you said in the beginning of the video about not being able to find documentation about this. We're always looking for ways to make the Firebase docs better - what did you look for / what didn't you find?

NSPersistentCloudKitContainer bugs - doesn't sync objects changed offline

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...

unable to create a context in swift file

I need to fetch some data from CoreData and need to do it frequently and hence trying to create utility class for it.
When I try to create context for it, it gives me error and below is the code.
I added a new .swift file and pasted below code
import Foundation
import UIKit
import CoreData
class armyDataSource{
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
}
really not sure what wrong I'm doing it here.
if you want to create a wrappper class for core data manager you can write class like given below code in a swift file with your implementation.
import UIKit
import CoreData
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {} // Prevent clients from creating another instance.
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: "StackOF")
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)")
}
}
}
}
You cannot initialize these properties like that in a class. You need to do this initialization in a method, best in the init call.
Cannot use instance member 'appDelegate' within property initializer; property initializers run before 'self' is available
So this means that you cannot use a property to initialize another property because this is all done before the init is called and self is fully available.
Try this instead:
class armyDataSource {
let appDelegate: UIApplicationDelegate
let context: NSManagedObjectContext
init() {
appDelegate = UIApplication.shared.delegate as! AppDelegate
context = appDelegate.persistentContainer.viewContext
}
}

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.

Core Data Entity Unique Constraint Does Not Work

I am trying to set a constraint in core data with the new Entity Constraints inspector (To make the name of the item unique). All that I've read says it's pretty simple - Set the constraint and handle the error. I don't get any errors and can add the same entry as many times as I want.
The app does require IOS 9.0, Xcode tools requirement is set to 7.0
The constraint, category1Name, is a String.
My addItem code is:
func addNewRecord() {
//check to be sure the entry is not empty
if (categoryTextField.text == "") {
//prompt requiring a name
let ac = UIAlertController(title: nil, message: "Name Required", preferredStyle: .Alert)
ac.addAction(UIAlertAction(title: "Ok", style: .Default, handler: nil))
self.presentViewController(ac, animated: true, completion: nil)
} else {
let newManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Category1", inManagedObjectContext: kAppDelegate.managedObjectContext) as! Category1
newManagedObject.category1Name = categoryTextField.text
newManagedObject.category1Description = categoryTextView.text
//bunch more items...
//save it
kAppDelegate.saveContext()
makeEntryFieldsEnabledNO()
performSegueWithIdentifier("unwindToCategoriesTableViewController", sender: self)
}//if else
}//addNewRecord
The AppDelegate save is standard:
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
//insert your standard error alert stuff here
let nserror = error as NSError
print("From the print line: Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}//do catch
}//if moc
}//saveContext
Here's the Core Data constraint:
This app is iCloud enabled.
The managedObjectContext merge policy is set to NSMergeByPropertyObjectTrumpMergePolicy
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return managedObjectContext
}()//var managedObjectContext
Any guidance would be appreciated.
It would appear Apple have finally fixed the crazy Xcode problem where changes you make in a data model file don't actually change.
Putting that aside, the current formula seems to be:
in your core data singleton ...
container = NSPersistentContainer(name: _nom)
// during development, right HERE likely delete the sql database file
// and start fresh, as described here stackoverflow.com/a/60040554/294884
container.loadPersistentStores { storeDescription, error in
if let error = error {
print("\n ERROR LOADING STORES! \(error) \n")
}
else {
print("\n STORES LOADED! \(storeDescription) \n")
}
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
self.container.viewContext.automaticallyMergesChangesFromParent = true
}
You must use merge policy and automatically merges.
Then in your data model file
Don't bother unless every relationship has an inverse, with
"to one or many" correctly set
and (almost certainly, except in very unusual source data) your unique id for each entity is indicated as a constraint
Then when you add new data, you must
use the new background context supplied by the handy core data function which does that
so, never try to make your own separate thread
double-check you have done (1) and (2) !
when you do add a few entities, you must do that inside a perform
and when you have finished adding entities (ie on the new thread) you must while still in the perform ...
do a performAndWait which does two things
save the new items (on the new child thread), and then
save the new items (on the main view thread)
naturally for both 7 and 8, you have to check .hasChanges before saving
Easy right?
So something like
let pm = core.container.newBackgroundContext()
pm.perform {
for onePerson in someNewData {
... create your new CDPerson entity ...
}
pm.bake()
}
Note that the bake routine is within the perform block,
and it looks like this:
func bake() {
self.performAndWait {
if self.hasChanges {
do {
try self.save()
}
catch {
print("bake disaster type 1 \(error)")
}
}
// OPTIONALLY, SEE BELOW
if core.container.viewContext.hasChanges {
do {
try core.container.viewContext.save()
}
catch {
print("bake disaster type 2 \(error)")
}
}
// OPTIONALLY, SEE BELOW
}
}
To be clear, notice the pm.bake ... in the function bake(), the self in the first half is indeed that newBackgroundContext which is created for the loop inside the perform.
Note that these days you don't even need to save to the main context
Nowadays automaticallyMergesChangesFromParent seems to work perfectly, if you "do everything in the long list above".
• In the bake above, add a couple print lines to see what is saved to the viewContext. You'll see that nothing, at all, is ever saved. It's all done properly by the child/whatever relationships in the engine
• So in fact, in reality you can just omit that passage of the code. All you have to do is
func bake() {
self.performAndWait {
if self.hasChanges {
do {
try self.save()
}
catch {
print("bake disaster type 1 \(error)")
}
}
}
If you want to get an error when there are merge conflicts and handle them manually then you need to change your policy to NSErrorMergePolicy and you will get an error and in the user info the object IDs that you need to solve merge conflict , otherwise it will merge and save according to the specified merge policy.
The policy that you set will overwrite the object attributes but not relationships, if you want to overwrite the attributes and relationships then specify the NSMergeByPropertyObjectTrumpMergePolicy.
The comment from pbasdf above seems to be correct. Constraints in the inspector don't save. I used both methods suggested in the link provided - I added the constraint, I changed another attribute, did a file save, then changed that attribute back and did a file save again. The constraint now acts as I expect. I'll mark this as answered. pbasdf should get the credit.

Resources