multiple sections with different order using 1 NSFetchedResultsController - ios

I have a tableview with two sections, "favorite" and "recent" to order stored contacts.
I would like to sort the favorite alphabetically, and the recent by date.
Each contact has a "name" and a "createdAt" value (and a value "favorite" if it is a favorite).
Using the sort below I have my sections "favorite" and "recent" with the correct cells but both sorted alphabetically.
...
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "favorite", ascending: false),
NSSortDescriptor(key: "firstName", ascending: true)]
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: stack.viewContext, sectionNameKeyPath: "favorite", cacheName: nil)
...
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let currentSection = fetchedResultsController.sections?[section] else { return nil }
if currentSection.name == "0" {
return "Recent".localized
} else {
return "Favorites".localized
}
}
How can I sort the section "recent" by date?

I solved it by manually changing the indexPath of the second section.
I have a function that return indexPath ordered differently for the second section.
func changeIndexPath (_ indexPath: IndexPath) -> IndexPath {
... //sort you want
}
Then I can use the functions of the tableview
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
newIndexPath = changeIndexPath(indexPath)
...
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
}
etc

Related

NSFetchedResultsController doesn't respond to changes in the number of sections

I'm getting the following error when I delete a cell from the table view:
Invalid update: invalid number of sections. The number of sections
contained in the table view after the update (0) must be equal to the
number of sections contained in the table view before the update (1),
plus or minus the number of sections inserted or deleted (0 inserted,
0 deleted)
I'm using the NSFetchedResultsControllerDelegate to respond to the changes in the number of section. I'm retrieving the data from a plist file in Core Data and each addition of the table view cell increases the number of sections accordingly, so 3 cells = 3 sections.
override func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let goal = fetchedResultsController.object(at: indexPath)
cell.textLabel!.text = goal.title
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let goal = fetchedResultsController.object(at: indexPath)
self.context.delete(goal)
self.saveContext()
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
default:
break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let section = IndexSet(integer: sectionIndex)
switch type {
case .delete:
tableView.deleteSections(section, with: .automatic)
default:
break
}
}
context is this:
var context : NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
How I load the saved Core Data onto FRC:
func loadSavedData() {
if fetchedResultsController == nil {
let request = Goal.createFetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sort]
request.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: "title", cacheName: nil)
fetchedResultsController.delegate = self
}
fetchedResultsController.fetchRequest.predicate = goalPredicate
do {
try fetchedResultsController.performFetch()
tableView.reloadData()
} catch {
print("Fetch failed")
}
}
try calling beginUpdates and endUpdates in these delegate apis also.
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates
}
if you still face issues then in this delegate function, try to call like this:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.performBatchUpdates {
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.deleteSections(section, with: .automatic)
} completion: { (status) in
}
}

create an empty section(header view) with no cell in core data

I have two entities. user and attributes. user has a one-to-one relationship to attributes and attribute has one-to-many relationship to the user.
i am using FRC(Fetch result controller and want to use 'user' entity as section( header view) and attributes as table view cell.
I want to be able to add a user section(header view) with no cell.
but as default I get one cell per new section.
here is the code for FRC :
lazy var fetchResultController: NSFetchedResultsController<Attribute> = {
let fetchRequest: NSFetchRequest<Attribute> = Attribute.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Attribute.name), ascending: false)]
let fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.managedContext, sectionNameKeyPath: #keyPath(Attribute.user.name), cacheName: nil)
fetchResultController.delegate = self
return fetchResultController
}()
tableView:
func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchResultController.sections else {return 0}
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = fetchResultController.sections?[section] else{return 0}
return section
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ID.TableView.mainPage) as! AttributeCell
let task = fetchResultController.object(at: indexPath)
cell.nameLbl.text = task.name
return cell
}
try this
var numberSections = 0
func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchResultController.sections else {
numberSections = 0
return 0}
numberSections = sections + 1
return sections.count + 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = fetchResultController.sections?[section] else{return 0}
return section + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ID.TableView.mainPage) as! AttributeCell
let task = fetchResultController.object(at: indexPath)
//4 //5
if numberSections - 2 > indexPath.section
{
cell.nameLbl.text = task.name
}
else
{
cell.nameLbl.text = ""
}
return cell
}
this creates an empty section until the end

