Hi everyone, I am working on a coin collection app, and I have recently started integrating core data into my app. My app is of the following format: I have a core data database which stores custom CoinCategory objects and each CoinCategory object has an array of Coin objects. This way, my app stores everything in terms of categories.
When I integrated the core data, I can add my first category without any errors and delete it without any problems, but when I add in a second coin category, I experience the following error:
2017-06-26 09:55:37.218 CoinCollection[18889:12839563] -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0
2017-06-26 09:55:37.219215-0400 CoinCollection[18889:12839563] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null)
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null)
2017-06-26 09:55:37.242 CoinCollection[18889:12839563] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0'
OK. I have been working on this for several days now and I have read a couple of stackoverflow threads on core data. Apparently some writers believe that there is an issue with multithreading and that people think that they should create an NSManagedContext object with concurrency type being .PrivateQueueConcurrencyType as shown in this thread: Swift CoreData: error: Serious application error. Exception was caught during Core Data change processing.
OK. I have been working on this for several days now and I have read a couple of stackoverflow threads on core data. Apparently some writers believe that there is an issue with multithreading and that people think that they should create an NSManagedContext object with concurrency type being .PrivateQueueConcurrencyType as shown in this thread: Click Here
However, I have not done any multithreading explicitly in my application, and the fact that I am able to add my first category to the coin collection app's core data, and then I experience an error when I am adding the second one makes me think that this might not be the case.
Could anyone please advise me on how to fix this error? I am sure that there are other programmers who have also faced core data frustrations and would be interested in seeing how this problem could get resolved.
I am attaching my code below for my viewcontroller and the app delegate that is running the core data. I can also show my code for the CoinCategory and Coin objects that are stored in CoreData.
First up: AppDelegate.swift:
// We customize the app, system-wide
import UIKit
import CoreData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
//we customize the navigation bar's appearance
UINavigationBar.appearance().barTintColor = UIColor(red: 0.9882, green: 0.5765, blue: 0, alpha: 1.0)
UINavigationBar.appearance().tintColor = UIColor.white
if let barFont = UIFont(name: "HelveticaNeue-Bold", size: 25.0) {
UINavigationBar.appearance().titleTextAttributes =
[NSForegroundColorAttributeName:UIColor.white, NSFontAttributeName:barFont]
}
//ensure that the status bar has light content
//throughout the entire app!
UIApplication.shared.statusBarStyle = .lightContent
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
// MARK: - Core Data stack
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: "CoinCollection")
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)")
}
}
}
}
And then for my view controller. Notice that I get a Coin object from a sub-viewcontroller that invokes this method, and that we decide whether the Coin object fits into the existing categories. If not, then we add the new Coin.
// Controls the table view controller showing the general coins (one per each category)
import UIKit
import CoreData
class CoinTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
@IBOutlet var navItem: UINavigationItem!
//this is an array of all the coins in the collection
//each row of this two-dimensional array represents a new category
var coinsByCategory: [CoinCategoryMO] = []
var fetchResultController: NSFetchedResultsController<CoinCategoryMO>!
//we sort the coins by the category and then display them in the view controller
//example includes [ [Iraq Dinar 1943, Iraq Dinar 1200], etc. etc.]
//these two buttons are supposed to allow the toggling of a boolean (isEditing) that allows
//the user to reorder the rows in the tableviewcontroller
private var cancelButton: UIBarButtonItem!
private var editButton: UIBarButtonItem!
//this label is supposed to let the user know that the collection is empty
private var collectionEmptyLabel: UILabel!
////////////////////////////////////////////////////////////////////////
override func viewDidLoad()
{
super.viewDidLoad()
//////////////////////////////////////////////////////////////////////////////////
//we now fetch the data
let fetchRequest : NSFetchRequest<CoinCategoryMO> = CoinCategoryMO.fetchRequest()
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let context = appDelegate.persistentContainer.viewContext
let sortDescriptor = NSSortDescriptor(key: "coinCategory", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchResultController.delegate = self
do
{
try fetchResultController.performFetch()
if let fetchedObjects = fetchResultController.fetchedObjects
{
self.coinsByCategory = fetchedObjects
}
}
catch
{
print(error)
}
}
//////////////////////////////////////////////////////////////////////////////////
self.cancelButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done,
target: self, action: #selector(self.toggleEditing))
self.editButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit,
target: self, action: #selector(self.toggleEditing))
if self.coinsByCategory.count > 1
{
//we have more than one category that is to represented in the tableview
//thus, we can reorder the items in the table.
//if we had only one category, then the table would not be reorderable
//and thus, there is no need for the Edit button to appear
self.navItem.setLeftBarButton(editButton, animated: true)
}
//if there is an empty area in the table view, instead of showing
//empty cells, we show a blank area
self.tableView.tableFooterView = UIView()
//we configure the row heights for the table view so that the cells are resizable.
//ALSO: should the user want to adjust the text size in "General"->"Accessibility"
//the text size in the app will be automatically adjusted for him...
if UIDevice.current.userInterfaceIdiom == .phone
{
tableView.estimatedRowHeight = 90
}
else
{
//we are on an ipad..
tableView.estimatedRowHeight = 120
}
tableView.rowHeight = UITableViewAutomaticDimension
//we remove the title of the back button as we want to see the back
//back button in the navigation controller, not the title "Coin Categories"
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
//we want the user to be able to hide the navigation bar
//while scrolling downwards in the application
collapseNavControllerIfNeeded()
//now there is a possibility that the coin collection is empty
//this function will check the needd conditions and alert the user if there is an issue
initCollectionEmptyLabel()
messageUserIfCollectionEmpty()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
//we want the user to be able to hide the navigation bar
//while scrolling downwards in the application
collapseNavControllerIfNeeded()
}
func collapseNavControllerIfNeeded()
{
//we collapse the navigation controller if we need
//the screen estate for a controller CODE
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
func toggleEditing()
{
//we toggle the editing settings for reordering the order of the tableview's rows
}
func messageUserIfCollectionEmpty()
{
//this function is to be called when the coin collection is empty,
//and the program gives a message to a user asking him if he wants to add a coin
}
func askUserIfWantToDeleteCategoryAndDeleteIfYes(rowPath: IndexPath)
{
//This function is called when the user clicks the "Delete" action
//when he/she swipes the cell to the right....
//The parameter "rowPath" indicates the index path of the row that user has swiped to delete...
}
func deleteCoinCategory(rowPath: IndexPath)
{
if 0 <= rowPath.row && rowPath.row < self.coinsByCategory.count
{
//we have just tested that the rowPath index is valid
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let context = appDelegate.persistentContainer.viewContext
let coinCategoryToDelete = self.fetchResultController.object(at: rowPath)
context.delete(coinCategoryToDelete)
appDelegate.saveContext()
}
}
}
func deleteCoin(c: Coin, indexOfSelectedCategory: IndexPath) -> Bool
{
//we have a coin that we want to delete from this viewcontroller
//and the data contained in it.
//
//the parameter indexOfSelectedCategory refers to the IndexPath of the
//row in the TableView contained in THIS viewcontroller whose category
//of coins we are modifying in this method
//
//Return value: a boolean that indicates whether a single coin has
//been deleted - meaning that the user should return to the parentviewcontroller
//
//the coinsByCategory.joined() syntax means that we have taken the 2-d array
//of the coins sorted by each category and transformed it into a 1-d array
//of all the coins in the collection
if 0 < indexOfSelectedCategory.row && indexOfSelectedCategory.row < self.coinsByCategory.count && self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.hasCoin(c: c) == true
{
//the index is valid as it refers to a category in the coinsByCategory array
//and the examined category has the coin in question
if self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.count == 1
{
//the coin "c" that we are going to delete is the only coin in the entire category
//we reduce the problem to a simpler one that has been already solved (thanks mathematicians!)
self.deleteCoinCategory(rowPath: indexOfSelectedCategory)
//we have removed the number of categories in the coin
//collection and we might not enough categories
//to cover all of the screen, meaning that we might
//make the navigation controller uncollapsable
//if we don't need the screen estate
collapseNavControllerIfNeeded()
return true
}
else
{
//there is more than one coin in the category
self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.removeCoin(c: c)
//we save the changes in the database...
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
appDelegate.saveContext()
}
return false
}
}
return false
}
func addCoin(coinToAdd: Coin)
{
//we check over each category to see if the coin can be added
for category in self.coinsByCategory
{
if category.coinCategory?.coinFitsCategory(aCoin: coinToAdd) == true
{
//we can add the coin to the category
category.coinCategory?.addCoin(newCoin: coinToAdd)
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
//we save changes to the database
appDelegate.saveContext()
//we are DONE with this function
return
}
}
}
//since the coinToAdd does not fall in the existing categories, we create a new one
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let newCategory = CoinCategoryMO(context: appDelegate.persistentContainer.viewContext)
print("DEBUG 1: newCategory is: \(newCategory).")
print("DEBUG: newCategory.coinCategory is: \(newCategory.coinCategory?.coinsInCategory)")
newCategory.coinCategory = CoinCategory(coins: [coinToAdd], categoryType: CoinCategory.CategoryTypes.COUNTRY_VALUE_AND_CURRENCY)
print("DEBUG 2: newCategory is: \(newCategory).")
print("DEBUG: newCategory.coinCategory is: \(newCategory.coinCategory?.coinsInCategory)")
print("DEBUG: newcatgory.coinCategory.coin is: \(newCategory.coinCategory?.getCoin(at: 0))")
print("Saving data to context ...")
appDelegate.saveContext()
}
}
///////////////////////////////////////////////////////////////////////////////////////
//these delegate methods controll the core data database
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
{
switch type
{
case .insert :
if let newIndexPath = newIndexPath
{
tableView.insertRows(at: [newIndexPath], with: .fade)
//since we have added a new category, we have the possibility that we need
//to display the button for re-ordering the items in the table
if coinsByCategory.count == 2 && self.navItem.leftBarButtonItem == nil //we had one category and now added the second one
{
self.navItem.setLeftBarButton(editButton, animated: true)
}
//since we have added a new category, there is a possiblity that
//we want to be able to collapse the navigation controller
collapseNavControllerIfNeeded()
//since we are adding a coin, the collection will not be empty anymore!
activateCollectionEmptyLabel(newState: false)
}
case .delete:
if let indexPath = indexPath
{
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath
{
tableView.reloadRows(at: [indexPath], with: .fade)
}
default:
tableView.reloadData()
}
if let fetchedObjects = controller.fetchedObjects
{
self.coinsByCategory = fetchedObjects as! [CoinCategoryMO]
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
tableView.endUpdates()
//since we might have changed the number of categories, we need to consider if we have enough to make it reorderable
if coinsByCategory.count <= 1 && self.navItem.leftBarButtonItem != nil
{
//we do not have enough coin categories to make the rows of the tableView reorderable
//and the left button needs to be removed
self.navItem.leftBarButtonItem = nil
}
//given that we might have changed the number of categories
//we may need to reconfigure the navigation controller
collapseNavControllerIfNeeded()
//given that we might have changed the number of categegories, we might
//have the collection empty as a result
self.messageUserIfCollectionEmpty()
}
}
I am looking forward to hearing your thoughts and thanks in advance for the help!
P.S:
Keep in mind that if in my sample code there is a function that just has comments in it that I have removed my code and replaced it with comments to reduce the post's length.