How to correctly implement NSFetchedResultsController.controllerDidChange() - swift

I have a UIView which has a UITableView along with different components. Its corresponding UIViewController has the viewDidLoad() as follows:
Class Foo: UIViewController, NSFetchedResultsControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
/* some other stuff */
// An instance of a class implementing UITableViewData Source
// and NSFetchedResultsController.
// It also contains the instance of the
// NSFetchedResultsController.
self.namesTable.dataSource = self.namesTableController
self.namesTableController.fetchedResultsController.delegate = self
}
// For updating the table when its content changes - NSFetchedResultsControllerDelegate
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.namesTable.reloadData()
}
/* Other functions, etc, etc.... */
}
This arrangement works fine: when I add/remove entries to/from the table, the table reloads.
But I don't think that this is the right way of doing it, since the entire table is re-loaded every time when any change occurs. Seems like an overkill.
After looking online for several hours, I am not coming with anything. Can anyone point me to the right way of doing this? or am I doing it right?

Although there is nothing "wrong" with reloadData() it is unnecessary to call it every time one object changes, as you stated. You can update only the content you want by implementing the other NSFetchedResultsControllerDelegate functions and using beginUpdates() and endUpdates() like so:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case NSFetchedResultsChangeType.Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case NSFetchedResultsChangeType.Move:
break
case NSFetchedResultsChangeType.Update:
tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
}
}

Related

How to update the model after tableView was edited

I am learning Swift by writing a single table app view which lists the content of a Core Data table (entity) upon start-up. Then the user can reorder the rows in the table view.
I need to be able to save the newly ordered rows such that they replace the previous database table, so when the user starts the app again, the new order is shown.
The editing (re-ordering) feature is activated by a long press and calls
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
self.projectTableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
}
A second long press then inactivates the editing feature:
// Called when long press occurred
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .ended {
let touchPoint = gestureRecognizer.location(in: self.projectTableView)
if let indexPath = projectTableView.indexPathForRow(at: touchPoint) {
if self.projectTableView.isEditing == true {
self.projectTableView.isEditing = false
db.updateAll() //this is a stub
} else {
self.projectTableView.isEditing = true
}
}
}
}
The call to db.updateAll() in 'handleLongPress' above is just a blank, and I don't know how to update the database. Is there a way to read the content of the tableView in the new sequence into an array, then replace the table in the db? Feels a little "brute force" but can't see any other solution.
Ok you can achieve that in several ways :
1- Using NSFetchedResultsController , here you can automatically synchronizing changes made to your core data persistence store with a table view,
so quickly here are the steps :
Conform to NSFetchedResultsControllerDelegate
Declare an instance of NSFetchedResultsController with you core data model
Make an NSFetchRequest, call NSFetchedResultsController initializer with the request, then assign it to your instance declared before
call performFetch method on your instance
set the viewController to be the delegate
And now you can implement the delegates, here you want didChange , so something like that :
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
switch type {
/*
....
*/
case .move:
if let deleteIndexPath = indexPath {
self.tableView.deleteRows(at: [deleteIndexPath], with: .fade)
}
if let insertIndexPath = newIndexPath {
self.tableView.insertRows(at: [insertIndexPath], with: .fade)
}
}
}
2- Second option which personally i prefer it over the NSFetchedResultscontroller
You can add a property in your model (core data model). That can be an Int for example "orderNum".
So when you fetch request you can order the result using this prperty.
So if your table view cell re-arranged, after implementing moveItem method you can update this property for all your objects(loop over them) and they will be as they are displayed.
try to save your managed object context now,
Next time when you want to fetch request you can use a sort descriptor to sort on the "orderNum".
Maybe updating your data source (by removing and re-inserting the item) when moveRowAt is called would be better?
So something like:
// assuming your data source is an array of names
var data = ["Jon", "Arya", "Tyrion", "Sansa", "Winterfell"]
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
self.projectTableView.moveRow(at: sourceIndexPath, to: destinationIndexPath)
let item = self.data.remove(at: sourceIndexPath.row)
if sourceIndexPath.row > destinationIndexPath.row {
// "Sansa" was moved to be between "Jon" and "Arya"
self.data.insert(item, at: destinationIndexPath.row
} else {
// if the new destination comes after previous location i.e. "Sansa"
// was moved to the end of the list
self.data.insert(item, at: destinationIndexPath.row - 1
}
}

How can I delete a row with native animation of a tableView by a button in custom cell?

I want to delete a table row by action from button in custom cell, using native tableView animations. Can someone help me please?
I Found a good solution, using a custom delegate and
TableViewDataSource method!
I had to work with Protocol in my custom cell Class, like this:
protocol MyCellDelegate: class {
func didDelete(cell: MyCell)
}
In my custom cell Class I made this:
class MyCell: UITableViewCell {
weak var delegate: MyCellDelegate?
#IBAction func deleteButtonTapped(_ sender: Any) {
delegate?.didDelete(cell: self)
}
}
In my ViewController I made the delegates and data source implementation:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
tableView.beginUpdates()
tableView.deleteRows(at:[indexPath], with: .fade)
tableView.endUpdates()
default:
break
}
}
}
extension ViewController: MyCellDelegate {
func didDelete(cell: MyCell) {
guard let index = tableView.indexPath(for: cell) else {
return
}
myArray?.remove(at: index.row)
self.tableView(self.tableView, commit: .delete, forRowAt: index)
}
}

