An instance of NSFetchedResultsController requires a non-nil fetchRequest and managedObjectContext' - ios

Other people have asked a similar question, but the answers given did not help me. I am trying to create a table view with core data and I keep getting an error message that my fetchRequest and/or managedObjectContext are nil
class CustomTableViewController: UITableViewController,
NSFetchedResultsControllerDelegate {
var coreDataContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
#available(iOS 10.0, *)
fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Movie> = {
// Initiate the query
let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest() as! NSFetchRequest<Movie>
// Sort the data
fetchRequest.sortDescriptors = [NSSortDescriptor(key:"title", ascending: true)]
NSLog("Request: %#, Context: %#", fetchRequest, self.coreDataContext);
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setToolbarHidden(false, animated: false)
do {
// when view loads, run the fetch (query)
if #available(iOS 10.0, *) {
try self.fetchedResultsController.performFetch()
} else {
// Fallback on earlier versions
}
} catch {
let fetchError = error as NSError
print("Unable to Perform Fetch Request")
print("\(fetchError)")
}
}
Does anyone have any ideas what's wrong? Also, not sure if it matters, but my project is a mix of both Objective-C and Swift
Also, the NSLog("Request: %#, Context: %#", fetchRequest, self.coreDataContext) from my above code prints out:
Request: (null), Context: <NSManagedObjectContext: 0x1701db4e0>
I have an entity called Movie in my xcdatamodeld with three String attributes. I also created a class for Movie like this in its own Movie.swift file:
import UIKit
import CoreData
class Movie: NSManagedObject {
}

The problem is that your Movie class has no code for generating a fetch request.
#nonobjc public class func fetchRequest() -> NSFetchRequest<Movie> {
return NSFetchRequest<Movie>(entityName: "Movie");
}
The solution: Delete your Movie class file entirely.
In the data model, configure your Movie entity to do automatic code generation:
That will solve the problem. Xcode knows how to make the class files for an entity. So just permit it to do so, and all will be well.

Related

I want a generic fetchedResultController for CoreData to support multiple models

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.

MagicalRecord database inconsistency with NSFetchedResultsController

