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 _ {
}
}
}
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.