Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FetchedResultsController Swift 3 API Misuse: Attempt to serialize store access on non-owning coordinator

I'm attempting to use a fetchedResultsController to handle the results in my UITable.

It works initially when the program starts up. Then when I switch back to the inventory tab where my table is (for the viewToAppear again), this is when it crashes.

I'm getting a runtime crash error in my viewWillAppear() method of the window which has the table.

In particular it crashes on the Inventory+CoredataProperties.swift file on this line let characters = name!.characters.map { String($0) }, but I suspect the error is somewhere else as this works initially so why not now on the 2nd reload?

Here is the function.

override func viewWillAppear(_ animated: Bool) {
        print("view appearing")
        //When the view appears its important that the table is updated.

        //Trigger Event on SearchBar in case returning from BarCode Scanner
//        self.searchBar:SearchBar textDidChange:recentlySearchedWord;
        //searchBar.performSelector(":textDidChange")

        //Perform another fetch again to get correct data~
        do {
            //fetchedResultsController. //this will force setter code to run again.
            print("attempting fetch again, reset to use lazy init")
            fetchedResultsController = setFetchedResultsController() //sets it again so its correct.

            try fetchedResultsController.performFetch()
        } catch {
            print("An error occurred")
        }


        inventoryTable.reloadData()//this is important to update correctly for changes that might have been made
    }

The error occurs on the try fetchedResultsController.performFetch() statement. I'm getting a lot of errors before the actual crash occurs saying "API Misuse: Attempt to serialize store access on non-owning coordinator (PSC = 0x170265300, store PSC = 0x0). I've been refactoring my code to work with the new swift 3 standards I have a feeling I did something wrong or maybe something changed with how the fetched results controller works.

Any help is appreciated as to what could be the cause?

If you think I'm missing a file you need to see, just let me know and I'll add it to the relevant source code below.

POSSIBLE RELEVANT SOURCE CODE BELOW:

InventoryController.swift (Entire File)

import UIKit
import CoreData
import Foundation

