How to create alphabetical section headers with NSFetchedResultsController - Swift 3 - ios

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

Related

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

multiple sections with different order using 1 NSFetchedResultsController

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

Core data query returns correct values but table cell is empty

I am fetching data from Core Data, the returned data is correct when printed in the console. But the tableview always returns empty cells. The object exists, numberOfRowsinSection is returned with the proper count as well.
Been looking for hours, I hope its not a typo. Any help is appreciated, the code is below. I tried both valueForKey and valueForKeypath with no success
Thanks!
import UIKit
import CoreData
class HistoryViewController: UITableViewController{
#IBOutlet var historyTableView: UITableView!
var activitiesHistory: [NSManagedObject] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Past Workouts"
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "historyCell")
let entity = NSEntityDescription.entity(forEntityName: "Activity", in: CoreDataStack.context)
let fetchRequest: NSFetchRequest<Activity> = Activity.fetchRequest()
fetchRequest.entity = entity
//let sortDescriptor = NSSortDescriptor(key: #keyPath(Activity.timestamp), ascending: true)
//fetchRequest.sortDescriptors = [sortDescriptor]
do {
activitiesHistory = try CoreDataStack.context.fetch(fetchRequest)
//print(activitiesHistory)
} catch let error{
//handle error
print(error)
}
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return activitiesHistory.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let workout = activitiesHistory[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "historyCell", for: indexPath)
print(workout.value(forKey: "timestamp"))
cell.textLabel?.text = workout.value(forKey: "timestamp")as? String
//cell.textLabel?.text = "test"
return cell
}
}
This is worked for me.
var activities: [Activity] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
addData()
let app = UIApplication.shared.delegate as! AppDelegate
let context = app.persistentContainer.viewContext
do {
activities = try context.fetch(Activity.fetchRequest())
}
catch {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.activities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = self.activities[indexPath.row].timestamp
return cell
}
Result
Thanks for all your help! I found the solution:
The problem was that the data returned from the DB as of type "Date". The cell would simply show blank space and Xcode would show a warning or error. When I used DateFormatter() to convert the type to a formatted string, the cells displayed the data. Code below:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let workout = activities[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "historyCell", for: indexPath)
let time = workout.value(forKey: "timestamp")
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM"
let formatteddate = formatter.string(from: time as! Date)
cell.textLabel?.text = formatteddate
return cell
}

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