I use MagicalRecord for my project and in my database I have CDSong entity, which can be voted by multiple CDVoter entities.
The voters are added and deleted in background using NSManagedObjectContext.performAndWait(block:) called from a serial dispatch queue. I have an NSFetchedResultsController which fetches CDSongs and displays their voters (in this simple scenario it only prints the voters' names).
Everything would be fine, but I receive crashes occassionally in NSFetchedResultsControllerDelegate's controllerDidChangeContent method :-/ According to my analysis it seems like some invalid empty CDVoter (name = nil, votedSong = nil) objects appear in the CDSong.voters relationships. These empty voters are not returned from CDVoter.mr_findAll().
This is the code that simulates the crash (usually after <20 button clicks the app crashes because a CDVoter's name is nil). Am I doing something wrong with contexts and saving? Putting whole test code here with database and frc initialization if somebody wants to try it out, but the problematic part is in controllerDidChangeContent and buttonPressed methods. Thanks for your help :)
import UIKit
import CoreData
import MagicalRecord
class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate {
var frc: NSFetchedResultsController<NSFetchRequestResult>!
let dispatchQueue = DispatchQueue(label: "com.testQueue")
override func viewDidLoad() {
super.viewDidLoad()
self.initializeDatabase()
self.initializeFrc()
}
func initializeDatabase() {
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error)
MagicalRecord.setupCoreDataStack()
MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn)
if CDSong.mr_findFirst() == nil {
for i in 1...5 {
let song = CDSong.mr_createEntity()!
song.id = Int16(i)
}
}
NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()
}
func initializeFrc() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil)
self.frc!.delegate = self
try! self.frc!.performFetch()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
for song in controller.fetchedObjects! {
print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! }))
}
print("----");
}
#IBAction func buttonPressed(_ sender: Any) {
for _ in 1...10 {
self.dispatchQueue.async {
let moc = NSManagedObjectContext.mr_()
moc.performAndWait {
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
moc.mr_saveToPersistentStoreAndWait()
}
}
}
}
}
Note:
I tried to use MagicalRecord.save(blockAndWait:) with no success.
Ok, so I found the reason of the crashes: Although mr_saveToPersistentStoreAndWait waits until the changes are saved into rootSavingContext, it doesn't wait until they are merged into defaultContext (if they were made by a private queue context). If the rootSavingContext has been changed by another save before the main queue context merges the old changes on main thread, the merge is then corrupted (changes in NSManagedObjectContextDidSave notification don't correspond to current context state of rootSavingContext in rootContextDidSave: internal method of MagicalRecord).
Explanation of my proposed solution:
DatabaseSavingManager contains a private queue saving context, which will be used for all saves in the application (maybe a drawback if you want to use multiple saving contexts, but it's enough for my needs - saves happen in background and consistency is maintained). As #Sneak commented, there's no reason to use a background serial queue which creates multiple contexts and waits for them to finish (that's what I originally did), because NSManagedObjectContext has its own serial queue, so now I used one context which is created on main thread and thus must be always called from main thread (using perform(block:) to avoid main thread blocking).
After saving to persistent store the saving context waits for the NSManagedObjectContextObjectsDidChange notification from defaultContext, so that it knows that the defaultContext has merged the changes. That's why no other saves than using DatabaseSavingManager's saving context are allowed, because they could confuse the waiting process.
Here is the code of DatabaseSavingManager:
import Foundation
import CoreData
class DatabaseSavingManager: NSObject {
static let shared = DatabaseSavingManager()
fileprivate let savingDispatchGroup = DispatchGroup()
fileprivate var savingDispatchGroupEntered = false
fileprivate lazy var savingContext: NSManagedObjectContext = {
if !Thread.current.isMainThread {
var context: NSManagedObjectContext!
DispatchQueue.main.sync {
context = NSManagedObjectContext.mr_()
}
return context
}
else {
return NSManagedObjectContext.mr_()
}
}()
override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default())
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func save(block: #escaping (NSManagedObjectContext) -> ()) {
guard Thread.current.isMainThread else {
DispatchQueue.main.async {
self.save(block: block)
}
return
}
let moc = self.savingContext
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
func saveAndWait(block: #escaping (NSManagedObjectContext) -> ()) {
if Thread.current.isMainThread {
self.savingContext.performAndWait {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
}
}
else {
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.savingContext.perform {
block(self.savingContext)
self.saveToPersistentStoreAndWait()
group.leave()
}
}
group.wait()
}
}
fileprivate func saveToPersistentStoreAndWait() {
if self.savingContext.hasChanges {
self.savingDispatchGroupEntered = true
self.savingDispatchGroup.enter()
self.savingContext.mr_saveToPersistentStoreAndWait()
self.savingDispatchGroup.wait()
}
}
#objc fileprivate func defaultContextDidUpdate(notification: NSNotification) {
if self.savingDispatchGroupEntered {
self.savingDispatchGroup.leave()
self.savingDispatchGroupEntered = false
}
}
}
And example how to use it (no NSFetchedResultsController crashes anymore; can be called from any thread, also very frequently):
DatabaseSavingManager.shared.save { (moc) in
for song in CDSong.mr_findAll(in: moc)! {
let song = song as! CDSong
let voters = song.voters!
for voter in voters {
(voter as! CDVoter).mr_deleteEntity(in: moc)
}
for _ in 1...4 {
if arc4random()%2 == 0 {
let voter = CDVoter.mr_createEntity(in: moc)!
voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
voter.votedSong = song
}
}
}
}
Of course, this is not the most elegant solution for sure, just the first that came to my mind, so other approaches are welcome

How to notice changes on Managed Object Entity handled by Core Data?