class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
    @available(iOS 2.0, *)

    //Create fetchedResultsController to handle Inventory Core Data Operations
    lazy var fetchedResultsController: NSFetchedResultsController<Inventory> = {
        return self.setFetchedResultsController()
    }()

    //Reference to search text for filtering
    var m_searchText = ""

    func setFetchedResultsController() -> NSFetchedResultsController<Inventory>{

        let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        let inventoryFetchRequest : NSFetchRequest<Inventory> = Inventory.fetchRequest()

        var primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)//by default assume name.

        print("primarySortDescriptor...")

        if(g_appSettings[0].indextype=="numberfront"){
            primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
        }else if(g_appSettings[0].indextype=="numberback"){
            primarySortDescriptor = NSSortDescriptor(key: "barcodeReverse", ascending: true)
        }else if(g_appSettings[0].indextype=="numberfourth"){
            primarySortDescriptor = NSSortDescriptor(key: "barcodeFourth", ascending: true, selector: #selector(NSString.localizedCompare(_:)))
        }

         print("set primarySortDescriptor")

        //let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)

        inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]

        print("set sort descriptors to fetch request")

        var storefilter : Store? = nil
        var predicate: NSPredicate

        //Store should never be set to nil, the first store should always be selected by default.  For fringe cases just in case ... support added so doesn't break
        if(g_appSettings[0].selectedStore != nil){

            storefilter = g_appSettings[0].selectedStore
            predicate = NSPredicate(format: "store = %@", storefilter!) //default predicate assuming store is selected

            //However if search text is present then modify predicate
            if(m_searchText != ""){
                predicate = NSPredicate(format: "store = %@ AND name contains[cd] %@ OR store = %@ AND barcode contains[cd] %@", storefilter!,m_searchText,storefilter!,m_searchText)

            }
            //This will ensure correct data relating to store is showing (and if any filters present, them as well)

            inventoryFetchRequest.predicate = predicate
        }else{
            if(m_searchText != ""){
                predicate = NSPredicate(format: "name contains[cd] %@ OR barcode contains[cd] %@",m_searchText,m_searchText)
                inventoryFetchRequest.predicate = predicate
                //This will ensure correct data relating to store is showing
            }
        }

        //default assume letter section
        var frc = NSFetchedResultsController(
            fetchRequest: inventoryFetchRequest,
            managedObjectContext: moc,
            sectionNameKeyPath: "lettersection",
            cacheName: nil)

        if(g_appSettings[0].indextype=="numberfront"){
            frc = NSFetchedResultsController(
                fetchRequest: inventoryFetchRequest,
                managedObjectContext: moc,
                sectionNameKeyPath: "numbersection",
                cacheName: nil)
        }else if(g_appSettings[0].indextype=="numberback"){
            frc = NSFetchedResultsController(
                fetchRequest: inventoryFetchRequest,
                managedObjectContext: moc,
                sectionNameKeyPath: "numberendsection",
                cacheName: nil)
        }else if(g_appSettings[0].indextype=="numberfourth"){
            frc = NSFetchedResultsController(
                fetchRequest: inventoryFetchRequest,
                managedObjectContext: moc,
                sectionNameKeyPath: "numberfourthsection",
                cacheName: nil)
        }


        print("set the frc")

        frc.delegate = self

        return frc
    }

    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var inventoryTable: UITableView!

    // Start DEMO Related Code
    var numberIndex = ["0","1","2","3","4","5","6","7","8","9"]
    var letterIndex = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]

    var previousNumber = -1 //used so we show A,A, B,B, C,C etc for proper testing of sections

    func createInventoryDummyData(number: Int) -> Inventory{
        let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        let tempInventory = NSEntityDescription.insertNewObject(forEntityName: "Inventory", into: moc) as! Inventory
        if(number-1 == previousNumber){
            tempInventory.name = "\(letterIndex[number-2])-Test Item # \(number)"
            previousNumber = -1//reset it again
        }else{
            tempInventory.name = "\(letterIndex[number-1])-Test Item # \(number)"
            previousNumber = number //set previous letter accordingly
        }
        tempInventory.barcode = "00000\(number+1)00\(number)"

        //special exception to demo barcode reader
        if(number==5){
            tempInventory.barcode = "0051111407592"
        }

        if(number==6){
            tempInventory.barcode = "0036000291452"
        }

        tempInventory.barcodeReverse = String(tempInventory.barcode!.characters.reversed())

        //Convert barcode into array of characters and take note if its size for indexing
        let bcArraySize = Int(tempInventory.barcode!.characters.count) - 1//for correct indexing
        var bcArray = tempInventory.barcode!.characters.map { String($0) }

        print(bcArray)
        print(bcArraySize)

        //Take the digits from the 4th one at a time and convert to strings concatenating as you go.
        let fourth = "\(bcArray[bcArraySize-3])"+"\(bcArray[bcArraySize-2])"+"\(bcArray[bcArraySize-1])"+"\(bcArray[bcArraySize])"

        print(fourth)

        //Finally convert that into a number again and set to barcodeFourth
        tempInventory.barcodeFourth = fourth

        print(tempInventory.barcodeFourth!)

        //tempInventory.barcodeFourth =
        //print(tempInventory.barcodeReverse)


        tempInventory.currentCount = 0
        tempInventory.id = number as NSNumber?
        tempInventory.imageLargePath = "http://distribution.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
        tempInventory.imageSmallPath = "http://distribution.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
        tempInventory.addCount = 0
        tempInventory.negativeCount = 0
        tempInventory.newCount = 0
        tempInventory.store_id = 1 //belongs to same store for now

        //Select a random store to belong to 0 through 2 since array starts at 0
        let lo = 0;
        let hi = 2;
        let aRandomInt = Int.random(range:lo...hi)
        tempInventory.setValue(g_storeList[aRandomInt], forKey: "store") //assigns inventory to one of the stores we created.

        return tempInventory
    }

    func createStoreDummyData(number:Int) -> Store{
        let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        let tempStore = NSEntityDescription.insertNewObject(forEntityName: "Store", into: moc) as! Store

        tempStore.address = "100\(number) lane, Miami, FL"
        tempStore.email = "store\(number)@centraltire.com"
        tempStore.id = number as NSNumber?
        tempStore.lat = 1.00000007
        tempStore.lng = 1.00000008
        tempStore.name = "Store #\(number)"
        tempStore.phone = "123000000\(number)"

        return tempStore
    }

    // End DEMO Related Code

    override func viewDidLoad() {
        super.viewDidLoad()

        let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        print("InventoryController -> ViewDidLoad -> ... starting inits")

//        // Do any additional setup after loading the view, typically from a nib.
//        print("InventoryController -> ViewDidLoad -> ... starting inits")
//
        //First check to see if we have entities already.  There MUST be entities, even if its DEMO data.
        let inventoryFetchRequest = NSFetchRequest<Inventory>(entityName: "Inventory")
        //let storeFetchRequest = NSFetchRequest(entityName: "Store")

        do {
            let inventoryRecords = try moc.fetch(inventoryFetchRequest)
            //Maybe sort descriptor here? But how to organize into sectioned array?

            if(inventoryRecords.count<=0){

                g_demoMode = true
                print("No entities found for inventory.  Demo mode = True.  Creating default entities & store...")

                //Reset the Stores
                g_storeList = [Store]()

                var store : Store //define variable as Store type

                for index in 1...3 {
                    store = createStoreDummyData(number: index)
                    g_storeList.append(store)
                }

                //save changes for inventory we added
                do {
                    try moc.save()
                    print("saved to entity")
                }catch{
                    fatalError("Failure to save context: \(error)")
                }

                var entity : Inventory //define variable as Inventory type

                for index in 1...52 {
                    let indexFloat = Float(index/2)+1
                    let realIndex = Int(round(indexFloat))
                    entity = createInventoryDummyData(number: realIndex)
                    g_inventoryItems.append(entity)
                }

                //Save the changes
                (UIApplication.shared.delegate as! AppDelegate).saveContext()

                print("finished creating entities")
            }

        }catch{
            fatalError("bad things happened \(error)")
        }




//        //perform fetch we need to do.
//        do {
//            try fetchedResultsController.performFetch()
//        } catch {
//            print("An error occurred")
//        }

        print("InventoryController -> viewDidload -> ... finished inits!")
    }

    override func viewWillAppear(_ animated: Bool) {
        print("view appearing")
        //When the view appears its important that the table is updated.

        //Trigger Event on SearchBar in case returning from BarCode Scanner
//        self.searchBar:SearchBar textDidChange:recentlySearchedWord;
        //searchBar.performSelector(":textDidChange")

        //Perform another fetch again to get correct data~
        do {
            //fetchedResultsController. //this will force setter code to run again.
            print("attempting fetch again, reset to use lazy init")
            fetchedResultsController = setFetchedResultsController() //sets it again so its correct.

            try fetchedResultsController.performFetch()
        } catch {
            print("An error occurred")
        }


        inventoryTable.reloadData()//this is important to update correctly for changes that might have been made
    }

    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
        print("inventoryItemControllerPrepareForSegueCalled")

        if segue.identifier == "inventoryInfoSegue" {
            let vc = segue.destination as! InventoryItemController
            vc.hidesBottomBarWhenPushed = true //hide the tab bar.  This prevents crashing error from being on this page then syncing & returning.
            if let cell = sender as? InventoryTableViewCell{
                vc.inventoryItem = cell.inventoryItem //sets the inventory item accordingly, passing its reference along.
            }else{
                print("sender was something else")
            }
        }

    }

