How refresh data in app after update some data? - swift

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)

Related

NSFetchedResultsController error: 'no object at index in section at index 0'

I have a UITableView which populate it cells with a NSFetchedResultsController based on CoreData attribute isForConverter which is Bool and should be true to be displayed. isForConverter state sets in another ViewController.
When I want to delete some cells from the UITableView and after access cells which wasn't deleted I receive the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'no object at index 5 in section at index 0'
There is a GIF with the problem: https://cln.sh/M1aI9Z
My code for deleting cells. I don't need to delete it from database, just change it isForConverter from true to false:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let currency = fetchedResultsController.object(at: indexPath)
currency.isForConverter = false
coreDataManager.save()
}
}
NSFetchedResultsController Setup and delegates:
func setupFetchedResultsController() {
let predicate = NSPredicate(format: "isForConverter == YES")
fetchedResultsController = coreDataManager.createCurrencyFetchedResultsController(with: predicate)
fetchedResultsController.delegate = self
try? fetchedResultsController.performFetch()
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
}
I noticed that if I just add tableView.reloadData() to:
tableView(_ tableView: UITableView, commit editingStyle:
UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
Then everything works good. But deletion animation is really fast and antsy. Also according to docs I should not use tableView.reloadData() with NSFetchedResultsController...
How to fix that behaviour?
UPDATE:
It seems I found out what the reason of that crash was. This is what my print() tryings gave: SCREENSHOT.
What is a pickedCurrency: this is a global variable of custom type Currency which I created to receive its attribute currentValue (Double, 87.88). I need that value only from the picked to edit cell. After I use that value for calculation at cellForRowAt() and result of the calculation fills all other cells which is not in the edit mode now.
I define pickedCurrency in textFieldDidBeginEditing() because there I receive the exact row of Currency I picked to edit:
func textFieldDidBeginEditing(_ textField: UITextField) {
pickedCurrency = fetchedResultsController.object(at: IndexPath(row: textField.tag, section: 0))
numberFromTextField = 0
textField.textColor = UIColor(named: "BlueColor")
textField.placeholder = "0"
textField.text = ""
}
And then use it's value in cellForRowAt to calculate all other cells values based on pickedCell value and number I put in a textField of activeCell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
let currency = fetchedResultsController.object(at: indexPath)
cell.flag.image = currencyManager.showCurrencyFlag(currency.shortName ?? "notFound")
cell.shortName.text = currency.shortName
cell.fullName.text = currency.fullName
cell.numberTextField.tag = indexPath.row
cell.numberTextField.delegate = self
if let number = numberFromTextField, let pickedCurrency = pickedCurrency {
cell.numberTextField.text = currencyManager.performCalculation(with: number, pickedCurrency, currency)
}
return cell
}
It seems when I delete a lot of cells and then click on random cell to edit it's not updates its IndexPath(row: textField.tag, section: 0)...
Maybe there is a way to receive Currency object I picked for editing in cellForRowAt()?
I am not certain if this will work but it seems too long for it to be a comment so give the below a try.
Initially I told you could use tag to identify a specific view which is good for quick and simple implementations but when rows get moved / deleted as we have it now, it will be very difficult to manage using tags as you have to constantly update them.
For static table views, they are fine but if your rows will change, then more sophisticated patterns like delegate / observer might be better.
Anyways, what I think can help your situation for now in textFieldDidBeginEditing is stop using the tag to get the index path and get the indexpath from what is tapped.
I still think maybe delegate pattern is better but this might work for you:
func textFieldDidBeginEditing(_ textField: UITextField) {
// Get the coordinates of where we tapped in the table
let tapLocation = textField.convert(textField.bounds.origin,
to: tableView)
if let indexPath = self.tableView.indexPathForRow(at: tapLocation)
{
// don't use tag of textfield anymore
pickedCurrency
= fetchedResultsController.object(at: IndexPath(row: indexPath,
section: 0))
numberFromTextField = 0
textField.textColor = UIColor(named: "BlueColor")
textField.placeholder = "0"
textField.text = ""
}
}
Does this help ?

NSFetchedResultsController crash when Section index changes

