CoreData context not updating and bool always false? - core-data

I am trying to update a user record with a boolean value in coredata. The record itself is updating, but I can only see the change between app sessions, not the same session.
Essentially, I am trying to trap the user agreeing or refusing the apps terms of use. A check is performed on the landing screen after login and if the terms have been agreed to, then continue as normal. However, if the terms have not been agreed to then I need to fire a popover allowing them to accept the terms (This happens every time they hit the landing page until they accept)
I am running the check but no matter what the value is in the core data record, it always evaluates as false:
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "User")
fetchRequest.predicate = NSPredicate(format: "email == %#", userEmail)
fetchRequest.returnsObjectsAsFaults = false
do {
userRecord = try context.fetch(User.fetchRequest())
if userRecord[0].acceptedTerms != nil && userRecord[0].acceptedTerms {
print("Terms accepted, move along")
} else {
print("Terms refused, pop dialog")
}
} catch {
print("No user found")
}
When a user accepts the terms, I try to update the core data record. Which does update, however I can only see the update between app sessions. Whilst in the same session, if the user hits the landing screen again, the value is still set to false (Or at least the acceptedTerms field in core data is still a 0) - I have done some searching on this and seen there is automaticallyMergesChangesFromParent and also refreshAllObjects() however neither seem to have any effect.
** Update with save code from AppDelegate **
func updateTermsComplete(userLogin: String, termsPermission: String){
let context = self.persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "User")
fetchRequest.predicate = NSPredicate(format: "email == %#", userLogin)
fetchRequest.returnsObjectsAsFaults = false
do {
userRecord = try context.fetch(fetchRequest) as! [User]
} catch {
print("No user found")
}
if !userRecord.isEmpty {
print("User already exists")
userRecord[0].setValue(termsPermission, forKey: "acceptedTerms")
do {
try context.save()
print("User updated")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
}
self.saveContext()
}
context.refreshAllObjects()
}
func saveContext () {
let context = persistentContainer.viewContext
context.automaticallyMergesChangesFromParent = true
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}

Related

Multiple duplicate NSPersistentStoreRemoteChange notifications fired from CloudKit + CoreData

