2
votes

Attached at very bottom of this question is my inventory controller file. My problem is I'm getting duplicate results in all the sections. I narrowed down the reason to

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

My code in that function does not account for how many rows there are in each section. As such I'm just printing out the same duplicate results every section.

The actual question is listed after the images below...

Refer to images below:

enter image description here enter image description here

I also have the ability to change the index from my settings menu so it can index by numbers, like 0-9. Refer to image below:

enter image description here

That said, I currently load the data from Core Data. Attached is reference image of the entities I use and there relationships.

enter image description here

The Question:

My question is, how can I get the results from coreData to be sorted into the A,B,C type sections or 1,2,3 sections so that navigating the table will be simple.

My hunch is the line that says let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory] needs a sort descriptor to sort based on how I like, but how I then take the data and put into the correct array structure to split into the sections I need...I have no idea.

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

InventoryController.swift

import UIKit
import CoreData

class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate {

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

    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 moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext //convinience variable to access managed object context

    // Start DEMO Related Code
    func createInventoryDummyData(number: Int) -> Inventory{
        let tempInventory = NSEntityDescription.insertNewObjectForEntityForName("Inventory", inManagedObjectContext: moc) as! Inventory

        tempInventory.name = "Test Item # \(number)"
        tempInventory.barcode = "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()

        // 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]
            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)
                }
            }

            let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory]
            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...20 {
                    entity = createInventoryDummyData(index)
                    g_inventoryItems.append(entity)
                }

                print("finished creating entities")
            }
        }catch{
            fatalError("bad things happened \(error)")
        }

        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.

        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]? {
        //Use correct index on the side based on settings desired.
        if(g_appSettings[0].indextype=="letter"){
            return letterIndex
        }else{
            return numberIndex
        }
    }

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

        //TODO: Need to figure out how many rows for ...column A,B,C or 1,2,3 based on indexType using~
        //To do this we need to organize the inventory results into a section'ed array.

        if(g_appSettings[0].selectedStore != nil){
            return (g_appSettings[0].selectedStore?.inventories!.count)! //number of rows is equal to the selected stores inventories count
        }else{
            return g_inventoryItems.count
        }
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("InventoryTableCell", forIndexPath: indexPath) as! InventoryTableViewCell

        if(g_appSettings[0].selectedStore != nil){
            //Get the current Inventory Item & Set to the cell for reference.
            cell.inventoryItem = g_appSettings[0].selectedStore?.inventories?.allObjects[indexPath.row] as! Inventory
        }else{
            //This only happens for DEMO mode or first time.
            cell.inventoryItem = g_inventoryItems[indexPath.row]//create reference to particular inventoryItem this represents.
        }

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

        return cell

    }

    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if(g_appSettings[0].indextype == "letter"){
            return letterIndex[section]
        }else{
            return numberIndex[section]
        }

    }

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        if(g_appSettings[0].selectedStore != nil){
            if(g_appSettings[0].indextype=="letter"){
                return letterIndex.count
            }else{
                return numberIndex.count
            }
        }else{
            return 1//only one section for DEMO mode.
        }
    }

    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
    }
}

Update:: **

So I was looking around and found this article (I implemented it).

https://www.andrewcbancroft.com/2015/03/05/displaying-data-with-nsfetchedresultscontroller-and-swift/

I'm really close now to figuring it out. Only problem is I can get it to auto create the sections, but only on another field, like for example store.name, I can't get it to section it into A,B,C sections or 1,2,3.

This is my code for the fetchedResultsController using the methods described in that article.

//Create fetchedResultsController to handle Inventory Core Data Operations
    lazy var fetchedResultsController: NSFetchedResultsController = {
        let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
        let primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
        inventoryFetchRequest.sortDescriptors = [primarySortDescriptor, secondarySortDescriptor]

        let frc = NSFetchedResultsController(
            fetchRequest: inventoryFetchRequest,
            managedObjectContext: self.moc,
            sectionNameKeyPath: "store.name",
            cacheName: nil)

        frc.delegate = self

        return frc
    }()

Question is what to put for sectionNameKeyPath: now that will make it section it on A B C and I got this !

Found a stackoverflow post very similar to my issue, but need swift answer.

A-Z Index from NSFetchedResultsController with individual section headers within each letter?

Here is another similar article but all objective-c answers.

NSFetchedResultsController with sections created by first letter of a string

Update::

Found another article I think with my exact issue (How to have a A-Z index with a NSFetchedResultsController)

2

2 Answers

2
votes

Ok I figured it out, phew was this confusing and took a lot of research.

Okay, so first thing you have to do is create a transient property on the data model. In my case I called it lettersection. To do this in the entity just create a new attribute and call it lettersection and in graph mode if you select it (double click it), you will see option in inspector for 'transient'. This means it won't be saved to the database and is used more for internal reasons.

You then need to manually set up the variable in the extension area of the model definition. Here is how it looks for me.

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?

    var lettersection: String? {
        let characters = name!.characters.map { String($0) }
        return characters[0].uppercaseString
    }

}

Once you do this, you simply call this new 'lettersection' with the fetchedResultsController like so...

let frc = NSFetchedResultsController(
            fetchRequest: inventoryFetchRequest,
            managedObjectContext: self.moc,
            sectionNameKeyPath: "lettersection",
            cacheName: nil)

and everything will work! It sorts by the name of my inventory items, but groups them by the first letters, for a nice A,B,C type list!

1
votes

"My question is, how can I get the results from coreData to be sorted into the A,B,C type sections or 1,2,3 sections so that navigating the table will be simple."

Using "Store" as your entity and property "name" to be what you want to sort the records by.

    override func viewDidLoad() { super.viewDidLoad()

        let fetchRequest = NSFetchRequest() 
        let entity = NSEntityDescription.entityForName("Store", inManagedObjectContext: managedObjectContext)
        fetchRequest.entity = entity

        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
        do { 
              let foundObjects = try managedObjectContext.executeFetchRequest(fetchRequest)

              locations = foundObjects as! [Location]
        } catch {
              fatalCoreDataError(error) }
  }

You are going to use this function to set the number of sections:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

return letterindex.count // if that is how you want to construct sections
}

I learned this from the Ray Wenderlich e-book "iOS Apprentice". From Lesson 3 - MyLocations. Highly recommend this and their e-book book on CoreData.