I'm writing my app in Swift 3 (converted) in Xcode 8.
NSFetchedResultsController is causing a Serious Application Error for me.
My main table view is sectioned by a text identifier called "yearText" which is being set on any given Event record (NSManagedObject) when the user changes the "Event Date" with a date picker. When the picker is changed or dismissed, the year is stripped from the date, converted to text, and stored in the Event object. The managed object context is then saved.
If a date is picked for which there is already a section in existence (i.e. the year "2020") an error is thrown that says:
[error] error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
As long as the date chosen is not within a year that already has a section named after it, it all works fine.
Here is my relevant code for updating the database and tableview:
var fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
// Fetch the default object (Event)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
let entity = NSEntityDescription.entity(forEntityName: "Event", in: managedObjectContext!)
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 60
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext!, sectionNameKeyPath: "yearText", cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} catch {
// Implement error handling code here.
abort()
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>?
// MARK: - UITableViewDelegate
extension EventListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! EventCell
cell.isSelected = true
configureCell(withCell: cell, atIndexPath: indexPath)
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! EventCell
cell.isSelected = false
configureCell(withCell: cell, atIndexPath: indexPath)
}
}
// MARK: - UITableViewDataSource
extension EventListViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EventCell", for: indexPath) as! EventCell
configureCell(withCell: cell, atIndexPath: indexPath)
return cell
}
func configureCell(withCell cell: EventCell, atIndexPath indexPath: IndexPath) {
// bunch of stuff to make the cell pretty and display the data
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let context = fetchedResultsController.managedObjectContext
context.delete(fetchedResultsController.object(at: indexPath) as! NSManagedObject)
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
//print("Unresolved error \(error), \(error.userInfo)")
abort()
}
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.name
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
// make the section header look good
view.tintColor = kWPPTintColor
let header = view as! UITableViewHeaderFooterView
header.textLabel?.textColor = kWPPDarkColor
header.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.subheadline)
}
}
// MARK: - NSFetchedResultsControllerDelegate
extension EventListViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
switch type {
case .insert:
tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
case .delete:
tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
default:
return
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
configureCell(withCell: tableView.cellForRow(at: indexPath!)! as! EventCell, atIndexPath: indexPath!)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
}
I hope you can offer me some suggestions. Thank you.
EDIT: Took out some code that was just getting in the way and revised .move to use .moveRow
EDIT 2: Added FRC generation code.
I met the same error when I update some properties on my Core Data managed objects.
Here is my controller func:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
self.tableView.insertRows(at: [newIndexPath!], with: .fade)
case .delete:
self.tableView.deleteRows(at: [indexPath!], with: .fade)
case .update:
self.tableView.reloadRows(at: [indexPath!], with: .fade)
case .move:
self.tableView.insertRows(at: [newIndexPath!], with: .fade)
self.tableView.deleteRows(at: [indexPath!], with: .fade)
}
}
Before I used newIndexPath for the update case, but I found this will cause some section rows mismatch issue when fetch result controller do some update action. Instead, using indexPath for update case is fine.

Reload Tableview in Swift

I'm currently working on a tableView based project in swift. I have two tableView running in my project. As my first tableView loaded with data which has a editActionsForRowAtIndexPath function to delete cell(as a Favourite) and moving it to my second tableview.
But, I am getting trouble saving data and reloading tableview on both my first and second tableView Controller after tableViewCell been moved to my second tableView...
var arrays = ["Alpha","Beta","Gamma","Phill","Below","Above","Clean",]
var deleted: [String] = []
//passing data to another tableVC
var sendSelectedData = NSString()
let defaults = NSUserDefaults.standardUserDefaults()
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let favorite = UITableViewRowAction(style: .Normal, title: "Favourite") { action, index in
print("favourite button tapped")
let editingStyle = UITableViewCellEditingStyle.Delete
if editingStyle == UITableViewCellEditingStyle.Delete {
self.tableView.beginUpdates()
self.deleted.append(self.arrays[indexPath.row])
self.arrays.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
}
}
favorite.backgroundColor = UIColor.orangeColor()
return [favorite]
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "ShowDetails") {
// initialize new view controller and cast it as your view controller
let viewController = segue.destinationViewController as! favTableViewController
// your new view controller should have property that will store passed value
viewController.arrayx = deleted
}
}
I am not sure whether i have to use NSUserDefaults function to save data or just reload tableview..
Thanks in Advance.

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

Swift UISearchController wired up in Core Data Project, app runs, but search not updating

