I am trying two update elements in collection view. When I pass just one message it works nicely
func simulate() {
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
FriendsController.createMessageWithText(text: "Here's a text message that was sent a few minutes ago...", friend: friend!, minutesAgo: 1, context: context)
//second message
FriendsController.createMessageWithText(text: "Another message to give you hard time.", friend: friend!, minutesAgo: 1, context: context)
do {
try context.save()
}
catch let error as NSError {
print("Error \(error.localizedDescription)")
}
}
but when I do same steps except passing two messages it throws error:
CoreData: error: Serious application error. Exception was caught
during Core Data change processing. This is usually a bug within an
observer of NSManagedObjectContextObjectsDidChangeNotification.
Invalid update: invalid number of items in section 0. The number of
items contained in an existing section after the update (8) must be
equal to the number of items contained in that section before the
update (6), plus or minus the number of items inserted or deleted from
that section (1 inserted, 0 deleted) and plus or minus the number of
items moved into or out of that section (0 moved in, 0 moved out).
with userInfo (null)
Maybe someone have any clue?
There are: NSFetchController and delegate function I implemented:
lazy var fetchedResultsController: NSFetchedResultsController = { () -> NSFetchedResultsController<Message> in
let fetchRequest = NSFetchRequest<Message>(entityName: "Message")
fetchRequest.sortDescriptors = [NSSortDescriptor(key:"date",ascending: true)]
fetchRequest.predicate = NSPredicate(format: "friend.name = %#", (self.friend?.name!)!)
let delegate = UIApplication.shared.delegate as! AppDelegate
let context = delegate.persistentContainer.viewContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == .insert {
collectionView?.insertItems(at: [newIndexPath!])
collectionView?.scrollToItem(at: newIndexPath!, at: .bottom, animated: true)
}
}
If you want to insert(or delete, reload) more than one cell, than you need to make it in performBatchUpdates method of UICollectionView instance. You need to call it in func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>).
Look at this example: https://github.com/NoFearJoe/Timmee/blob/master/Timmee/Timmee/Sources/Core%20layer/Database/StorageObservable.swift
or:
https://gist.github.com/tempire/debbabb2cccafa90320e
Related
I am creating a todo list with CollectionView(VC1) and FRC.
I am first displaying VC1 on my homepage(in a section of another CVC) with a limit of items set to 3(Like a preview). Then I have a button which presents VC1 fullscreen and displays the entire count of items.
So far fetch limit is working correctly. However when I am on VC1 fullscreen I can update/delete items, now when I perform collectionView.performBatchUpdates I delete/update items and both instances of the views and updated accordingly. But...
If I add 3 more items, VC1 on the home page: limit is not 3 anymore but 6.
If I delete 2 items, VC1 on the home page: is now only displaying 1 item even if in coreData I have another 20.
It seems that no matter what the limit is set to, how many ever I delete or insert the collectionView updates accordingly. Now should I recall fetch items every time?
Here is some code of how I set the limits and update collectionView
lazy var fetchedResultsController: NSFetchedResultsController<TodoItem> = {
let context = CoreDataManager.shared.persistentContainer.viewContext
let request: NSFetchRequest<TodoItem> = TodoItem.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(key: "date", ascending: false)
]
request.fetchLimit = mode == .fullscreen ? .max : 3
let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
} catch let err {
print(err)
}
return frc
}()
// Number of items in CV
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let allItems = fetchedResultsController.sections?[section].numberOfObjects ?? 0
let numberOfRows = mode == .fullscreen ? allItems : min(allItems, 3)
return numberOfRows
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
itemChanges.append((type, indexPath, newIndexPath))
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({
for change in self.itemChanges {
switch change.type {
case .insert:
self.collectionView?.insertItems(at: [change.newIndexPath!])
case .delete:
self.collectionView?.deleteItems(at: [change.indexPath!])
case .update:
self.collectionView?.reloadItems(at: [change.indexPath!])
case .move:
self.collectionView?.deleteItems(at: [change.indexPath!])
self.collectionView?.insertItems(at: [change.newIndexPath!])
#unknown default:
return
}
}
}, completion: { finished in
self.itemChanges.removeAll()
})
}
Thank you for any help in advance.
The closure of the lazy instantiated variable is executed only once.
You have to change the fetch request with appropriate fetch limit somewhere else.
I have simplified my problem to make the question easier, so hopefully the code I am posting is sufficient.
My goal is to use one class that can support multiple models from CoreData so that I can avoid recreating the same stuff for each class.
class FRC<T> where T: NSManagedObject {
let context: NSManagedObjectContext
fileprivate lazy var fetchedResultscontroller: NSFetchedResultsController<T> = { [weak self] in
guard let this = self else {
fatalError("lazy property has been called after object has been descructed")
}
guard let request = T.fetchRequest() as? NSFetchRequest<T> else {
fatalError("Can't set up NSFetchRequest")
}
request.sortDescriptors = [NSSortDescriptor(key: "key", ascending: true)]
return NSFetchedResultsController<T>(fetchRequest: request,
managedObjectContext: this.context,
sectionNameKeyPath: nil,
cacheName: nil)
}()
init(context: NSManagedObjectContext) {
self.context = context
}
}
enum ModelType {
case modelA
case modelB
}
class GenericDataModel: NSObject {
let container: NSPersistentContainer
// STUPID!!!
var frcA: FRC<ModelA>?
var frcB: FRC<ModelB>?
init(modelType: ModelType) {
container = NSPersistentContainer(name: "MyContainer")
container.loadPersistentStores { _, error in /* Handle */ }
// Below here is ugly!
switch modelType {
case .modelA:
frcA = FRC<ModelA>(context: container.viewContext)
case .modelB:
frcB = FRC<ModelB>(context: container.viewContext)
}
}
}
extension GenericDataModel: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
// Handle
}
}
It seems that the best I can do is to make multiple variables in my GenericDataModel class for different model classes, which will not scale well for a large number of them.
However, trying to make a single local variable for the FRC class keeps failing and I cannot understand why.
I am new to generics, so it is likely I am just doing something stupid.
It is not a bad idea. I already did it:
https://github.com/jon513/FetchedResultsControllerNeverCrash/blob/master/coreDataNeverCrash/FetchedResultsControllerManager.swift
This implements a method to have core data never crash when getting a delete and update at the same time (see App crashes after updating CoreData model that is being displayed in a UITableView), as well as making the api a little easier.
To use it create a FetchedResultsControllerManager with fetchRequest, context and a delegate.
The delegate implementation is easy:
extension ViewController : FetchedResultsControllerManagerDelegate {
func managerDidChangeContent(_ controller: NSObject, change: FetchedResultsControllerManagerChange) {
change.applyChanges(collectionView: collectionView)
}
}
I couldn't get the delegate's to return the correct class because of the generics stuff, but I never had to test to anything other than equally so it never bothered me. If you can get it work with generics I would love to get a pull request.
In my iOS (Swift 3, Xcode 8) Core Data app I have 2 View Controllers (CategoriesViewController and ObjectsViewController) (Both are inside the same navigation controller).
Each ViewController has it's own tableView and it's own fetchResultsController to manage the results returned from Core Data request (Fetching entities titled Category in CategoriesViewController and entities titled Object in ObjectsViewController).
In my CategoriesViewController I have this variable:
var fetchResultsController: NSFetchedResultsController<Category>!
I've added the following code to CategoriesViewController to avoid having errors when opening another view :
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
self.fetchedResultsController.delegate = nil
self.fetchedResultsController = nil
}
In CategoriesViewController I've added these methods for fetchedResultsController :
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
guard let path = indexPath
else { return }
tableView.reloadRows(at: [path], with: .automatic)
case .delete:
guard let path = indexPath
else { return }
tableView.deleteRows(at: [path],
with: .automatic)
case .insert:
guard let path = newIndexPath
else { return }
tableView.insertRows(at: [path],
with: .automatic)
case .move:
guard let _ = indexPath,
let _ = newIndexPath
else { return }
// tableView.moveRow(at: fromPath, to: toPath)
if indexPath != newIndexPath {
tableView.deleteRows(at: [indexPath!], with: .none)
tableView.insertRows(at: [newIndexPath!], with: .none)
}
}
}
To fetch Core Data objects I wrote a coreData_fetchAll_Categories(). I've placed it into a viewWillAppear method of CategoriesViewController. After that i'm reloading data of a tableView.
func coreData_fetchAll_Categories(handleCompleteFetching:#escaping (()->())) {
let context = CoreDataManager.sharedInstance.viewContext
let fetchRequest: NSFetchRequest<Category> = Category.fetchRequest()
var sortDescriptors = [NSSortDescriptor]()
let indexSortDescriptior = NSSortDescriptor(key: "indexOrder", ascending: true)
sortDescriptors.append(indexSortDescriptior)
fetchRequest.sortDescriptors = sortDescriptors
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context!, sectionNameKeyPath: nil, cacheName: nil)
self.coreDataFetchedResultsController.delegate = self
do { try self.fetchedResultsController.performFetch()
} catch {
print("performFetch() finished with error")
}
}
With the above code, after i'm returning back from my ObjectsViewController (where I also have all the methods with fetchedResultsController for Object entity and I also set fetchedResultsController to nil there in viewWillDisappear) my tableView in CategoriesViewController freezes. If I delete these 2 lines from viewWillDisappear of CategoriesViewController, everything works fine, but I need these lines to avoid another bugs.
self.fetchedResultsController.delegate = nil
self.fetchedResultsController = nil
Code in ViewWillAppear looks like this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.tableView.register(UINib.init(nibName: “CategoriesTableViewCell", bundle: nil), forCellReuseIdentifier: “categoriesTableViewCell")
self.tableView.delegate = self
self.tableView.dataSource = self
self.coreData_fetchAll_Categories {
DispatchQueue.main.async { // Submit to the main Queue async
self.tableView.reloadData()
}
}
}
After a CategoriesViewController appears a VC creates new version of fetchedResultsController (I've checked , it is not nil). Also i've noticed that tableView doesn't call cellForRowAt indexPath: . Seems Strange. delegates of tableView set to self in viewWillAppear.
I don't understand why can this bug happen, because i don't receive any errors.
Any opinions for this bug welcome. Working in Swift 3 (XCode 8).
You have to make sure that the tableview and the fetchedResultsController are never out of sync. Here are a few places that it can happen in your code:
When you nil out the fetchedResultsController you must also reload the tableview, because now it should have zero section and zero rows
coreData_fetchAll_Categoriesis already running on the main thread - there is no reason for a completion hander like that. Furthermore the use of DispatchQueue.main.async can cause real harm as there is a period of time before reloadData is called when the tableview and fetchedResultsController are out of sync
Also (unrelated to your core data problems) when you call super.viewWillDisappear and super.viewWillAppear you should pass along the animated parameter - and not always pass true
Hope that helps
I have a Swift app that I'm trying to use NSFetchedResultsController to populate four separate tableviews, which are in containers inside a main VC, using inheritance from a parent custom tableview controller class. I have a parent class with this method defined that all of my subclasses are inheriting:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
var task = fetchedResultsController.objectAtIndexPath(indexPath) as Task
if task.isKindOfClass(Task) == true {
cell.detailTextLabel?.text = task.name//EXC_BAD_ACCESS code=1
}
return cell
}
I commented the line where it is giving me the EXC_BAD_ACCESS compile error.
I know that the Task class object that inherits from NSManagedObject is successfully storing in that variable but it seems that whenever I try to access the name property (even println will cause the error), or any property on the object I get this compile error.
My hunch is that it has to do with four different tableviews trying to all access at once. I'm new to Core Data and not really sure.
EDIT
More code that might help with the problem:
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
var fetchedResultsController: NSFetchedResultsController = NSFetchedResultsController()
func getFetchedResultsController() -> NSFetchedResultsController {
fetchedResultsController = NSFetchedResultsController(fetchRequest: taskFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}
func taskFetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Task")
let sortDescriptor = NSSortDescriptor(key: "priority", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultsController = getFetchedResultsController()
fetchedResultsController.delegate = self
fetchedResultsController.performFetch(nil)
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
Screenshot of the entity inspector:
http://i.stack.imgur.com/Htyne.png
Got it working! A few things: my name property was nil because I didn't set a default value in the model inspector and needed a default value for good measure, but more importantly: my managedObjectContext wasn't unwrapped with ! in its declaration and so I didn't unwrap it on every reference to it and so sometimes I was getting the optional(managedObjectContext?) when I needed the unwrapped value. It was let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
but what it needed was let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!. Optional problems and not being aware enough of when to unwrap were my downfall. I also deleted my initial app.sqlite because I ran into an issue there after I changed the model assigning the default value to name.
I have a function responsible for deleting all the items of an entity:
func removeItems() {
if let managedContext = managedObjectContext {
let fetchRequest = NSFetchRequest()
let entity = NSEntityDescription.entityForName("Ent", inManagedObjectContext: managedContext)
fetchRequest.entity = entity
fetchRequest.includesPropertyValues = false
var error: NSError?
var results = managedContext.executeFetchRequest(fetchRequest, error: &error)
for result in results as [NSManagedObject] {
managedContext.deleteObject(result)
}
if !managedContext.save(&error) {
println("could not save \(error), \(error?.userInfo)")
}
}
}
My application consists of a TabBar with 3 screens:
The first tab presents a list of cities, and when one is selected, a segue is executed and goes to a product listing page, in which the user can "tag" products.
The second tab has a screen that shows the listing of these branded products, and also has a badge showing the amount of products
However, I need to delete all objects of this entity whenever the user selects a different city or when he starts the application after terminated.
For the first case, I delete all the objects in "prepareForSegue" function when the user selects a city, and it works perfectly.
The problem comes when I try to run the second case.
If I try to call the remove function in the "application didFinishLaunchingWithOptions" of the AppDelegate or "viewDidLoad" in the first tab, the bank is corrupted, and I get the following message when I try to enter in the second tab:
Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd000000000140000 ''
But if I remove the function of "application didFinishLaunchingWithOptions" or "viewDidLoad" the first tab, the application works perfectly.
Looking more closely, the error is occurring in the second tab (the product listing).
I have a variable in which I use to keep up the items in the table (in the second tab):
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest()
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let entity = NSEntityDescription.entityForName("Entidade", inManagedObjectContext: managedContext)
fetchRequest.entity = entity
let sortDescriptor1 = NSSortDescriptor(key: "nome", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1]
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedContext,
sectionNameKeyPath: "nome",
cacheName: "Entidade")
fetchedResultsController.delegate = self
return fetchedResultsController
}()
And the error is occurring exactly this line of the second tab:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = editButtonItem()
var error: NSError?
if !fetchedResultsController.performFetch(&error) { // <----- HERE
fatalCoreDataError(error)
}
}
Would anyone have any suggestions of what I'm doing wrong?
The problem was entirely in the second tab.
The answer to the problem was to remove the variable:
lazy var fetchedResultsController: NSFetchedResultsController = {
.
.
.
}()
Now the "viewDidLoad" the second tab was as follows (the fetch was removed):
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = editButtonItem()
}
Was added the following variable:
var entities = [Entidade]()
And added the following methods:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
fetchLog()
}
And
func fetchLog() {
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "Entidade")
var error: NSError? = nil
if let results = managedContext.executeFetchRequest(request, error: &error) as? [Entidade] {
self.entities = results
} else {
println("Could not fetch \(error), \(error!.userInfo)")
}
}
With these changes, I can finally remove the objects when the application is started by placing the following code in the cities list screen:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appDelegate.removeItens()
}
Or choose to call the "removeItems()" method in AppDelegate when the application starts or ends.
If anyone needs, I can post the entire source code of the screens.
Updated
I found out what really happened, I have a method in AppDelegate in which is responsible for updating the "badgeValue" tab of the list whenever a user marks a product.
He was as follows (and was called every time a change occurred in managedObjectContext):
func updateUI() {
let tabBarController = window!.rootViewController as UITabBarController
if let tabBarViewControllers = tabBarController.viewControllers {
let navigationController = tabBarViewControllers[3] as UINavigationController
let listViewController = navigationController.viewControllers[0] as ListViewController
listViewController.managedObjectContext = managedObjectContext // <--- Here's the problem
let fetchRequest = NSFetchRequest(entityName: "Entidade")
if let fetchResults = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) {
navigationController.tabBarItem.badgeValue = String(fetchResults.count)
}
}
}
I can not set the managedObjectContext to a screen this way, I need to assign it only once in the "application didFinishLaunchingWithOptions", so I got to keep the old code to take advantage of NSFetchedResultsController