CoreData, displaying saved journeys in a tableview - ios

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

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

Grouping Data in TableView from Core Data in Swift 4

I am having trouble finding out how to group my sections by a property in my Core Data database. This is what my DB looks like here. I am trying to group my tableView by the dueDate property. I have loaded up my Attributes in an array and that is how they are displayed. I plan on customizing the headers as well, so I would like to use the standard tableView methods. Here is the code from my ViewController.
import UIKit
import CoreData
class MainTableViewController: UITableViewController {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var taskArray = [Task]()
override func viewDidAppear(_ animated: Bool) {
loadData()
}
// MARK: - Table view functions
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return taskArray.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Date"
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 65.00
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as! TaskCell
cell.nameLabel.text = taskArray[indexPath.row].name ?? "Add Items"
if taskArray[indexPath.row].dueTime == nil {
cell.timeLabel.text = ""
} else {
let timeFormatter = DateFormatter()
timeFormatter.timeStyle = .short
cell.timeLabel.text = timeFormatter.string(from: taskArray[indexPath.row].dueTime!)
}
return cell
}
// MARK: Add New Task
#IBAction func addButtonPressed(_ sender: Any) {
performSegue(withIdentifier: "newTaskSegue", sender: self)
}
// MARK: Save & Load Data
func saveData() {
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
tableView.reloadData()
}
func loadData() {
let request : NSFetchRequest<Task> = Task.fetchRequest()
let sort = NSSortDescriptor(key: "dueDate", ascending: false)
let sort2 = NSSortDescriptor(key: "dueTime", ascending: false)
request.sortDescriptors = [sort, sort2]
do {
taskArray = try context.fetch(request)
} catch {
print("Error loading data \(error)")
}
tableView.reloadData()
}
}
Any help would be much appreciated. Thanks!
You can easily group your data using NSFetchedResultsController. One parameter in the instantiation of NSFetchedResultsController specifically allows you to group your results into sections by passing the keyPath of an attribute that constitutes the predicate for section grouping.
Apple's documentation explains this pretty clearly, with example code:
override func numberOfSections(in tableView: UITableView) -> Int {
if let frc = <#Fetched results controller#> {
return frc.sections!.count
}
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = self.<#Fetched results controller#>?.sections else {
fatalError("No sections in fetchedResultsController")
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = <#Get the cell#>
guard let object = self.<#Fetched results controller#>?.object(at: indexPath) else {
fatalError("Attempt to configure cell without a managed object")
}
// Configure the cell with data from the managed object.
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let sectionInfo = <#Fetched results controller#>?.sections?[section] else {
return nil
}
return sectionInfo.name
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return <#Fetched results controller#>?.sectionIndexTitles
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
guard let result = <#Fetched results controller#>?.section(forSectionIndexTitle: title, at: index) else {
fatalError("Unable to locate section for \(title) at index: \(index)")
}
return result
}
It is generally a Good Idea(tm) to use an NSFetchedResultsController when dealing with CoreData and UITableView or UICollectionView as you get handy notifications (through a NSFetchedResultsControllerDelegate) when your data changes that allow you to insert or remove cells from your displayed view.

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

Swift3, CloudKit and UITableView returns blank table

I am having a small mental block here, I am pretty comfortable with Core Data and decided to have a delve into CloudKit for some of my Apps, however whilst the upload side was fairly basic, I am having trouble populating a simple table view.
The CKRecord is Activity and the field I would like to display is name. The print function print(actName) returns 7 times showing all the records have been counted but the table is blank and no errors.
I am sure this is something simple and i can't see the wood for the trees, so I am happy for a point in the right direction please.
Cheers
import UIKit
import CloudKit
class TableViewController: UITableViewController {
var activities = [Activity]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("counted records")
return activities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let active = activities[indexPath.row]
if let actName = active.name {
cell.textLabel?.text = actName
print(actName)
}
return cell
}
I seem to have sorted it out, user3069232, I don't think there was/is a great latency problem as the updating code was instantaneous with CloudKit Desktop. Nirav I think you were right it boiled down to not storing and then reloading. I have modified my original code as I think 'Activity' was causing problems too, the script below works well, thanks for the point in the right direction guys.
import UIKit
import CloudKit
class CoursesVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var coursesTable: UITableView!
var coursesArray: Array<CKRecord> = []
var selectedCourseIndex: Int!
override func viewDidLoad() {
super.viewDidLoad()
coursesTable.delegate = self
coursesTable.dataSource = self
fetchCourses()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coursesArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "courseCell", for: indexPath)
let courseRecord: CKRecord = coursesArray[indexPath.row]
cell.textLabel?.text = courseRecord.value(forKey: "courseVenue") as? String
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMMM yyyy"
let CSDate = dateFormatter.string(from: courseRecord.value(forKey: "courseDate") as! Date)
// let CSDetail = courseRecord.value(forKey: "courseDetail") as? String
cell.detailTextLabel?.text = CSDate
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40.0
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedCourseIndex = indexPath.row
performSegue(withIdentifier: "showMapDetail", sender: self)
}
func fetchCourses() {
let container = CKContainer.default()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Courses", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "courseDate", ascending: true)]
publicDatabase.perform(query, inZoneWith: nil) { (results, error) -> Void in
if error != nil {
print("error fetch notes \(error)")
} else {
print("Success")
for result in results! {
self.coursesArray.append(result )
}
OperationQueue.main.addOperation({ () -> Void in
self.coursesTable.reloadData()
self.coursesTable.isHidden = false
})
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

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