4

I am trying to implement a tableView with CoreData. The table has four sorting ways. I had no problem implementing the first three but the forth was different as it was an entity that has a relationship. In the second View Controller where I can add items I added function that fetch existing items info and displays them it their relative cells.

The app has 2 viewControllers, one is for the tableView and the other is for adding/editing items which is being viewed by the tableView. The two classes is next :

import UIKit
import CoreData

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate{

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var segment: UISegmentedControl!

    var controller: NSFetchedResultsController<Item>!

    override func viewDidLoad() {
        super.viewDidLoad()



        tableView.delegate = self
        tableView.dataSource = self

        //self.tableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell")

        generateData()
        attemptFetchRequest()
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 150
    }

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

        if let sections = controller.sections {

            let sectionInfo = sections[section]
            return sectionInfo.numberOfObjects
        }

        return 0
    }

    func numberOfSections(in tableView: UITableView) -> Int {

        if let sections = controller.sections {

            return sections.count
        }
        return 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as! ItemCell

        configureCell(cell: cell, indexPath: indexPath as NSIndexPath)
        return cell

    }

    func configureCell (cell: ItemCell, indexPath: NSIndexPath) {
        let item = controller.object(at: indexPath as IndexPath)
        cell.configCell(item: item)
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        if let objs = controller.fetchedObjects , objs.count > 0 {
            let item = objs[indexPath.row]

            performSegue(withIdentifier: "ItemVC", sender: item)
        }
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "ItemVC" {
            if let destination = segue.destination as? ItemVC {
                if let item = sender as? Item {
                    destination.itemtoEdit = item
                }
            }
        }
    }

    func attemptFetchRequest() {

        let fetchrequest: NSFetchRequest = Item.fetchRequest()


        let dateSort = NSSortDescriptor(key: "created", ascending: false)
        let priceSort = NSSortDescriptor(key: "price", ascending: true)
        let alphabetSort = NSSortDescriptor(key: "title", ascending: true)
        let typeSort = NSSortDescriptor(key: "toItemType.type", ascending: true)

        if segment.selectedSegmentIndex == 0 {
            fetchrequest.sortDescriptors = [dateSort]
        }else if segment.selectedSegmentIndex == 1 {
            fetchrequest.sortDescriptors = [priceSort]
        }else if segment.selectedSegmentIndex == 2 {
            fetchrequest.sortDescriptors = [alphabetSort]
        }else if segment.selectedSegmentIndex == 3{
            fetchrequest.sortDescriptors = [typeSort]
        }


        let controller = NSFetchedResultsController(fetchRequest: fetchrequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
        controller.delegate = self
        self.controller = controller

        do{
            try controller.performFetch()
        } catch {
            let error = error as NSError
            print("\(error)")
        }
    }

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch (type) {
        case .insert:
            if let indexPath = newIndexPath{
                tableView.insertRows(at: [indexPath], with: .fade)
            }
            break
        case .delete:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .fade)
            }
            break
        case .update:
            if let indexPath = indexPath {
                >>let cell = tableView.cellForRow(at: indexPath) as! ItemCell
                configureCell(cell: cell, indexPath: indexPath as NSIndexPath)
            }
            break
        case .move:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .fade)
            }
            if let indexPath = newIndexPath {
                tableView.insertRows(at: [indexPath], with: .fade)
            }
            break
        }
    }

    @IBAction func segmentChanged(_ sender: AnyObject) {
        attemptFetchRequest()
        tableView.reloadData()
    }

    func generateData() {

        let item1 = Item(context: context)
        item1.title = "Car of the cars"
        item1.price = 100000
        item1.details = "Nothing much to say, it's a crapy car, don't buy it"

        let item2 = Item(context: context)
        item2.title = "Rocket"
        item2.price = 50000
        item2.details = "It's not fast as the actual rocket, but still faster than a bicycle"

        let item3 = Item(context: context)
        item3.title = "bal bla bla"
        item3.price = 50
        item3.details = "The price talks!"

        let item4 = Item(context: context)
        item4.title = "Old is Gold"
        item4.price = 60000000
        item4.details = "It's old, but also considered as great inheritance"
    }


}