CoreData, displaying saved journeys in a tableview

I am trying to display a list of all journeys recorded in a fitness-style app in a table view to show the distance (boolean) and date (timestamp) of each journey.
At the moment I have just created a variable to contain the Journeys from the core data file. When I print out the journeysArray, it shows 0 in the console even though there are some recorded journeys.
import UIKit
import CoreData
class SavedJourneysViewController: UITableViewController {
var journeyArray: [Journey] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(journeyArray.count)
return journeyArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "JourneyItem", for: indexPath)
return cell
}
If Journey is your NSManagedObject subclass, you should use a NSFetchedResultsController to fetch the persisted objects.
Your SavedJourneysViewController must have a reference to the NSManagedObjectContext instance that you'll use to fetch your Journey objects. Let's assume that you have a viewContext property of type NSManagedObjectContext in your SavedJourneysViewController that's being set from the outside, wherever you initialize your SavedJourneysViewController.
You'll want to declare a fetchedResultsController in SavedJourneysViewController.
private lazy var fetchedResultsController: NSFetchedResultsController<Journey> = {
let fetchRequest: NSFetchRequest< Journey > = Journey.fetchRequest()
let sortDescriptor = NSSortDescriptor(keyPath: \Journey.date, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: viewContext, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultsController
}()
Then perform fetch in viewDidLoad (for example) by calling try? fetchedResultsController.performFetch():
Then in numberOfRowsInSection return fetchedResultsController.sections?[section].objects?.count ?? 0:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedResultsController.sections?[section].objects?.count ?? 0
}
Don't forget about implementing func numberOfSections(in tableView: UITableView) -> Int and returning fetchedResultsController.sections?.count ?? 0:
func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
In cellForRowAt, configure your cell with a Journey object:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = let cell = tableView.dequeueReusableCell(withIdentifier: "JourneyItem", for: indexPath)
guard let journey = fetchedResultsController.sections?[indexPath.section].objects?[indexPath.row] as? Journey else {
return cell
}
// handle cell configuration
cell.textLabel?.text = String(journey.distance)
return cell
}
More about using NSFetchedResultsController with UITableViewController -
https://cocoacasts.com/populate-a-table-view-with-nsfetchedresultscontroller-and-swift-3
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/nsfetchedresultscontroller.html

How to create alphabetical section headers with NSFetchedResultsController - Swift 3

I am using NSFetchedResultsController to populate a tableView. The tableView can get quite long because it shows a list of people, and I want to sort it alphabetically. I know I need to use titleForHeaderInSection, but I am stuck on how to get the first letter of each object in my fetchedObjectsController.fetchedObjects and display that as the section header as well as sort it, just how the Contacts app works.
This is what my View Controller looks like.
var fetchedResultsController: NSFetchedResultsController<Client> = {
let fetchRequest: NSFetchRequest<Client> = Client.fetchRequest()
let sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
fetchRequest.sortDescriptors = sortDescriptors
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataStack.context, sectionNameKeyPath: "name", cacheName: nil)
}()
override func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchedResultsController.sections else { return 0 }
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = fetchedResultsController.sections else { return 0 }
return sections[section].numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "clientCell", for: indexPath)
let client = fetchedResultsController.object(at: indexPath)
cell.textLabel?.text = client.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let client = fetchedResultsController.object(at: indexPath)
ClientController.sharedController.delete(client)
}
}
This is a very small example of how you can get your headers texts, I use a class only for test that have only name, then applying map using characters.prefix we get the first characters of the names and after casting to String and sorting we have what you need
var arrayOfUsers : [User] = [User(name:"test"),User(name:"pest"),User(name:"aest"),User(name:"nest"),User(name:"best")]
let finalArray = arrayOfUsers.map({String.init($0.name.characters.prefix(1)) }).sorted(by: {$0 < $1})
debugPrint(finalArray)
Console log result
["a", "b", "n", "p", "t"]
Hope this helps you

