TableView.reloadData() doesn't work after save data into core data entity - swift

I'm trying to insert a default record into my core data entity while the tableview first-time loaded and checked there's no data in the entity.
The data inserted just fine , but the reloadData() didn't work, after navigate to other view and navigate back to the view the data appears. no matter the reloadData() in or out of the .save() method.
override func viewDidLoad() {
let cateContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let categoryRequest = NSFetchRequest(entityName: "Category")
categoryArray = (try! cateContext.executeFetchRequest(categoryRequest)) as! [Category]
if categoryArray.count == 0 {
let category = NSEntityDescription.insertNewObjectForEntityForName("Category", inManagedObjectContext: cateContext) as! Category
category.sno = "1"
category.category = "General"
category.locate = false
do {
try cateContext.save()
self.categoryTableView.reloadData()
} catch let saveError as NSError {
print("Saving Error : \(saveError.localizedDescription)")
}
}
//self.categoryTableView.reloadData()
}

If your are calling self.categoryTableView.reloadData() in viewDidLoad() method it will not reload your tableView twice. You need to call self.categoryTableView.reloadData() after you have checked if entity existed again.

Related

NSFetchRequestResult returns duplicate data from the database

I have a simple entity with identifier (constraint) and name fields. In ViewController I try to add data to the database without worrying about duplicates and there really is no duplicate data in the database, but when I try to get records, I get twice as many of them. As I found out, this only happens when I try to write something to the database, and regardless of whether the attempt was successful, the data goes to my NSFetchRequestResult. What is the reason for this behavior?
DB content now:
ViewController:
class ViewController: UIViewController {
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
//if comment this loop I won't get duplicates
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
try? moc.save() //if the code is not executed for the first time, then the attempt is unsuccessful due to identifier constraint
}
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Entity")
let fetchedEntities = try? moc.fetch(fetch) as? [Entity]
print(fetchedEntities!.count) // Output: 12 (actually only 6 records in the db)
}
}
Change your code where you create the objects to
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
}
do {
try moc.save()
} catch {
moc.reset()
}
This way you will remove the faulty (duplicate) objects from the context

Why tableView.reloadData() is not triggered after Core Data container.performBackgroundTask()

I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.
class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
var container:NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
var frc : NSFetchedResultsController<Student>?
override func viewDidLoad() {
container?.performBackgroundTask { context in
// adds 100 dummy records in background
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
} // end of background inserting.
// now defining frc:
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch() // this works and I get no errors
tableView.reloadData()
frc.delegate = self
} // end of frc definition
}
}
If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).
But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.
The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:
// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData() // causes UI to behave weirdly
}
then I tried to call viewContext.perform to reload the table view in the proper thread
func viewDidLoad() {
// code for inserting 100 dummy rows in background thread
// code for defining frc and setting self as delegate
if let context = container?.viewContext {
context.perform { [weak self] in
self?.tableView.reloadData() // that also causes UI to behave weirdly
}
}
}
How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?
override func viewDidLoad() {
super.viewDidLoad()
//Always need your delegate for the UI to be set before calling the UI's delegate functions.
frc.delegate = self
//First we can grab any already stored values.
goFetch()
//This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
container?.performBackgroundTask { context in
//We are in a different queue than the main queue, hence "backgroundTask".
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
//Now that we are done saving its ok to fetch again.
goFetch()
}
//goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
//The reason it works if you restart the app because that data you didn't let finish saving is persisted
//So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.
}
func goFetch() {
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch()
//Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
//To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.
My code is slightly different since I used a project I had for this. Anyway here is what I did:
In my view controller I had a property for my container
private var persistentContainer = NSPersistentContainer(name: coreDataModelName)
And in viewDidLoad I loaded the persistent store and created my 100 records.
persistentContainer.loadPersistentStores { persistentStoreDescription, error in
if let error = error {
print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
} else {
self.createFakeNotes() // Here 100 elements get created
DispatchQueue.main.async {
self.setupView() // other stuff, not relevant
self.fetchNotes() // fetch using fetch result controller
self.tableView.reloadData()
}
}
}
Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer
I also delete old notes first to make the testing easier.
private func createFakeNotes() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
} catch {
print("Delete error [\(error)]")
return
}
let privateContext = persistentContainer.newBackgroundContext()
privateContext.automaticallyMergesChangesFromParent = true //Important!!!
privateContext.perform {
let createDate = Date()
for i in 1...100 {
let note = Note(context: privateContext)
note.title = String(format: "Title %2d", i)
note.contents = "Content"
note.createdAt = createDate
note.updatedAt = createDate
}
do {
try privateContext.save()
do {
try self.persistentContainer.viewContext.save()
} catch {
print("Fail saving main context [\(error.localizedDescription)")
}
} catch {
print("Fail saving private context [\(error.localizedDescription)")
}
}
}
You should fetch your data by calling it from viewwillappear and then try to reload your tableview.
override func viewWillAppear(_ animated: Bool) {
getdata()
tableView.reloadData()
}
func getdata() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do{
persons = try context.fetch(Person.fetchRequest())
}
catch {
print("fetching failed")
}
}