Motivation is to get a trigger for recalculation upon changes on values of Entity.
My quick solution cited below works, but it has drawbacks. It is inefficient.
In actual App, there are tens of entities. Changes on any of them will cause unnecessary notifications. Those could be avoided, if possible.
In this example, the only EmployeeMO is interested. No other entity needs to be observed.
What is your thoughts?
let n = NotificationCenter.default
n.addObserver(self, selector: #selector(mocDidChange(notification:)),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: managedObjectContext)
#objc func mocDidChange(notification n: Notification) {
if n.isRelatedTo(as: EmployeeMO.self) {
// do recalculation
}
}
And an extension to check if the notification is related to a given managed object:
extension Notification {
public func isRelatedTo<T>(as t: T.Type) -> Bool where T: NSManagedObject {
typealias S = Set<T>
let d = userInfo as! [String : Any]
return d[NSInsertedObjectsKey] is S ||
d[NSUpdatedObjectsKey] is S ||
d[NSDeletedObjectsKey] is S ||
d[NSRefreshedObjectsKey] is S ||
d[NSInvalidatedObjectsKey] is S
}
}
Xcode 9 Beta, Swift 4
Thanks.
The is a built in object that does exactly this already - NSFetchedResultsController. It is designed to work with a tableview or collectionView, but can work fine without one. It is lightweight enough that it is safe to use for just one object.
Thanks to #jon, now my source code has been greatly improved.
class myBaseArrayController: NSArrayController, NSFetchedResultsControllerDelegate {
// https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller
var frc: NSFetchedResultsController<NSManagedObject>?
func setupObserver() {
frc = NSFetchedResultsController(fetchRequest: defaultFetchRequest() as! NSFetchRequest<NSManagedObject>,
managedObjectContext: managedObjectContext!,
sectionNameKeyPath: nil, cacheName: nil)
frc?.delegate = self
do {
try frc?.performFetch()
}
catch {
fatalError("...")
}
}
}
Every ArrayController for its corresponding entity simply implements controllerDidChangeContent().
class myOneOfThemArrayController: myBaseArrayController {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("controllerDidChangeContent: \(controller.fetchRequest.entityName)")
// do some work
}
}
No more comparison to find which is what for. :-)

Refresh Core Data in ViewController when Modal View (2nd view) is Dismissed - Swift

I'm trying to figure out how to reload my UIViewController after I dismiss a Modal View. What's happening is I segue from View 1 (my UIVIewController) to a Modal View where I make an update to Core Data. Upon completion, I save the Core Data and dismiss the Modal View, sending the user back to View 1 (the UIViewController). Problem is the UIViewController is not pulling the updated change to the Core Data (but instead is presenting the old information, because it has not been refreshed).
This was the closest answer I think that could work, but I'm having trouble translating from Objective-C to Swift.
Any ideas? Thanks in advance for the help.
Here is quick NSFetchedResultsController example
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
do {
try fetchedResultsController.performFetch()
} catch {
print("An error occurred")
}
}
private lazy var fetchedResultsController: NSFetchedResultsController = {
let animalsFetchRequest = NSFetchRequest(entityName: "Animal")
let sortDescriptor = NSSortDescriptor(key: "classification.order", ascending: true)
animalsFetchRequest.sortDescriptors = [sortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: animalsFetchRequest,
managedObjectContext: self.context,
sectionNameKeyPath: nil,
cacheName: nil)
frc.delegate = self
return frc
}()
// delegate method
func controllerDidChangeContent(controller: NSFetchedResultsController) {
// update UI
}
My suggestion for this issue is to create delegate that will notify View 1.
For instance:
in presented view controller create delegate:
protocol NotifyReloadCoreData
func notifyDelegate()
end
create property of view controller:
var delegate: NotifyReloadCoreData?
when press save or something like that :
delegate.notifyDelegate()
in your View 1
class UIViewController1: UIViewController, NotifyReloadCoreData
and implement function
func notifyDelegate(){
// reload core data here
}

NSObjectInaccessibleException - Deleting all entity objects - NSFetchedResultsController

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

Resources