SearchBar & Display in table view with Coredata using Swift?

I am new to iOS development and am in my second app with Swift. I am unable to incorporate SearchBar & Display search in my Table view controller.
There are 2 views in my app. First View generates an Array of fruits. Output of First View: [Apple, Orange, Banana, Pears]. Then [Pomegranate, Pears, Watermelon, Muskmelon] and so on. One array at a time.
Each array of fruits is stored in core-data using NSFetchedResultsController to be displayed in a tableviewcontroller.
My Coredata layout:
import UIKit
import CoreData
#objc(Fruits)
class Fruits: NSManagedObject {
#NSManaged var date: String
#NSManaged var fruit: String
}
My Second View displays the list of fruits. Now I have added a Search Bar & Display on top on the table view. But I am unable to make it work.
Protocols used:
class HistoryTableViewController: UITableViewController, NSFetchedResultsControllerDelegate{
Variables declared:
let managedObjectContext: NSManagedObjectContext? = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext
var fetchedResultsController: NSFetchedResultsController?
var fetchRequest = NSFetchRequest(entityName: "Fruits")
There are two outlets in my class:
#IBOutlet weak var searchBar: UISearchBar! // the Search Bar & Display
#IBOutlet var tblHistory: UITableView! = nil // Table View
Data population for table view:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController?.sections?.count ?? 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return fetchedResultsController?.sections?[section].numberOfObjects ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: (NSIndexPath!)) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as UITableViewCell
if let cellFruit = fetchedResultsController?.objectAtIndexPath(indexPath) as? Fruits
{
cell.textLabel?.text = cellFruit.fruit
cell.detailTextLabel?.text = cellFruit.date
}
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
//MARK: NSFetchedResultsController Delegate Functions
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case NSFetchedResultsChangeType.Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Move:
break
case NSFetchedResultsChangeType.Update:
break
default:
break
}
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
//Delete object from entity, remove from list
if editingStyle == .Delete {
}
switch editingStyle {
case .Delete:
managedObjectContext?.deleteObject(fetchedResultsController?.objectAtIndexPath(indexPath) as! Fruits)
do {
try managedObjectContext?.save()
}catch{}
print("Fruit set deleted successfully.")
case .Insert:
break
case .None:
break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case NSFetchedResultsChangeType.Insert:
tableView.insertRowsAtIndexPaths(NSArray(object: newIndexPath!) as! [NSIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Delete:
tableView.deleteRowsAtIndexPaths(NSArray(object: indexPath!) as! [NSIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Move:
tableView.deleteRowsAtIndexPaths(NSArray(object: indexPath!) as! [NSIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
tableView.insertRowsAtIndexPaths(NSArray(object: newIndexPath!) as! [NSIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
break
case NSFetchedResultsChangeType.Update:
tableView.cellForRowAtIndexPath(indexPath!)
break
default:
break
}
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
How do I Search any fruit from coredata and filter on the view?
Which delegate function should be used for the search bar & display functionality in Swift coding?
Any help on a concept I might have missed somewhere in here would be appreciated! Thanks.
Update: as Daniel Eggert suggested I am trying to use predicate for the Search Controller functionality, but the Predicate always gives a nil. What am I missing???
Variables declared in class:
var results: NSArray = []
var searchPredicate: NSPredicate?
var searchController: UISearchController!
Predicate Functions:
//Search Functionality
func updateSearchResultsForSearchController(searchController: UISearchController)
{
let searchText = self.searchController?.searchBar.text
if let searchText = searchText {
searchPredicate = NSPredicate(format: "fruit contains[cd] %#", searchText)
results = (self.fetchedResultsController!.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
} as! [Fruits]?)!
self.tableView.reloadData()
}
}
// Called when text changes (including clear)
func searchBar(searchBar: UISearchBar, textDidChange searchText: String)
{
if !searchText.isEmpty
{
var predicate: NSPredicate = NSPredicate()
predicate = NSPredicate(format: "fruit contains [cd] %#", searchText)
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchedResultsController?.fetchRequest.predicate? = predicate
print(fetchedResultsController?.fetchRequest.predicate) // this is always nil. Why????????????
do {
try fetchedResultsController?.performFetch()
}catch{}
print("results array: \(results)") // this array should have values of the table view, but is empty. Why ?????????????????
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext!,
sectionNameKeyPath: "fruit", cacheName: nil)
fetchedResultsController?.delegate = self
tableView.reloadData()
}
}
You want to set a predicate on you fetched results controller.
Once you've changed the fetch request on the fetched results controller you need to call performFetch() to have it refetch its data. Then call reloadData() on the table view.
Text search for non-trivial examples can be tricky to do right, though, due to how languages, locales, and Unicode interact. I'd recommend the Text chapter of my book: https://www.objc.io/books/core-data/
If you want to implement searchable tableView? You're must must use UISearchResultsUpdating protocol method instead of the UISearchBarDelegate protocol method called searchBar:textDidChange. You can use searchBar:textDidChange method when UISearchResultsUpdating protocol method is not available! Both classes are useful and powerful. And easy to implement

Fetch One Attribute of NSManagedObject after Pressing a Button in UITableViewCell

I'm using core data and I have a tableView to display all user's approvals
I'm using NSFetchedResultsController to fetch the approvals from core data
I have added these methods which are used when the delegate is notified that the content will change, we tell the tableView to begin updates
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
// 1
switch type {
case .Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Automatic)
case .Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Automatic)
default: break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
// 2
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Automatic)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
default: break
}
}
what I understood: the NSFetchtedResultsController can monitor changes to the NSManagedObject context and update the table to reflect those changes
In tableViewCell I have "inquire Again" button which will update the Approval Status
and this is ApprovalObject Class:
public class ApprovalObject: NSManagedObject {
#NSManaged public var reference_no: NSNumber
#NSManaged public var status: String
#NSManaged public var date: NSDate
#NSManaged public var card: Card
#NSManaged public var hospital: Hospital
}
my question is: how to get the value of "status" attribute for specific "ApprovalObject" after pressing the button in the tableViewCell ?? can I just call tableView.beginUpdates() ??
#IBAction func inquireApprovalBtn(sender: AnyObject) {
// what should i do here ???
}
It is enough to modify your data object, e.g. the status of your ApprovalObject instance (and save if desired). The NSFetchedResultsControllerDelegate should pick up on that and update the cell in question.
Of course, you have at some point deleted the .Change case in your switch statement in the delegate callback. Make sure you put it back in. (You can copy it from a "Master-Detail" Xcode template.)

How refresh data in app after update some data?

I have a problem with update contact data. View no.1 main view with tableView i have list of contacts. After tap some person from list i have next tableView (view no.2 [push from view 1 to view 2]) with details and button with EDIT. Next if i press Edit i have modal view no3 with edit input when i can change NAME. After SAVE how can i refresh
title in tableView navBar
title in backButton
main tableView in view1
i am using in my view no1
let managedObjectContext: NSManagedObjectContext? = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext
var fetchedResultsController: NSFetchedResultsController?
override func viewDidLoad() {
super.viewDidLoad()
self.configureView()
fetchedResultsController = NSFetchedResultsController(fetchRequest: CLInstance.allContactsFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController?.delegate = self
do { try fetchedResultsController?.performFetch() } catch _ {}
tableView.reloadData()
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(
controller: NSFetchedResultsController,
didChangeObject anObject: AnyObject,
atIndexPath indexPath: NSIndexPath?,
forChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths(NSArray(object: newIndexPath!) as! [NSIndexPath], withRowAnimation: .Fade)
break
case .Delete:
tableView.deleteRowsAtIndexPaths(NSArray(object: indexPath!) as! [NSIndexPath], withRowAnimation: .Fade)
break
case .Move:
print("move")
//tableView.moveRowAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
break
case .Update:
print("update")
tableView.cellForRowAtIndexPath(indexPath!)
break
}
}
I expected when i save data in this code NSFetchedResultsController make update but on my log file i see print out "MOVE" not "UPDATE"how it is possible to do this in some other way ?
First of all you need to configure Outlets to any of the on screen elements you want to update. Then you just set their .text attribute to the new value.
The tableView is a bit different though, you have to reload it : myTableView.reloadData()
Just a few gotcha's:
If your 'myTableView isn't reloading, make sure that the
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
function where the tableViewCell is created is using the updated source for creating the cell.
Do all your updates on the main thread (if you're using threads)