//    func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
//        //This scrolls to correct section based on title of what was pressed.
//        return letterIndex.indexOf(title)!
//    }

    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        //This is smart and takes the first letter of known sections to create the Index Titles
        return self.fetchedResultsController.sectionIndexTitles
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        if let sections = fetchedResultsController.sections {
            let currentSection = sections[section]
            return currentSection.numberOfObjects
        }

        return 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "InventoryTableCell", for: indexPath as IndexPath) as! InventoryTableViewCell

        print("IndexPath=")
        print(indexPath)

        let inventory : Inventory = fetchedResultsController.object(at: indexPath as IndexPath)
        cell.inventoryItem = inventory

        cell.drawCell() //uses passed inventoryItem to draw it's self accordingly.

        return cell
    }


    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {

        if let sections = fetchedResultsController.sections {
            let currentSection = sections[section]
            return currentSection.name
        }

        return nil
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        if let sections = fetchedResultsController.sections {
            return sections.count
        }

        return 0
    }

    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        //dispatch_async(dispatch_get_main_queue()) {
        //[unowned self] in
        print("didSelectRowAtIndexPath")//does not recognize first time pressed item for some reason?
        let selectedCell = self.tableView(tableView, cellForRowAt: indexPath) as? InventoryTableViewCell

        self.performSegue(withIdentifier: "inventoryInfoSegue", sender: selectedCell)
        //}
    }


    @IBAction func BarcodeScanBarItemAction(sender: UIBarButtonItem) {
        print("test of baritem")
    }
    @IBAction func SetStoreBarItemAction(sender: UIBarButtonItem) {
        print("change store interface")
    }

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        self.barcodeTextDidChange(searchText: searchText)
    }

    func barcodeTextDidChange(searchText: String){
        print("text is changing")
        //Code to change NSFetchRequest Here~ & Reload Table

        m_searchText = searchText //sets the local variable to this class so the setFetchedResultsController() will update accordingly

        //Perform another fetch again to get correct data~
        do {
            //fetchedResultsController. //this will force setter code to run again.
            print("attempting fetch again, reset to use lazy init")
            fetchedResultsController = setFetchedResultsController() //sets it again so its correct.
            try fetchedResultsController.performFetch()
        } catch {
            print("An error occurred")
        }

        inventoryTable.reloadData()//refreshes the data~

    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        print("ended by cancel")
        searchBar.text = ""

        m_searchText = "" //set the search text accordingly back to nothing.

        //Perform another fetch again to get correct data~
        do {
            //fetchedResultsController. //this will force setter code to run again.
            print("attempting fetch again, reset to use lazy init")
            fetchedResultsController = setFetchedResultsController() //sets it again so its correct.

            try fetchedResultsController.performFetch()
        } catch {
            print("An error occurred")
        }

        inventoryTable.reloadData()//refreshes the data~

        searchBar.resignFirstResponder()
    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        print("ended by search")
        searchBar.resignFirstResponder()
    }

    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        print("ended by end editing")
        searchBar.resignFirstResponder()
    }

    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        print("DidBeginEditing")
        //searchBar.keyboardType = UIKeyboardType.NamePhonePad
    }


    @IBAction func unwindBackToInventory(segue: UIStoryboardSegue) {
        print("unwind attempt")

        let barcode = (segue.source as? ScannerViewController)?.barcode
        searchBar.text = barcode!

        barcodeTextDidChange(searchText: searchBar.text!)//force it to re-run function manually.

        print("barcode="+barcode!)

        inventoryTable.reloadData()//reload the data to be safe.

    }

}