how to display 2 different prototype cells at different different sizes

UPDATE:
I went a different route. Heres what I would like to do. Design my app that lets me save core data and view it in another console in tableview. Once in the tableview console, I can also see a chart at the top of the console as well.
What I did:
I created a UIViewController, dragged over an imageview just to use that as an example. I also dragged in a tableview, cells...etc.
My Problem:
I can view the blank tableview cells and see the sample image. Once I save the core data and go back to try viewing the data, I get an error. I have the datasource and delegate implemented but, do I need to put that in my code.
class ViewMealsViewController: UIViewController, NSFetchedResultsControllerDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var menuButton: UIBarButtonItem!
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext
var fetchedResultController: NSFetchedResultsController<MealStats> = NSFetchedResultsController()
override func viewDidLoad() {
super.viewDidLoad()
fetchedResultController = getFetchedResultController()
fetchedResultController.delegate = self
do {
try fetchedResultController.performFetch()
} catch _ {
}
if revealViewController() != nil {
revealViewController().rearViewRevealWidth = 325
menuButton.target = revealViewController()
menuButton.action = #selector(SWRevealViewController.revealToggle(_:))
view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK:- Retrieve Stats
func getFetchedResultController() -> NSFetchedResultsController<MealStats> {
fetchedResultController = NSFetchedResultsController(fetchRequest: taskFetchRequest(), managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
return fetchedResultController
}
func taskFetchRequest() -> NSFetchRequest<MealStats> {
let fetchRequest = NSFetchRequest<MealStats> (entityName: "MealStats")
let timeSortDescriptor = NSSortDescriptor(key: "mealtype",
ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))
let milesSortDescriptor = NSSortDescriptor(key: "mealname",
ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:)))
fetchRequest.sortDescriptors = [timeSortDescriptor, milesSortDescriptor]
return fetchRequest
}
// MARK: - TableView data source
func numberOfSections(in tableView: UITableView) -> Int {
let numberOfSections = fetchedResultController.sections?.count
return numberOfSections!
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfRowsInSection = fetchedResultController.sections?[section].numberOfObjects
return numberOfRowsInSection!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let mealstats = fetchedResultController.object(at: indexPath) as! MealStats
cell.textLabel?.text = mealstats.mealtype
cell.detailTextLabel!.text = mealstats.mealname
return cell
}
// MARK: - TableView DeleteĆ’
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let managedObject:NSManagedObject = fetchedResultController.object(at: indexPath) as! NSManagedObject
managedObjectContext.delete(managedObject)
do {
try managedObjectContext.save()
} catch _ {
}
}
// MARK: - TableView Refresh
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.reloadData()
}
}
UIViewController with a sample image for an example
Error I get once I try to view saved core data in the tableview
Use UITableViewController
add two different cells to tableview on storyboard,
set two unique identifiers for them i.e
Cell No. 1 identifier : iden_1
Cell No. 2 identifier : iden_2
then in your class
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell
if(condition)
{
cell = tableView.dequeueReusableCell(withIdentifier: "iden_1", for: indexPath)
let stats = fetchedResultController.object(at: indexPath) as! Stats
cell.textLabel?.text = stats.type
cell.detailTextLabel!.text = stats.name
}
else{
cell = tableView.dequeueReusableCell(withIdentifier: "iden_2", for: indexPath)
let stats = fetchedResultController.object(at: indexPath) as! Stats
cell.textLabel?.text = stats.type
cell.detailTextLabel!.text = stats.name
}
return cell
}
and use this for identifying height for both cells.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if(condition)
return 100
return 200
}

Resources