I am seeing .NSPersistentStoreRemoteChange notifications being received multiple times, sometimes up to 10 times.
I don't think this is harmful, but best case it's using up processing power.
So my questions are:
Is this harmful?
Can I prevent it?
If not, is there a recommended way to ignore duplicate notifications?
--
I have the following code to setup my container. This is contained in the initialiser of a singleton and I have confirmed that it is called once.
guard let modelURL = Bundle(for: type(of: self)).url(forResource: momdName, withExtension:"momd"),
let mom = NSManagedObjectModel(contentsOf: modelURL)
else {
fatalError("🔐 Error loading model from bundle")
}
let containerURL = folderToStoreDatabaseIn.appendingPathComponent("Model.sqlite")
container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: mom)
guard let description = container.persistentStoreDescriptions.first else {
fatalError("🔐 ###\(#function): Failed to retrieve a persistent store description.")
}
description.url = containerURL
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
super.init()
// this must be called after super.init()
// ***** ADD OBSERVER *****
NotificationCenter.default.addObserver(self,
selector: #selector(updatedFromCKCD(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
Logger.error("🔐 ###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
The code to process these changes:
// https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
#objc func updatedFromCKCD(_ notifiction: Notification) {
let fetchHistoryRequest = NSPersistentHistoryChangeRequest.fetchHistory(
after: lastToken
)
let context = container.newBackgroundContext()
guard
let historyResult = try? context.execute(fetchHistoryRequest)
as? NSPersistentHistoryResult,
let history = historyResult.result as? [NSPersistentHistoryTransaction]
else {
Logger.error("⛈ Could not convert history result to transactions")
assertionFailure()
return
}
Logger.debug("⛈ Found cloud changes since: \(self.lastToken?.description ?? "nil")")
DispatchQueue.main.async {
// look for particular set of changes which require the user to take action
...
}
}

Manual Core Data Requests in SwiftUI

Given that #FetchRequest does not support dynamic predicates (eg, I could not update the "month" in a calendar and re-query for the events I have scheduled in that month), I am trying to get a manual request to work.
The #FetchRequest already works if I don't try to make a dynamic predicate, so I assume the core data is configured to work correctly.
In my SceneDelegate I have:
guard let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else {
fatalError("Unable to read managed object context.")
}
let contentView = ContentView().environment(\.managedObjectContext, context)
and in my AppDelegate:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Transactions")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
If I try doing a manual request in my ContentView like:
let employeesFetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Employees")
do {
let employees = try managedObjectContext.fetch(employeesFetch) as! [Employees]
} catch {
fatalError("Failed to fetch employees: \(error)")
}
This fails because `Exception NSException * "+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Employees'"
What configuration am I missing?

NSFetchedResultsController can't update the tableView swift4

I'm trying to make it work for last couple of days and can't get it working. Its something tiny detail obviously I can't seem to find.
Could you take a look and give me some insights about my code?
I'm trying to update the logView with app savings in the coredata.
Here's the entire code for ViewController and CoreData Handler.
/// fetch controller
lazy var fetchController: NSFetchedResultsController = { () -> NSFetchedResultsController<NSFetchRequestResult> in
let entity = NSEntityDescription.entity(forEntityName: "Logs", in: CoreDataHandler.sharedInstance.backgroundManagedObjectContext)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
fetchRequest.entity = entity
let nameDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [nameDescriptor]
let fetchedController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataHandler.sharedInstance.backgroundManagedObjectContext, sectionNameKeyPath: "duration", cacheName: nil)
fetchedController.delegate = self as? NSFetchedResultsControllerDelegate
return fetchedController
}()
override func viewDidLoad() {
title = "Week Log"
tableView.tableFooterView = UIView(frame: CGRect.zero)
tableView.separatorColor = UIColor.black
tableView.backgroundColor = UIColor.red
refreshView()
loadNormalState()
loadCoreDataEntities()
}
/**
Refresh the view, reload the tableView.
*/
func refreshView() {
loadCoreDataEntities()
tableView.reloadData()
}
/**
Load history entities from core data. (I'm printing on the console and
be able to see the the fetched data but I can't load it to tableView.)
*/
func loadCoreDataEntities() {
do {
try fetchController.performFetch()
} catch {
print("Error occurred while fetching")
}
}
import Foundation
import CoreData
class CoreDataHandler: NSObject {
/**
Creates a singleton object to be used across the whole app easier
- returns: CoreDataHandler
*/
class var sharedInstance: CoreDataHandler {
struct Static {
static var instance: CoreDataHandler = CoreDataHandler()
}
return Static.instance
}
lazy var backgroundManagedObjectContext: NSManagedObjectContext = {
let backgroundManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let coordinator = self.persistentStoreCoordinator
backgroundManagedObjectContext.persistentStoreCoordinator = coordinator
return backgroundManagedObjectContext
}()
lazy var objectModel: NSManagedObjectModel = {
let modelPath = Bundle.main.url(forResource: "Model", withExtension: "momd")
let objectModel = NSManagedObjectModel(contentsOf: modelPath!)
return objectModel!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.objectModel)
// Get the paths to the SQLite file
let storeURL = self.applicationDocumentsDirectory().appendingPathComponent("Model.sqlite")
// Define the Core Data version migration options
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
// Attempt to load the persistent store
var error: NSError?
var failureReason = "There was an error creating or loading the application's saved data."
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
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 persistentStoreCoordinator
}()
func applicationDocumentsDirectory() -> NSURL {
return FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last! as NSURL
}
func saveContext() {
do {
try backgroundManagedObjectContext.save()
} catch {
print("Error while saving the object context")
// Error occured while deleting objects
}
}
You have a data source delegate somewhere. That data source delegate tells the table view how many items there are, and what their contents is. How does it know how many items? That must be stored somewhere.
When the fetch controller is successful, it must modify the data that the data source delegate relies on in some way, and then call reloadData. Are you doing this? Are you doing anything that causes the data source delegate to change the number of items it reports?
And calling loadCoreDataEntities, immediately followed by reloadData, is nonsense. loadCoreDataEntities is asynchronous. By the time you call reloadData, it hasn't loaded any entities yet. realodData is called when loadCoreDataEntities has finished.

doubled savings with core data swift 4

developing a little app for my comic collection encountered this issue:
in my second "add comic" VC I have a button and the func below, but I save TWICE entities in manged context (ate least, I think this is the issue)
for example if I have 2 comics yet shown in main VC tableview, go to "add comic VC" and save a third one, going back to main VC I'll print 3 objects with title, number etc but also print 2 new objects with no data as I had saved twice a manger context a "right one" and another one with same number of object but empty. If I keep adding a 4th comic, I'll get 6 complete comic + the 4th and more 6 "blank itmes" with default values "no title"
let kComicEntityName = "Comic"
func addingSingleComic(gotTitle: String, gotIssue: Int16, gotInCollection: Bool ) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: kComicEntityName, in: managedContext)!
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
comicToAdd.comicTitle = gotTitle
comicToAdd.issueNumber = gotIssue
comicToAdd.inCollection = gotInCollection
do {
try managedContext.save()
} catch let error as NSError {
print("could not save. \(error), \(error.userInfo)")
}
print("new single comic crated: title: \(comicToAdd.comicTitle ?? "!! not title !!"), n. \(comicToAdd.issueNumber), owned?: \(comicToAdd.inCollection)")
}
in the main VC I use this to check items in core data
func asyncPrintEntities() {
self.asyncComicEntityArray.removeAll(keepingCapacity: true)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let comicFetch : NSFetchRequest<Comic> = Comic.fetchRequest()
asyncFetchRequest = NSAsynchronousFetchRequest<Comic>(fetchRequest: comicFetch) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let AllComicEntityResult = result.finalResult else {
return
}
self.asyncComicEntityArray = AllComicEntityResult
//************************************
do {
self.asyncComicEntityArray = try managedContext.fetch(comicFetch)
if self.asyncComicEntityArray.count > 0 {
print("Ok! model is not empty!")
} else {
print("No entites availabe")
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
guard self.asyncComicEntityArray != nil else {return}
for comicFoundInArray in self.asyncComicEntityArray {
let entity = NSEntityDescription.entity(forEntityName: self.kComicEntityName, in: managedContext)!
var comicTouse = Comic(entity: entity, insertInto: managedContext)
// var comicTouse = Comic() //to be deleted since this kind of init is not allowed, better above insertInto
comicTouse = comicFoundInArray as! Comic
print("comic title: \(comicTouse.comicTitle ?? "error title"), is it in collection? : \(comicTouse.inCollection)")
}
self.MyTableView.reloadData()
//************************************
}
// MARK: - async fetch request 3
do {
try managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
//end of function
}
In your addingSingleComic you create a new Comic here:
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
Then you assign values to the object's properties.
Separately, in asyncPrintEntities, you also create new Comic objects here:
var comicTouse = Comic(entity: entity, insertInto: managedContext)
This time you do not assign values to the object's properties. They will have no title, etc, because you created them but never assigned a title. This line executes once for every object in asyncComicEntityArray, so if the array has two objects, you create two new objects that contain no data. You don't use comicToUse anywhere except in the one print, but it still exists in the managed object context and will still get saved the next time you save changes.
This is why you're getting extra entries-- because you're creating them in this line of code. It's not clear why you're creating them here. You just executed a fetch request, and then you immediately create a bunch of no-data entries which you don't use. It looks like that entire for loop could just be deleted, because the only thing it does is create these extra entries.

NSFetchedResultsController and to-many relationship not working

Ok, I was searching and trying in that case for the last 1-2 weeks and I didn't get it work. I would be able to achieve what I want without NSFRC but for performance reasons and convienience I would like to do it with the NSFRC.
So, I have a DataModel with 2 Entities - see the picture
There is one Account and one account can have many accountchanges - which is quite obvious.
So I want to be able to choose an Account and then show all AccountChanges for that specific Account.
So far I was able to get the Account and also accessing the NSSet in cellForRow Function but I am not getting the correct sections and numberOfRowsInSection - this is the main issue.
Here is some code:
func numberOfSections(in tableView: UITableView) -> Int {
print("Sections : \(self.fetchedResultsController.sections?.count)")
if (self.fetchedResultsController.sections?.count)! <= 0 {
print("There are no objects in the core data - do something else !!!")
}
return self.fetchedResultsController.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Section Name")
print(self.fetchedResultsController.sections![section].name)
let sectionInfo = self.fetchedResultsController.sections![section]
print("Section: \(sectionInfo) - Sections Objects: \(sectionInfo.numberOfObjects)")
return sectionInfo.numberOfObjects
}
There are some print statements which are only for information!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = myTable.dequeueReusableCell(withIdentifier: "myCell")! as UITableViewCell
let accountBalanceChanges = self.fetchedResultsController.object(at: indexPath)
print("AccountBalanceChanges from cell....")
print(accountBalanceChanges)
let details = accountBalanceChanges.accountchanges! as NSSet
print("Print out the details:")
print(details)
let detailSet = details.allObjects
let detailSetItem = detailSet.count // Just for information!
let myPrint = detailSet[indexPath.row] as! AccountChanges
let myVal = myPrint.category
myCell.textLabel?.text = myVal
return myCell
}
So, I am able to get the data but always only one item and not the whole set - I guess due to the fact that the sections/ numberOfRows are wrong.
Here is my NSFRC
var fetchedResultsController: NSFetchedResultsController<Accounts> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<Accounts> = Accounts.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "aName", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate = NSPredicate(format: "(ANY accountchanges.accounts = %#)", newAccount!)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} 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)")
}
return _fetchedResultsController!
}
I am assuming it is the SortDescriptor or the predicate - or maybe both?
Any help or at least directions are well appreciated.
I already tried many different approaches but none was giving me the correct results.
I would do the opposite, I mean using the FRC to fetch all the changes for an account with a certain Id, and use the following predicate:
let predicate = NSPredicate(format: "accounts.aId = %#", ACCOUNTID)
or
let predicate = NSPredicate(format: "accounts = %#", account.objectID)
I would rename Accounts entity to Account and same for the relationship since it's a to-one relationship.
That's assuming you have a table view with all the accounts and when you click on one it gives you back its changes.
var fetchedResultsController: NSFetchedResultsController<AccountChanges> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<AccountChanges> = AccountChanges.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "aName", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate = NSPredicate(format: "accounts.aId = %#", ACCOUNTID)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} 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)")
}
return _fetchedResultsController!
}
Cheers

Resources