//Extention to INT to create random number in range.
extension Int
{
    static func random(range: ClosedRange<Int> ) -> Int
    {
        var offset = 0

        if range.lowerBound < 0   // allow negative ranges
        {
            offset = abs(range.lowerBound)
        }

        let mini = UInt32(range.lowerBound + offset)
        let maxi = UInt32(range.upperBound   + offset)

        return Int(mini + arc4random_uniform(maxi - mini)) - offset
    }
}

globals.swift

import Foundation
import CoreData

//Array of Inventory & Store Core Data Managed Objects
var g_inventoryItems = [Inventory]()
var g_storeList = [Store]()
var g_appSettings = [AppSettings]()
var g_demoMode = false

Inventory+CoreDataProperties.swift

import Foundation
import CoreData

extension Inventory {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Inventory> {
        return NSFetchRequest<Inventory>(entityName: "Inventory");
    }

    @NSManaged var addCount: NSNumber?
    @NSManaged var barcode: String?
    @NSManaged var barcodeReverse: String?
    @NSManaged var barcodeFourth: String?
    @NSManaged var currentCount: NSNumber?
    @NSManaged var id: NSNumber?
    @NSManaged var imageLargePath: String?
    @NSManaged var imageSmallPath: String?
    @NSManaged var name: String?
    @NSManaged var negativeCount: NSNumber?
    @NSManaged var newCount: NSNumber?
    @NSManaged var store_id: NSNumber?
    @NSManaged var store: Store?