core data and relationship predicate

I've start swift & core data few month ago usually I've found my answer on this website but for the first time I'm really stuck with "Relationship" and "Predicates"
I've created a first view controller with a tableview which is populated by the user and this part is working like I wish.
The user can "tap" a cell and open a new view controller with a new tableview and I'd like populate this tableview with data that in relation with the cell the user tapped.
I'm using CoreData and I've set 2 entities : "Compte" and "Operation" they are in relationship by ONE TO MANY (ONE compte for MANY operation)
Here where I am :
when the user tap the cell i'm using segue to send the "Compte" to the second view controller :
//Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let guest = segue.destination as! OperationsViewController
let indexPath = tableView.indexPathForSelectedRow
let operation = fetchedResultController.object(at: indexPath!)
guest.compteTestRelation = operation
}
In the OperationsViewController i've set this variable :
var compteTestRelation: Compte!
for testing my data I've create a FOR LOOP like this and a FUNCTION:
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
the FUNCTION
func displayOperation() {
if let opList = compteTestRelation.relationCompte as? Set<Operation> {
sortedOperationArray = opList.sorted(by: { (operationA:Operation, operationB:Operation) -> Bool in
return operationA.date!.compare(operationB.date! as Date) == ComparisonResult.orderedAscending
})
print(sortedOperationArray)
}
}
In the console with "print" It work like I wish depend the cell is tapped the print(sortedOperationArray) appear or not
My problem now is how populate my tableview with this data, when I use predicates in my FetchResultController I've got error or an empty tableview but in the console everything seems to work so I'm thinking the relationship is OK ..
If I don't use PREDICATE I can populate my tableview with the data but I see always ALL the data
I've seen other similar problems and answers on stackoverflow.com but nothing work for the moment.
Thank You! :)
I've found an another way to predicate my data and it works for me now
I've create a new attribute in my OPERATION entity called "id" and when I create my data I attribute an ID like this :
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
newOp.id = "id123\(compteTestRelation.nom!)"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
then I predicate my data like this in my FetchResultController :
func setupFetchedResultController () {
let operationsRequest: NSFetchRequest<Operation> = Operation.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "nom", ascending: true)
let keyPath = "id"
let searchString = "id123\(compteTestRelation.nom!)"
let operationsPredicate = NSPredicate(format: "%K CONTAINS %#", keyPath, searchString)
operationsRequest.returnsObjectsAsFaults = false
operationsRequest.sortDescriptors = [sortDescriptor]
operationsRequest.predicate = operationsPredicate
fetchedResultController = NSFetchedResultsController(fetchRequest: operationsRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
do {
try fetchedResultController.performFetch()
} catch {
print(error.localizedDescription)
}
}

How to reload TableView in other View?