and the class for the second view controller :

import UIKit
import CoreData

class ItemVC: UIViewController, UIPickerViewDelegate,                              UIPickerViewDataSource, UIImagePickerControllerDelegate,   UINavigationControllerDelegate {

    @IBOutlet weak var storesPicker: UIPickerView!
    @IBOutlet weak var name : UITextField!
    @IBOutlet weak var price : UITextField!
    @IBOutlet weak var details : UITextField!
    @IBOutlet weak var image: UIImageView!

    var stores = [Store]()
    var types = [ItemType]()

    var itemtoEdit: Item?

    var imagePicker: UIImagePickerController!

    override func viewDidLoad() {
        super.viewDidLoad()

        if let topItem = self.navigationController?.navigationBar.topItem {

            topItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil)
        }

        storesPicker.delegate = self
        storesPicker.dataSource = self

        imagePicker = UIImagePickerController()
        imagePicker.delegate = self

        generateData()

        fetchRequest()

        if itemtoEdit != nil {
            loadData()
        }

    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {

        var returnValue = 0

        switch component {
        case 0:
            returnValue = stores.count
        case 1:
            returnValue = types.count
        default:
            break
        }

        return returnValue
    }

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 2
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {

        var returnValue : String!

        switch component {
        case 0:
            returnValue = stores[row].name
        case 1:
            returnValue = types[row].type
        default:
            break
        }

        print(returnValue)
        return returnValue
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        //update
    }

    func fetchRequest (){
        let storefetch : NSFetchRequest<Store> = Store.fetchRequest()
        let typefetch : NSFetchRequest<ItemType> = ItemType.fetchRequest()

        do {
            self.stores = try context.fetch(storefetch)
            self.types = try context.fetch(typefetch)
            self.storesPicker.reloadAllComponents()
        } catch {
            //print("fetch error")
        }
    }


    @IBAction func saveItem(_ sender: AnyObject) {

        var item : Item!

        let pic = Image(context: context)
        pic.image = image.image

        if itemtoEdit == nil {
            item = Item(context: context)
        } else {
            item = itemtoEdit
        }

        item.toImage = pic

        if let title = name.text{
            item.title = title
        }

        if let price = price.text {
            item.price = (price as NSString).doubleValue
        }

        if let details = details.text {
            item.details = details
        }

        item.toStore = stores[storesPicker.selectedRow(inComponent: 0)]

        >>item.toItemType = types[storesPicker.selectedRow(inComponent: 1)]



        ad.saveContext()

        _ = navigationController?.popViewController(animated: true)
        //dismiss(animated: true, completion: nil)
    }

    func loadData() {

        if let item = itemtoEdit {

            name.text = item.title
            price.text = "\(item.price)"
            details.text = item.details
            image.image = item.toImage?.image as? UIImage


            if let store = item.toStore {

            var index = 0

            repeat{


                if store.name == stores[index].name {

                    storesPicker.selectRow(index, inComponent: 0, animated: false)
                }

                index += 1
            } while(index < stores.count)
        }

            if let type = item.toItemType {

                var index = 0

                repeat{


                    if type.type! == types[index].type! {

                        storesPicker.selectRow(index, inComponent: 1, animated: false)
                    }

                    index += 1
                } while(index < types.count)
            }


        }
    }



