Search code examples
iosuitableviewcore-dataswift3nsfetchedresultscontroller

Single table view through two different NSFetchedResultsControllers with sections


Good morning to everyone. I am using Swift 3.1.1, Xcode 8.3.2. I need to connect a single Table View to two different tables (entities) in Core Data through two different NSFetchedResultsControllers. I have created two NSFetchedResultsControllers, and even fetched data from table but I faced problem how to tell Table View that first controller should response for section one and second controller should be responsible for section two. I can show you the code:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tv: UITableView!

    @IBAction func pressed(_ sender: UIButton) {
        ModelA.read { table1 in
            ModelB.read { table2 in
                if table1.isEmpty {
                    ModelA.save(recordToSave: [(a: 1, b: "a"), (a: 2, b: "b"), (a: 3, b: "c")]) {
                        ModelB.save(recordToSave: [(a: 4, b: 5.0, c: true), (a: 6, b: 7.0, c: false)]) {
                            self.tvReload()
                        }
                    }
                } else {
                    self.tvReload()
                }
            }
        }
    }

    var fetchedResultsControllerForModelA = CoreDataFunctions.fetchedResultsController
    var fetchedResultsControllerForModelB = CoreDataFunctions.fetchedResultsController

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        tvReload()
    }

    func modelOfTableA(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
        if let fetchedResultsControllerForModelA = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelA, indexPath: indexPath) {
            if let model = ModelA.read(nsmanagedobject: fetchedResultsControllerForModelA) {
                return (forLabel1: "\(model.a)", forLabel2: model.b, forLabel3: "")
            }
        }
        return nil
    }

    func modelOfTableB(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
        if let fetchedResultsControllerForModelB = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelB, indexPath: indexPath) {
            if let model = ModelB.read(nsmanagedobject: fetchedResultsControllerForModelB) {
                return (forLabel1: "\(model.a)", forLabel2: "\(model.b)", forLabel3: "\(model.c)")
            }
        }
        return nil
    }

    func tvReload() {
        fetchedResultsControllerForModelA = CoreDataFunctions(tableName: .a).fetchedResultsController(keyForSort: ModelA.a.rawValue, searchParameters: nil)
        fetchedResultsControllerForModelB = CoreDataFunctions(tableName: .b).fetchedResultsController(keyForSort: ModelB.a.rawValue, searchParameters: nil)

        do {
            try fetchedResultsControllerForModelA?.performFetch()
            try fetchedResultsControllerForModelB?.performFetch()

            DispatchQueue.main.async {
                self.tv.reloadData()
            }
        } catch {
            print("Error")
        }
    }

    func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let sections1 = fetchedResultsControllerForModelA?.sections {
            if let sections2 = fetchedResultsControllerForModelB?.sections {
                return sections1[section].numberOfObjects + sections2[section].numberOfObjects
            }
            return sections1[section].numberOfObjects
        }
        return 0
    }

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

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

        if indexPath.section == 0 {
            if let modelOfTable = modelOfTableA(indexPath: indexPath) {
                cell.l1.text = modelOfTable.forLabel1
                cell.l2.text = modelOfTable.forLabel2
                cell.l3.text = modelOfTable.forLabel3
            }
        } else {
            if let modelOfTable = modelOfTableB(indexPath: indexPath) {
                cell.l1.text = modelOfTable.forLabel1
                cell.l2.text = modelOfTable.forLabel2
                cell.l3.text = modelOfTable.forLabel3
            }
        }

        return cell
    }

}

I could not find any tutorial on this theme, so I am asking question there. I do not want to use inheritance from single entity in Core Data, because, in real life it would be impossible. Thank you for any help or advice!


Solution

  • OK - I downloaded your code, and there are a couple issues...

    1) If you want your data to fill two sections in the table, the table must have two sections - currently, you are just returning 1, so use this (although you may want/need different handling based on data retrieved):

    func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
        if fetchedResultsControllerForModelA == nil || fetchedResultsControllerForModelB == nil {
            return 0
        }
        return 2
    }
    

    2) For number of rows in each section, your code was close but not quite right...

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
        if section == 0 {
            if let sections = fetchedResultsControllerForModelA?.sections {
                return sections[0].numberOfObjects
            }
        } else {
            if let sections = fetchedResultsControllerForModelB?.sections {
                return sections[0].numberOfObjects
            }
        }
    
        return 0
    
    }
    

    3) For the actual cellForRowAtIndexPath data, again your code was close but you need to keep track of which data to get for each section...

    func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
    
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
    
        if indexPath.section == 0 {
            if let modelOfTable = modelOfTableA(indexPath: indexPath) {
                cell.l1.text = modelOfTable.forLabel1
                cell.l2.text = modelOfTable.forLabel2
                cell.l3.text = modelOfTable.forLabel3
            }
        } else {
            // Each "Table data model" has only one section... since this is the 2nd table section, we need
            //  to change the section of the index path for our call to the data model
            let dataIndexPath = IndexPath(row: indexPath.row, section: 0)
            if let modelOfTable = modelOfTableB(indexPath: dataIndexPath) {
                cell.l1.text = modelOfTable.forLabel1
                cell.l2.text = modelOfTable.forLabel2
                cell.l3.text = modelOfTable.forLabel3
            }
        }
    
        return cell
    }
    

    That should get you on your way.