I have some CoreData base wich I'm used in my TableView.
When I'm tried to clear those base in other View I have a message in my console log.
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
for deleting CoreData Array I'm used this code
self.historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
I know I need to reload TableView, but how can I do this in other View?
ill tried this, but this code don't work.
var tableViewHistoryClass = HistoryView()
self.tableViewHistoryClass.tableView.reloadData()
Please help me to fix this message.
You can achieve this by using notification.
create observer in viewDidLoad method where you can display your table view data.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"refreshTableView", name: "reloadTable", object: nil)
}
func refreshTableView () {
self.tableView.reloadData()
}
Second view controller
-> In this view controller you can change your data( if you want to do) or send data object
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil)
so like this it will reload your table view.
One solution is to notify your tableview when data is removed.
When data is removed your code post notifications :
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
NSNotificationCenter
.defaultCenter()
.postNotificationName("dataDeleted", object: self)
}
}
And in controller where is your tableview add an observer for this notification:
override func viewDidLoad() {
NSNotificationCenter
.defaultCenter()
.addObserver(
self,
selector: #selector(viewController.reloadTableView),
name: "dataDeleted",
object: nil)
}
func reloadTableView() {
self.tableview.reloadData
}
Thanks all for answers!
I'm created new method, all my Clear CoreData function i added to my View in which i have TableView for showing all data from CoreData :P
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"clearCoreDataArray", name: "clearAllData", object: nil)
}
func clearCoreDataArray() {
historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do
{
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
self.tableView.reloadData()
}
and in View when I'm need to use this method i use this code
NSNotificationCenter.defaultCenter().postNotificationName("clearAllData", object: self)
now i don't have any CoreData warnings

NSFetchedResultsController calls delegate when data is deleted but not when data is inserted

I am developing a journaling app using Core Data and iCloud. A simplified view of the Core Data model looks as follows:
[Journal] <-->> [Post] <-->> [Content]
I use NSFetchedResultControllers to populate a UITableView with 'Content'-, and a UICollectionView with 'Journals' from the model.
The table view works perfectly and the fetched results controller's delegate is called whenever content changes, is added or deleted. In the collection view controller however, the fetched results controller is not calling the delegate when new journals are added or the posts or content within them changes (i realise the last part might be expected since the journal object itself doesn't change). However the delegate is called when journals are deleted.
I want the delegate to be called whenever journals are inserted, and have wrestled with this issue for hours without luck or further insight.
The fetched results controller is initialized and configured as follows:
// property declaration
lazy var fetchedResultsController: NSFetchedResultsController = self.initialFetchedResultsController()
// initialization
func initialFetchedResultsController() -> NSFetchedResultsController {
let request = NSFetchRequest(entityName: "Journal")
let nameSort = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [nameSort]
return return NSFetchedResultsController(fetchRequest: request, managedObjectContext: DataModel.sharedModel().managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
}
// configuration, called in viewDidLoad()
func configureFetchedResultsController() {
fetchedResultsController.delegate = collectionViewFetchedResultsControllerDelegate
do {
try fetchedResultsController.performFetch()
print("fetching")
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}
The collectionViewFetchedResultsControllerDelegate passed to the fetchedResultsController during configuration is a standard NSFetchedResultsControllerDelegate managing the collection view of this view controller.
When data is deleted the following method is invoked in the shared data model, resulting in the fetchedResultsController calling its delegate (i simply assert this by adding some debugging print statements to all the delegate's methods).
func deleteJournal(journal: Journal) {
// delete all posts attached to journal
deletePostsForJournal(journal)
// delete journal
managedObjectContext.deleteObject(journal)
saveManagedObjectContext()
}
func deletePostsForJournal(journal: Journal) {
// delete all content attached to posts of journal
deleteContentForJournal(journal)
// delete all posts attached to journal
performBatchDeleteForJournal(journal, deleteEntityName: "Post", predicateFormat: "journal == %#")
}
func deleteContentForJournal(journal: Journal) {
// delete all content attached to posts of journal
performBatchDeleteForJournal(journal, deleteEntityName: "Content", predicateFormat: "post.journal == %#")
// delete all images attached to posts of journal
performBatchDeleteForJournal(journal, deleteEntityName: "Image", predicateFormat: "post.journal == %#")
}
private func performBatchDeleteForJournal(journal: Journal, deleteEntityName entityName: String, predicateFormat: String) {
let fetchRequest = NSFetchRequest(entityName: entityName)
fetchRequest.predicate = NSPredicate(format: predicateFormat, journal)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try persistentStoreCoordinator.executeRequest(deleteRequest, withContext: managedObjectContext)
} catch let error as NSError {
print("Could not execute batch delete fetch request \(error), \(error.userInfo)")
}
saveManagedObjectContext()
}
However, when I update the model by inserting a new journal using the insert method below, the fetchedResultsController is not being called.
func createJournal(name: String) {
// creates or returns (if already existing) a journal from the managed object context
journalForName(name)
saveManagedObjectContext()
}
func journalForName(name: String) -> Journal {
// configures fetch request
let fetchRequest = NSFetchRequest()
let entityDescription = NSEntityDescription.entityForName("Journal", inManagedObjectContext: managedObjectContext)
fetchRequest.entity = entityDescription
// configures predicate to match posts with date
fetchRequest.predicate = NSPredicate(format: "name == %#", name)
// executes fetch request
do {
let result = try self.managedObjectContext.executeFetchRequest(fetchRequest)
if let journal = result.first as? Journal {
return journal
}
} catch let error as NSError {
print("Could not execute fetch request from managedObjectContext \(error), \(error.userInfo)")
}
// creates a new journal since one doesn't exist with this name
let journal = Journal(entity: entityDescription!, insertIntoManagedObjectContext: managedObjectContext)
journal.name = name
return journal
}
The journalForName method is implemented to only create a new Journal if one with the same name doesn't exist already.
The insertion works and is updated in the model since the created Journal appears if you restart the application. I'd appreciate any help in solving this issue! Thanks.