I have been wrestling the past few days with creating a filter in a Swift project for a TableViewController that uses Core Data. I finally figured out I need to use a UISearchController, create an NSPredicate for the searchController.searchBar, etc.
I found this post EXTREMELY helpful, but after modeling my TVC after this project, I find "all the lights are on, but nobody's home". I can search, predicate is created in searchBar, add, remove, etc. but the cells don't update for the search. I'm missing something, but I don't know what.
Here are the relevant portions of my code.
class MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchControllerDelegate, UISearchResultsUpdating
// Properties that get instantiated later
var detailViewController: DetailViewController? = nil
var addNoteViewController:AddNoteViewController? = nil // I added this
var managedObjectContext: NSManagedObjectContext? = nil
// Added variable for UISearchController
var searchController: UISearchController!
var searchPredicate: NSPredicate? // I added this. It's optional on and gets set later
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = self.editButtonItem()
if let split = self.splitViewController {
let controllers = split.viewControllers
let context = self.fetchedResultsController.managedObjectContext
let entity = self.fetchedResultsController.fetchRequest.entity!
self.detailViewController = controllers[controllers.count-1].topViewController as? DetailViewController
}
// UISearchController setup
searchController = UISearchController(searchResultsController: nil)
searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
self.tableView.tableHeaderView = searchController?.searchBar
self.tableView.delegate = self
self.definesPresentationContext = true
}
// MARK: - UISearchResultsUpdating Delegate Method
// Called when the search bar's text or scope has changed or when the search bar becomes first responder.
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchText = self.searchController?.searchBar.text // steve put breakpoint
println(searchController.searchBar.text)
if let searchText = searchText {
searchPredicate = NSPredicate(format: "noteBody contains[c] %#", searchText)
self.tableView.reloadData()
println(searchPredicate)
}
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
var note: Note
if searchPredicate == nil {
note = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Note
} else {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
note = filteredObjects![indexPath.row] as! Note
}
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(note)
var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}
// MARK: - Fetched results controller
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("Note", inManagedObjectContext: self.managedObjectContext!)
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "noteTitle", ascending: false)
let sortDescriptors = [sortDescriptor]
fetchRequest.sortDescriptors = [sortDescriptor]
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
var error: NSError? = nil
if !_fetchedResultsController!.performFetch(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
println("Unresolved error \(error), \(error?.userInfo)")
abort()
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController? = nil
func controllerWillChangeContent(controller: NSFetchedResultsController) {
// self.tableView.beginUpdates() // ** original code, change if doesn't work** steve put breakpoint here
// ANSWER said this section is redundant, but keeping it b/c it doesn't crash
if searchPredicate == nil {
tableView.beginUpdates()
} else {
(searchController.searchResultsUpdater as! MasterViewController).tableView.beginUpdates()
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
var tableView = UITableView()
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as! MasterViewController).tableView
}
switch type {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
return
}
}
I think my problem "lives" here in this section. Autocomplete has been my friend up to here, but I'm don't see "searchIndex" referenced in AutoComplete. I think I'm missing something, but I'm not sure what or how.
If you've made it this far, thanks for reading. Here's the GitHub repo for the branch I'm working on.
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
var tableView = UITableView()
if self.searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (self.searchController.searchResultsUpdater as! MasterViewController).tableView
}
switch type {
case .Insert:
println("*** NSFetchedResultsChangeInsert (object)")
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
println("*** NSFetchedResultsChangeDelete (object)")
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
println("*** NSFetchedResultsChangeUpdate (object)")
// TODO: need fix this
// ORIGINAL CODE
// self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!) // original code
// PROSPECTIVE SOLUTION CODE
println("*** NSFetchedResultsChangeUpdate (object)")
if searchPredicate == nil {
self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!) // original code
} else {
// Should search the do something w/ the UISearchControllerDelegate or UISearchResultsUpdating
// Instead of "indexPath", it should be "searchIndexPath"--How?
let cell = tableView.cellForRowAtIndexPath(searchIndexPath) as LocationCell // My cell is a vanilla cell, not a xib
let location = controller.objectAtIndexPath(searchIndexPath) as Location // My object is a "Note"
cell.configureForLocation(location) // This is from the other guy's code, don't think it's applicable to me
}
case .Move:
println("*** NSFetchedResultsChangeMove (object)")
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
if self.searchPredicate == nil {
self.tableView.endUpdates()
} else {
println("controllerDidChangeContent")
(self.searchController.searchResultsUpdater as! MasterViewController).tableView.endUpdates()
}
}
Edit: Per #pbasdf, I'm adding the TableView methods.
// MARK: - Table View
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections?.count ?? 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchPredicate == nil {
let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
}
let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
The problems lie in your tableView datasource methods: numberOfSectionsInTableview:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath:. You need each of those methods to return different results if the searchPredicate is not nil - much like your tableView:commitEditingStyle: method does. I would make filteredObjects an instance property (defined at the start of the class) so that all those methods can access it:
var filteredObjects : [Note]? = nil
Now, when the search text changes, you want to rebuild the filteredObjects array. So in updateSearchResultsForSearchController, add a line to recompute it based on the new predicate:
if let searchText = searchText {
searchPredicate = NSPredicate(format: "noteBody contains[c] %#", searchText)
filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
} as [Note]?
self.tableView.reloadData()
println(searchPredicate)
}
I would also recommend (for simplicity) that when you activate the search, the results are displayed all in one section (otherwise you have to work out how many sections your filtered results fall into - possible but tiresome):
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if searchPredicate == nil {
return self.fetchedResultsController.sections?.count ?? 0
} else {
return 1
}
}
Next, if the searchPredicate is not nil, the number of rows in the section will be the count of filteredObjects:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.searchPredicate == nil {
let sectionInfo = self.fetchedResultsController.sections![section] as! NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
} else {
return filteredObjects?.count ?? 0
}
}
Finally, if the searchPredicate is not nil, you need to build the cell using the filteredObjects data, rather than the fetchedResultsController:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
if searchPredicate == nil {
self.configureCell(cell, atIndexPath: indexPath)
return cell
} else {
// configure the cell based on filteredObjects data
...
return cell
}
}
Not sure what labels etc you have in your cells, so I leave it to you to sort that bit.