Search code examples
swiftuitableviewcore-datansfetchedresultscontroller

Displaying data in sections in a Table View using Core Data in Swift


I have some troubles with displaying data grouped in sections in a table view using Core Data in Swift.

I will illustrate the problem using an example. Lets assume three classes: Order, Product and Service. Class Order has an attribute "date" and to-many relationships with Product and with Service.

For each order, a section should be created to display all products and services ordered at a specific date.

With NSFetchedResultsController all orders are fetched based on the atttribute date. This provides the correct number of sections. However, I encounter problems to display all products and services in seperate rows per section since "numberOfRowsInSection" is based on the number of orders and not on the number of products and services.

Below is the code to illustrate my problem. Hopefully, anybody could help me to the right direction.

Thanks in advance,

Gerard

import UIKit
import CoreData

class ExampleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {

@IBOutlet weak var tableView: UITableView!

let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()

var orders = [Order]()

override func viewDidLoad() {
    super.viewDidLoad()
    fetchedResultController.delegate = self
    tableView.dataSource = self
    tableView.delegate = self

    fetchData()
}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return fetchedResultController.sections?.count ?? 0
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let sectionInfo = fetchedResultController.sections![section] as NSFetchedResultsSectionInfo
    return sectionInfo.numberOfObjects
    // Unfortunatelly, this provides not the total number of products and services per section
}

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

    let order = orders[indexPath.row] // Data fetched using NSFetchedResultsController

    let products = order.products!.allObjects as! [Product] // Swift Array
    let services = order.services!.allObjects as! [Service] // Swift Array

    // Each product and each service should be displayed in separate rows. 
    // Instead, all products and services are displayed in a single row as below
    var displaytext = ""
    for product in products {
        displaytext += product.name! + "\n"
    }

    for service in services {
        displaytext += service.name! + "\n"
    }

    cell.orderLabel.text = displaytext // display text in ExampleTableViewCell

    return cell
}

func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    if let sections = fetchedResultController.sections {
        let currentSection = sections[section]
        return currentSection.name
    }
    return nil
}


func orderFetchRequest() -> NSFetchRequest {
    let fetchRequest = NSFetchRequest(entityName: "Order")
    let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
    let predicate = NSPredicate(format: "date >= %@ AND date <= %@", startDate, endDate) // startDate and endData are defined elsewhere

    fetchRequest.sortDescriptors = [sortDescriptor]
    fetchRequest.predicate = predicate

    return fetchRequest
}

func fetchData() {
    let fetchRequest = orderFetchRequest()
    fetchedResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "date", cacheName: nil)

    do {
        orders = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Order]
    }
    catch let error as NSError {
        print("Could not fetch \(error), \(error.userInfo)")
    }

    do {
        try fetchedResultController.performFetch()
    }
    catch _ {
    }
}

}


Solution

  • As Wain has said, the FRC doesn't help much. An FRC has two main advantages, over and above an ordinary fetch: it can manage the sections for you, and it can automatically handle inserts/deletes/updates, via its delegate methods. The first is of no use in your case, since you want one section per Order. And the second is of little help since the FRC will watch only one Entity for changes, yet your table view is built from three.

    Nevertheless, I think you might be able to use an FRC to some effect. First, do not bother with sectionNameKeyPath: you have a one-one mapping between Orders and table view sections, so you can use the tableview's section as an index on the FRC's fetchedObjects to identify the order for each section. The numberOrRowsInSection can then be found by summing the count of products and services for the relevant Order. The messy (and possibly slow) bit is mapping the table view's row to the correct element of either products or services.

    import UIKit
    import CoreData
    
    class ExampleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
    
        @IBOutlet weak var tableView: UITableView!
    
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
        var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()
    
        var orders = [Order]()
        var startDate : NSDate = NSDate()
        var endDate : NSDate = NSDate()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            fetchedResultController.delegate = self
            tableView.dataSource = self
            tableView.delegate = self
    
            fetchData()
        }
    
        func numberOfSectionsInTableView(tableView: UITableView) -> Int {
            return fetchedResultController.fetchedObjects!.count
        }
    
        func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            let order = fetchedResultController.fetchedObjects![section] as! Order
            return ((order.products?.count ?? 0) + (order.services?.count ?? 0))
        }
    
        func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let textCellIdentifier = "ExampleTableViewCell"
            let row = indexPath.row
    
            let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath) as! ExampleTableViewCell
    
            let order = fetchedResultController.fetchedObjects![indexPath.section] as! Order // Data fetched using NSFetchedResultsController
            let products = (order.products?.allObjects ?? [Product]()) as! [Product] // Swift Array
            let services = (order.services?.allObjects ?? [Service]()) as! [Service] // Swift Array
    
            if (row < products.count) { // this is a Product row
                cell.orderLabel.text = products[row].name!
            } else { // this is a Service row
                cell.orderLabel.text = services[row-products.count].name!
            }
            return cell
        }
    
        func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
            let order = fetchedResultController.fetchedObjects![section] as! Order
            return "\(order.date)"
        }
    
    
        func orderFetchRequest() -> NSFetchRequest {
            let fetchRequest = NSFetchRequest(entityName: "Order")
            let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
            let predicate = NSPredicate(format: "date >= %@ AND date <= %@", startDate, endDate) // startDate and endData are defined elsewhere
    
            fetchRequest.sortDescriptors = [sortDescriptor]
            fetchRequest.predicate = predicate
    
            return fetchRequest
        }
    
        func fetchData() {
            let fetchRequest = orderFetchRequest()
            fetchedResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath:nil, cacheName: nil)
    
            do {
                try fetchedResultController.performFetch()
            }
            catch let error as NSError {
                print("Could not fetch \(error), \(error.userInfo)")
            }
        }
    }
    

    There are probably neater ways to code this; I'm not a Swift aficionado.

    If you use the FRC delegate methods, you can at least watch for new/deleted Orders and add/delete sections to the TV as necessary, and you could use updates to reload the relevant section.