    //This is used for A,B,C ordering...
    var lettersection: String {
        let characters = name!.characters.map { String($0) }
        return (characters.first?.uppercased())!
    }

    //This is used for 1,2,3 ordering... (using front of barcode and using barcodeReverse)
    var numbersection: String {
        let characters = barcode!.characters.map { String($0) }
        return (characters.first?.uppercased())!
    }

    //This is used for 0000000123 ordering...(uses back number of barcode)
    var numberendsection: String {
        let characters = barcodeReverse!.characters.map { String($0) }
        return (characters.first?.uppercased())!
    }

    //This is used for 0000000 -> 0123 ordering...(uses back 4th number of barcode)
    var numberfourthsection: String {
        let characters = barcodeFourth!.characters.map { String($0) }
        //print("characters")
        //print(characters)
        return (characters.first?.uppercased())!
    }

}

Inventory.Swift

import Foundation
import CoreData


class Inventory: NSManagedObject {

// Insert code here to add functionality to your managed object subclass

}

Screenshots of Errors

enter image description here

enter image description here

like image 891
Joseph Astrahan Avatar asked Oct 10 '16 11:10

Joseph Astrahan


2 Answers

I was running into similar issue and i moved to the new CoreData api introduced in ios10. This uses the NSPersistentContainer class to create the stack and create associated contexts. This eliminates the need to manually call save or order the creation of fetch results controller.

Good blog post to read: https://useyourloaf.com/blog/easier-core-data-setup-with-persistent-containers/

My setup is a follows

create a store NSPersistentContainer

let persistentContainer = NSPersistentContainer(name: "ModelFileName");

configure settings

let url = NSPersistentContainer.defaultDirectoryURL()
let path = url.appendingPathComponent(persistentContainer.name);
description.shouldAddStoreAsynchronously = true; //write to disk should happen on background thread
self.persistentContainer.persistentStoreDescriptions = [description];

load the store

persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error {
              fatalError("Unresolved error \(error), \(error.localizedDescription)")
        }

    //configure context for main view to automatically merge changes
    persistentContainer.viewContext.automaticallyMergesChangesFromParent = true;
});

in the view controller you can access the view context by calling

persistentContainer.viewContext

if you need to make changes you can call

persistentContainer.performBackgroundTask({ (context) in ... });

or you can get a background context

let context = persistentContainer.newBackgroundContext()
context.perform({ ... })
like image 75
Ade Avatar answered Sep 18 '22 17:09

Ade


I have reviewed your all comments and contents posted here. You have not shared one file here, but the problem is occurring you are creating invalid managed objects in the context.

And then whenever you call viewWillAppear() function in InventoryViewController, it saves the context.

Finally, it synced empty records into your database. During parsing those invalid objects, it tried to parse nil value, so crashed.

Please never set default value for managed objects you are defining as properties. I hope this will clarify your issue.

like image 26
softninja Avatar answered Sep 20 '22 17:09

softninja