Search code examples
iosswiftcore-datansfetchedresultscontrollernsfetchrequest

NSFetchedResultsController Sort Descriptor for Last Character Of String


How do you set a NSSortDescriptor which will sort by an attribute (but the last character of it?)

For example, if I have the following barcodes...

0000000005353

0000000000224

0000000433355

It should sort using last character, in asc or desc order. So like 3,4,5 in this example. Which would create section headers 3,4,5.

The current code I have gives me an error, sayings the "fetched object at index 7 has an out of order section name '9'. Objects must be sorted by section name. Which tells me I messed up the sort. To understand more please look at the code as I'm using transient properties on the core data model.

The idea is that "numberendsection", should sort from the end of the number as I described previously.

The other two sorts I describe work perfectly right now.

Inventory+CoreDataProperties.swift

import Foundation
import CoreData

extension Inventory {

    @NSManaged var addCount: NSNumber?
    @NSManaged var barcode: 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?.uppercaseString
    }

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

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

}

InventoryController.swift - (showing only relevant part)

import UIKit
import CoreData
import Foundation

class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {

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

    func setFetchedResultsController() -> NSFetchedResultsController{
        let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")

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

        if(g_appSettings[0].indextype=="numberfront"){
            primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
        }else if(g_appSettings[0].indextype=="numberback"){
            primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
        }

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

        inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]

        let storefilter = g_appSettings[0].selectedStore!
        let predicate = NSPredicate(format: "store = %@", storefilter) //This will ensure correct data relating to store is showing

        inventoryFetchRequest.predicate = predicate

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

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

        frc.delegate = self

        return frc
    }

Entity Diagram

enter image description here

enter image description here

Entity + Core Data Screenshot

enter image description here

Screenshot of Error and Code where it occurs

enter image description here

Inventory.swift

enter image description here

** Inventory.swift Entire File **

import UIKit
import CoreData
import Foundation

class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {

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

    func setFetchedResultsController() -> NSFetchedResultsController{
        let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")

        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: "barcode", ascending: true)
        }

         print("set primarySortDescriptor")

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

        inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]

        print("set sort descriptors to fetch request")

        var storefilter : Store? = nil

        if(g_appSettings[0].selectedStore != nil){
            storefilter = g_appSettings[0].selectedStore
            let predicate = NSPredicate(format: "store = %@", storefilter!) //This will ensure correct data relating to store is showing
            inventoryFetchRequest.predicate = predicate
        }

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

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

        print("set the frc")

        frc.delegate = self

        return frc
    }

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



    var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext //convinience variable to access managed object context

    // 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 tempInventory = NSEntityDescription.insertNewObjectForEntityForName("Inventory", inManagedObjectContext: 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 = "\(number)00000000\(number)"
        tempInventory.currentCount = 0
        tempInventory.id = number
        tempInventory.imageLargePath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
        tempInventory.imageSmallPath = "http://website.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 aRandomInt = Int.random(0...2)
        tempInventory.setValue(g_storeList[aRandomInt], forKey: "store") //assigns inventory to one of the stores we created.

        return tempInventory
    }

    func createStoreDummyData(number:Int) -> Store{
        let tempStore = NSEntityDescription.insertNewObjectForEntityForName("Store", inManagedObjectContext: moc) as! Store

        tempStore.address = "100\(number) lane, Miami, FL"
        tempStore.email = "store\(number)@centraltire.com"
        tempStore.id = number
        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()

        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(entityName: "Inventory")
        let storeFetchRequest = NSFetchRequest(entityName: "Store")

        do {
            let storeRecords = try moc.executeFetchRequest(storeFetchRequest) as? [Store]
            //Maybe sort descriptor here? But how to organize into sectioned array?

            if(storeRecords!.count<=0){
                g_demoMode = true
                print("No store entities found.  Demo mode = True.  Creating default store entities...")

                var store : Store //define variable as Store type

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

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

            let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory]
            //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...")

                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(realIndex)
                    g_inventoryItems.append(entity)
                }

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

                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.

        //Look at the selected Store & Use the LIST of Inventory Under it.

        //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 prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        // 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.destinationViewController as! InventoryItemController
            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 sectionIndexTitlesForTableView(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, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("InventoryTableCell", forIndexPath: indexPath) as! InventoryTableViewCell

        let inventory = fetchedResultsController.objectAtIndexPath(indexPath) as! Inventory
        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 numberOfSectionsInTableView(tableView: UITableView) -> Int {

        if let sections = fetchedResultsController.sections {
            return sections.count
        }

        return 0
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        //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, cellForRowAtIndexPath: indexPath) as? InventoryTableViewCell
            self.performSegueWithIdentifier("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) {
        print("text is changing")
    }

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

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

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

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

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

        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: Range<Int> ) -> Int
    {
        var offset = 0

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

        let mini = UInt32(range.startIndex + offset)
        let maxi = UInt32(range.endIndex   + offset)

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

NOTE::

I've cleared phone database also, just in case it was old database by deleting the app (holding down till it wiggles and deleting).


Solution

  • When your persistent store for Core Data is stored in SQLite (which I am assuming here otherwise the other answers would have worked already) you can't use computed properties or transient properties.

    However, you can alter your data model so that you are storing the last digit of that bar code in its own property (known as denormalizing) and then sort on that new property. That is the right answer.

    You can also do a secondary sort after you have done a fetch. However that means that you are holding a sorted array outside of the NSFetchedResultsController and you will then need to maintain the order of that array as you receive delegate callbacks from the NSFetchedResultsController. This is the second best answer.

    If you can change the data model, then add a sort property. Otherwise your view controller code will be more complex because of the second sort.