    @IBAction func deleteItem(_ sender: UIBarButtonItem) {
        if itemtoEdit != nil {
            context.delete(itemtoEdit!)
            ad.saveContext()
        }
        _ = navigationController?.popViewController(animated: true)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {

        if let img = info[UIImagePickerControllerOriginalImage] as? UIImage {
            image.image = img
        }
        imagePicker.dismiss(animated: true, completion: nil)
    }

    @IBAction func setImg(_ sender: AnyObject) {

        present(imagePicker, animated: true, completion: nil)

    }

    func generateData(){

        let store1 = Store(context: context)
        store1.name = "Karfour"
        let store2 = Store(context: context)
        store2.name = "خير زمان"
        let store3 = Store(context: context)
        store3.name = "BestBuy"
        let store4 = Store(context: context)
        store4.name = "Virgin"
        let store5 = Store(context: context)
        store5.name = "Max"

        let type1 = ItemType(context: context)
        type1.type = "eletronics"
        let type2 = ItemType(context: context)
        type2.type = "food"
        let type3 = ItemType(context: context)
        type3.type = "wears"
        let type4 = ItemType(context: context)
        type4.type = "books"
        let type5 = ItemType(context: context)
        type5.type = "weapons"

        ad.saveContext()

    }

}

I tested the error and found that it and it was returning nill from the function :

let cell = tableView.cellForRow(at: indexPath) as! ItemCell

I am pretty sure about the outlets and setting the custom class the proper way so I tested and noticed that when I remove a certain line the error no longer shows up. Another thing I've noticed is that when I run the app for the first time in the simulator when no data of the app is stored it works totally fine even with the line that caused the problem before but, after that when I re-run the app the problem shows then. core data modelcore data modelcore data modelcore data modelerror I searched for the cause making the return value of the cellForRow to be will but I couldn't find anything helpful. I hope from you to help me. Thanks in advance.

SOLVED Unwrap "cell" using if let as follows :

case .update:
        if let indexPath = indexPath {
            if let cell = tableView.cellForRow(at: indexPath) as? ItemCell {
                configureCell(cell: cell, indexPath: (indexPath as NSIndexPath))
            }
        }
        break
Badr
  • 694
  • 1
  • 7
  • 26
  • What is ItemCell a XIB or UITableViewCell subclass added programatically. Why did you remove the line to registering the nib file. If you do so, if cell doesn't exist while you perform a deque you need to create the cell, i.e `let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as! ItemCell if cell == nil { cell = ItemCell(.Default, reuseIdentifier: "itemCell") }` – Sachin Vas Oct 12 '16 at 04:50
  • ItemCell is a subclass and as I wrote it worked fine in a case of removing a specific line -- refer to the line which starts with ">>" in the ItemVC. – Badr Oct 12 '16 at 04:56
  • put one line in your generateData() thats tableView.reloadData() and check your output – Himanshu Moradiya Oct 12 '16 at 05:11
  • still it gives the same error at different line – Badr Oct 12 '16 at 05:23
  • It is unclear from your example how the Core Data context's are being created and how are you inserting into it. Does both the ViewController has the same context. – Sachin Vas Oct 12 '16 at 05:28
  • it's only one context cause the second VC is just for editing or adding entities so they have to save to the same context – Badr Oct 12 '16 at 05:30
  • saveContext() methed was set in appDelegate. – Badr Oct 12 '16 at 06:18
  • `cellForRow(at:)` will return nil if the row being updated has been scrolled off screen. That might be the cause of the problem. Even if it's not, you should use optional binding to check for nil, in case the row is off screen. – pbasdf Oct 12 '16 at 09:11
  • do you mean that I should make the casting optional? – Badr Oct 12 '16 at 10:13
  • @Badr Yes: `if let cell = tableView.cellForRow(at: indexPath) as? ItemCell {configureCell(cell: cell, indexPath: indexPath as NSIndexPath)}`. – pbasdf Oct 12 '16 at 20:55
  • That requires to unwrap the cell argument in the line after so when I run it it gives also the same error at that line configureCell(cell : cell!, indexPath: (indexPath as NSIndexPath)) because cell is nil and unwrapping nil causes a crash. I just want you please to notice that the code was perfectly fine without this line : item.toItemType = types[storesPicker.selectedRow(inComponent: 1)] which is completely incomprehensible. I don't know what is the relation between a line that only stores a string in the core data with the tableView being unable to return cells?! – Badr Oct 13 '16 at 01:47
  • Check the optional binding: `cell` should already be unwrapped within the `{...}` of the `if let ...` statement. – pbasdf Oct 13 '16 at 16:54
  • I modified it as next and it worked but still I don't know why it didn't work originally for the reasons I mentioned in the question, anyway the modification is as follows: `case .update: if let indexPath = indexPath { if let cell = tableView.cellForRow(at: indexPath) as? ItemCell { configureCell(cell: cell, indexPath: (indexPath as NSIndexPath)) } } break` @pbasdf thanks for your help – Badr Oct 13 '16 at 17:44
  • Didn't read all, but if the cell if not visible, `tableView.cellForRow(at:)` should return nil. – Larme Oct 13 '16 at 18:34
  • please read the post even after I solved the problem but still don't know where does it originate from. @Larme – Badr Oct 13 '16 at 20:14
  • @Badr: I read more, and my comment seems still valid, as it seems to be also the reason given by pbasdf (I searched for "visible" previously, but it typed "off screen"). Also, off screen could be not in screen already (for instance, if view has not appeared yet, like it could be with if the viewcontroller which handled it is the delegate of the next screen for instance, compared to "scrolled off". – Larme Oct 13 '16 at 21:24
  • @Larme : Thanks for you response... ok but, if this is the case it should crash every time even I didn't add the line causing thr crash which is 'item.toItemType = types[storesPicker.selectedRow(inComponent: 1)]' but why this line does the crash and why the crash doesn't appear if I am simulating the code for the first time--like deleting it then building and running it again-- why in just specific cases the crash happens. And what's the relation between the line casing the crash and the cell being nil? – Badr Oct 13 '16 at 21:33
  • I don't speak Swift, but I remember seeing questions on SO about crashs because of bad casting, I don't know about `?` and `!`, but here someone that seems to know: http://stackoverflow.com/a/36024775/1801544 (and there seems to be a crash) For the other one, I don't know, maybe tomorrow if I got more time to check it. – Larme Oct 13 '16 at 21:43
  • @Larme thanks i appreciate that – Badr Oct 13 '16 at 21:47
  • @Badr Is the relationship from `ItemType` to `Item` to-one or to-many? – pbasdf Oct 13 '16 at 22:49
  • @Larme it's to-one – Badr Oct 13 '16 at 22:50

1 Answers1

3

Based on your description, this is what I think is happening:

You tap a cell in the ViewController, and segue to ItemVC. When you save the item, the fetched results controller delegate methods are called. In that method, you use cellForRow(at:) to get the cell that corresponds to the Item that was updated. Now, that row must be visible (albeit "behind" the ItemVC) - how else could you have tapped it? - so cellForRow(at:) returns the cell you need to configure.

That's how your code worked, and everything was apparently OK, until you added the line to update the relationship:

item.toItemType = types[storesPicker.selectedRow(inComponent: 1)]

You say that the relationship from ItemType to Item is to-one. Suppose you have a itemA which is related to the "food" ItemType. In your ViewController, you tap the cell for a different Item - itemB. In the ItemVC, you update the attributes for itemB and assign it to the "food" ItemType. But each ItemType can be related to only one Item. So the relationship from itemA to the "food" ItemType is removed. That means that the toItemType value for itemA is set to nil. So TWO Item objects are affected:

  • itemA has its toItemType set to nil, and
  • itemB has its toItemType set to the "food" ItemType.

Both of these updates are observed by the FRC, and the delegate methods are called for each. In the case of itemB, everything works fine as before - it's the cell that was tapped so it must be visible. But in the case of itemA, it might or might not be visible. If it is not visible, cellForRow(at:) returns nil, and your original code crashes. By using the optional binding on cell, you avoid the crash. The corresponding row is not updated, but never mind: it wasn't visible anyway, and when it does scroll on screen, it will be correctly configured as part of the normal processing.

The key point is that, because the relationship from ItemType to Item is to-one, potentially TWO objects are updated whenever you update in ItemVC. I think you might want to make this relationship to-many. Then adding itemB to the "food" ItemType will not have any effect on itemA: they can both be related to it.

pbasdf
  • 21,386
  • 4
  • 43
  • 75