UISearchDisplayController animate reloadData - uitableview

I've been reading all the documentation about UISearchDisplayController and its delegate but I can't find any way to animate the table view when the search criteria change.
I'm using these two methods :
They both return YES but still can't find any way to do something similar to :
I don't know if it's important but I'm using an NSfetchedResultsController to populate the UITableView in the UISearchDisplayController
That's it thanks !

When the search string or scope has changed, you assign a new fetch request for the fetched results controller and therefore have to call performFetch to get a new result set. performFetch resets the state of the controller, and it does not trigger any of the FRC delegate methods.
So the table view has to be updated "manually" after changing the fetch request. In most sample programs, this is done by
calling reloadData on the search table view, or
returning YES from shouldReloadTableForSearchString or shouldReloadTableForSearchScope.
The effect is the same: The search table view is reloaded without animation.
I don't think there is any built-in/easy method to animate the table view update when the search predicate changes. However, you could try the following (this is just an idea, I did not actually try this myself):
Before changing the fetch request, make a copy of the old result set:
NSArray *oldList = [[fetchedResultsController fetchedObjects] copy];
Assign the new fetch request to the FRC and call performFetch.
Get the new result set:
NSArray *newList = [fetchedResultsController fetchedObjects];
Do not call reloadData on the search table view.
Compare oldList and newList to detect new and removed objects. Call insertRowsAtIndexPaths for the new objects and deleteRowsAtIndexPaths for the removed objects. This can be done by traversing both lists in parallel.
Return NO from shouldReloadTableForSearchString or shouldReloadTableForSearchScope.
As I said, this is just an idea but I think it could work.

I know this question is old, but I recently faced this and wanted a solution that would work regardless of how the NSFetchedResultsController was initialized. It's based on #martin-r's answer above.
Here's the corresponding gist: https://gist.github.com/stephanecopin/4ad7ed723f9857d96a777d0e7b45d676
import CoreData
extension NSFetchedResultsController {
var predicate: NSPredicate? {
get {
return self.fetchRequest.predicate
}
set {
try! self.setPredicate(newValue)
}
}
var sortDescriptors: [NSSortDescriptor]? {
get {
return self.fetchRequest.sortDescriptors
}
set {
try! self.setSortDescriptors(newValue)
}
}
func setPredicate(predicate: NSPredicate?) throws {
try self.setPredicate(predicate, sortDescriptors: self.sortDescriptors)
}
func setSortDescriptors(sortDescriptors: [NSSortDescriptor]?) throws {
try self.setPredicate(self.predicate, sortDescriptors: sortDescriptors)
}
func setPredicate(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) throws {
func updateProperties() throws {
if let cacheName = cacheName {
NSFetchedResultsController.deleteCacheWithName(cacheName)
}
self.fetchRequest.predicate = predicate
self.fetchRequest.sortDescriptors = sortDescriptors
try self.performFetch()
}
guard let delegate = self.delegate else {
try updateProperties()
return
}
let previousSections = self.sections ?? []
let previousSectionsCount = previousSections.count
var previousObjects = Set(self.fetchedObjects as? [NSManagedObject] ?? [])
var previousIndexPaths: [NSManagedObject: NSIndexPath] = [:]
previousObjects.forEach {
previousIndexPaths[$0] = self.indexPathForObject($0)
}
try updateProperties()
let newSections = self.sections ?? []
let newSectionsCount = newSections.count
var newObjects = Set(self.fetchedObjects as? [NSManagedObject] ?? [])
var newIndexPaths: [NSManagedObject: NSIndexPath] = [:]
newObjects.forEach {
newIndexPaths[$0] = self.indexPathForObject($0)
}
let updatedObjects = newObjects.intersect(previousObjects)
previousObjects.subtractInPlace(updatedObjects)
newObjects.subtractInPlace(updatedObjects)
var moves: [(object: NSManagedObject, fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath)] = []
updatedObjects.forEach { updatedObject in
if let previousIndexPath = previousIndexPaths[updatedObject],
let newIndexPath = newIndexPaths[updatedObject]
{
if previousIndexPath != newIndexPath {
moves.append((updatedObject, previousIndexPath, newIndexPath))
}
}
}
if moves.isEmpty && previousObjects.isEmpty && newObjects.isEmpty {
// Nothing really changed
return
}
delegate.controllerWillChangeContent?(self)
moves.forEach {
delegate.controller?(self, didChangeObject: $0.object, atIndexPath: $0.fromIndexPath, forChangeType: .Move, newIndexPath: $0.toIndexPath)
}
let sectionDifference = newSectionsCount - previousSectionsCount
if sectionDifference < 0 {
(newSectionsCount..<previousSectionsCount).forEach {
delegate.controller?(self, didChangeSection: previousSections[$0], atIndex: $0, forChangeType: .Delete)
}
} else if sectionDifference > 0 {
(previousSectionsCount..<newSectionsCount).forEach {
delegate.controller?(self, didChangeSection: newSections[$0], atIndex: $0, forChangeType: .Insert)
}
}
previousObjects.forEach {
delegate.controller?(self, didChangeObject: $0, atIndexPath: previousIndexPaths[$0], forChangeType: .Delete, newIndexPath: nil)
}
newObjects.forEach {
delegate.controller?(self, didChangeObject: $0, atIndexPath: nil, forChangeType: .Insert, newIndexPath: newIndexPaths[$0])
}
delegate.controllerDidChangeContent?(self)
}
}
It exposes 2 properties on the NSFetchedResultsController, predicate and sortDescriptors which mirrors those found in the fetchRequest.
When either of these properties are set, the controller will automatically compute changes the changes and send them through the delegate, so hopefully there is no major code changes.
If you don't want animations, you can still directly set predicate or sortDescriptors on the fetchRequest itself.

Related

NSFetchedResultsController Sort Descriptor for Last Character Of String

How do you set a NSSortDescriptor which will sort by an attribute (but the last character of it?)
For example, if I have the following barcodes...
0000000005353
0000000000224
0000000433355
It should sort using last character, in asc or desc order. So like 3,4,5 in this example. Which would create section headers 3,4,5.
The current code I have gives me an error, sayings the "fetched object at index 7 has an out of order section name '9'. Objects must be sorted by section name. Which tells me I messed up the sort. To understand more please look at the code as I'm using transient properties on the core data model.
The idea is that "numberendsection", should sort from the end of the number as I described previously.
The other two sorts I describe work perfectly right now.
Inventory+CoreDataProperties.swift
import Foundation
import CoreData
extension Inventory {
#NSManaged var addCount: NSNumber?
#NSManaged var barcode: String?
#NSManaged var currentCount: NSNumber?
#NSManaged var id: NSNumber?
#NSManaged var imageLargePath: String?
#NSManaged var imageSmallPath: String?
#NSManaged var name: String?
#NSManaged var negativeCount: NSNumber?
#NSManaged var newCount: NSNumber?
#NSManaged var store_id: NSNumber?
#NSManaged var store: Store?
//This is used for A,B,C ordering...
var lettersection: String? {
let characters = name!.characters.map { String($0) }
return characters.first?.uppercaseString
}
//This is used for 1,2,3 ordering... (using front of barcode)
var numbersection: String? {
let characters = barcode!.characters.map { String($0) }
return characters.first?.uppercaseString
}
//This is used for 0000000123 ordering...(uses back number of barcode)
var numberendsection: String? {
let characters = barcode!.characters.map { String($0) }
return characters.last?.uppercaseString
}
}
InventoryController.swift - (showing only relevant part)
import UIKit
import CoreData
import Foundation
class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
//Create fetchedResultsController to handle Inventory Core Data Operations
lazy var fetchedResultsController: NSFetchedResultsController = {
return self.setFetchedResultsController()
}()
func setFetchedResultsController() -> NSFetchedResultsController{
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
var primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)//by default assume name.
if(g_appSettings[0].indextype=="numberfront"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}else if(g_appSettings[0].indextype=="numberback"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}
//let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]
let storefilter = g_appSettings[0].selectedStore!
let predicate = NSPredicate(format: "store = %#", storefilter) //This will ensure correct data relating to store is showing
inventoryFetchRequest.predicate = predicate
//default assume letter section
var frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "lettersection",
cacheName: nil)
if(g_appSettings[0].indextype=="numberfront"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}else if(g_appSettings[0].indextype=="numberback"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numberendsection",
cacheName: nil)
}
frc.delegate = self
return frc
}
Entity Diagram
Entity + Core Data Screenshot
Screenshot of Error and Code where it occurs
Inventory.swift
** Inventory.swift Entire File **
import UIKit
import CoreData
import Foundation
class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
//Create fetchedResultsController to handle Inventory Core Data Operations
lazy var fetchedResultsController: NSFetchedResultsController = {
return self.setFetchedResultsController()
}()
func setFetchedResultsController() -> NSFetchedResultsController{
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
var primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)//by default assume name.
print("primarySortDescriptor...")
if(g_appSettings[0].indextype=="numberfront"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}else if(g_appSettings[0].indextype=="numberback"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}
print("set primarySortDescriptor")
//let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]
print("set sort descriptors to fetch request")
var storefilter : Store? = nil
if(g_appSettings[0].selectedStore != nil){
storefilter = g_appSettings[0].selectedStore
let predicate = NSPredicate(format: "store = %#", storefilter!) //This will ensure correct data relating to store is showing
inventoryFetchRequest.predicate = predicate
}
//default assume letter section
var frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "lettersection",
cacheName: nil)
if(g_appSettings[0].indextype=="numberfront"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}else if(g_appSettings[0].indextype=="numberback"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}
print("set the frc")
frc.delegate = self
return frc
}
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var inventoryTable: UITableView!
var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext //convinience variable to access managed object context
// Start DEMO Related Code
var numberIndex = ["0","1","2","3","4","5","6","7","8","9"]
var letterIndex = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
var previousNumber = -1 //used so we show A,A, B,B, C,C etc for proper testing of sections
func createInventoryDummyData(number: Int) -> Inventory{
let tempInventory = NSEntityDescription.insertNewObjectForEntityForName("Inventory", inManagedObjectContext: moc) as! Inventory
if(number-1 == previousNumber){
tempInventory.name = "\(letterIndex[number-2])-Test Item # \(number)"
previousNumber = -1//reset it again
}else{
tempInventory.name = "\(letterIndex[number-1])-Test Item # \(number)"
previousNumber = number //set previous letter accordingly
}
tempInventory.barcode = "\(number)00000000\(number)"
tempInventory.currentCount = 0
tempInventory.id = number
tempInventory.imageLargePath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.imageSmallPath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.addCount = 0
tempInventory.negativeCount = 0
tempInventory.newCount = 0
tempInventory.store_id = 1 //belongs to same store for now
//Select a random store to belong to 0 through 2 since array starts at 0
let aRandomInt = Int.random(0...2)
tempInventory.setValue(g_storeList[aRandomInt], forKey: "store") //assigns inventory to one of the stores we created.
return tempInventory
}
func createStoreDummyData(number:Int) -> Store{
let tempStore = NSEntityDescription.insertNewObjectForEntityForName("Store", inManagedObjectContext: moc) as! Store
tempStore.address = "100\(number) lane, Miami, FL"
tempStore.email = "store\(number)#centraltire.com"
tempStore.id = number
tempStore.lat = 1.00000007
tempStore.lng = 1.00000008
tempStore.name = "Store #\(number)"
tempStore.phone = "123000000\(number)"
return tempStore
}
// End DEMO Related Code
override func viewDidLoad() {
super.viewDidLoad()
print("InventoryController -> ViewDidLoad -> ... starting inits")
// // Do any additional setup after loading the view, typically from a nib.
// print("InventoryController -> ViewDidLoad -> ... starting inits")
//
//First check to see if we have entities already. There MUST be entities, even if its DEMO data.
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
let storeFetchRequest = NSFetchRequest(entityName: "Store")
do {
let storeRecords = try moc.executeFetchRequest(storeFetchRequest) as? [Store]
//Maybe sort descriptor here? But how to organize into sectioned array?
if(storeRecords!.count<=0){
g_demoMode = true
print("No store entities found. Demo mode = True. Creating default store entities...")
var store : Store //define variable as Store type
for index in 1...3 {
store = createStoreDummyData(index)
g_storeList.append(store)
}
//save changes for the stores we added
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
}
let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory]
//Maybe sort descriptor here? But how to organize into sectioned array?
if(inventoryRecords!.count<=0){
g_demoMode = true
print("No entities found for inventory. Demo mode = True. Creating default entities...")
var entity : Inventory //define variable as Inventory type
for index in 1...52 {
let indexFloat = Float(index/2)+1
let realIndex = Int(round(indexFloat))
entity = createInventoryDummyData(realIndex)
g_inventoryItems.append(entity)
}
//save changes for inventory we added
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
print("finished creating entities")
}
}catch{
fatalError("bad things happened \(error)")
}
//perform fetch we need to do.
do {
try fetchedResultsController.performFetch()
} catch {
print("An error occurred")
}
print("InventoryController -> viewDidload -> ... finished inits!")
}
override func viewWillAppear(animated: Bool) {
print("view appearing")
//When the view appears its important that the table is updated.
//Look at the selected Store & Use the LIST of Inventory Under it.
//Perform another fetch again to get correct data~
do {
//fetchedResultsController. //this will force setter code to run again.
print("attempting fetch again, reset to use lazy init")
fetchedResultsController = setFetchedResultsController() //sets it again so its correct.
try fetchedResultsController.performFetch()
} catch {
print("An error occurred")
}
inventoryTable.reloadData()//this is important to update correctly for changes that might have been made
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
print("inventoryItemControllerPrepareForSegueCalled")
if segue.identifier == "inventoryInfoSegue" {
let vc = segue.destinationViewController as! InventoryItemController
if let cell = sender as? InventoryTableViewCell{
vc.inventoryItem = cell.inventoryItem! //sets the inventory item accordingly, passing its reference along.
}else{
print("sender was something else")
}
}
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
//This scrolls to correct section based on title of what was pressed.
return letterIndex.indexOf(title)!
}
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
//This is smart and takes the first letter of known sections to create the Index Titles
return self.fetchedResultsController.sectionIndexTitles
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("InventoryTableCell", forIndexPath: indexPath) as! InventoryTableViewCell
let inventory = fetchedResultsController.objectAtIndexPath(indexPath) as! Inventory
cell.inventoryItem = inventory
cell.drawCell() //uses passed inventoryItem to draw it's self accordingly.
return cell
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//dispatch_async(dispatch_get_main_queue()) {
//[unowned self] in
print("didSelectRowAtIndexPath")//does not recognize first time pressed item for some reason?
let selectedCell = self.tableView(tableView, cellForRowAtIndexPath: indexPath) as? InventoryTableViewCell
self.performSegueWithIdentifier("inventoryInfoSegue", sender: selectedCell)
//}
}
#IBAction func BarcodeScanBarItemAction(sender: UIBarButtonItem) {
print("test of baritem")
}
#IBAction func SetStoreBarItemAction(sender: UIBarButtonItem) {
print("change store interface")
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
print("text is changing")
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
print("ended by cancel")
searchBar.text = ""
searchBar.resignFirstResponder()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
print("ended by search")
searchBar.resignFirstResponder()
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
print("ended by end editing")
searchBar.resignFirstResponder()
}
#IBAction func unwindBackToInventory(segue: UIStoryboardSegue) {
print("unwind attempt")
let barcode = (segue.sourceViewController as? ScannerViewController)?.barcode
searchBar.text = barcode!
print("barcode="+barcode!)
inventoryTable.reloadData()//reload the data to be safe.
}
}
//Extention to INT to create random number in range.
extension Int
{
static func random(range: Range<Int> ) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
NOTE::
I've cleared phone database also, just in case it was old database by deleting the app (holding down till it wiggles and deleting).
When your persistent store for Core Data is stored in SQLite (which I am assuming here otherwise the other answers would have worked already) you can't use computed properties or transient properties.
However, you can alter your data model so that you are storing the last digit of that bar code in its own property (known as denormalizing) and then sort on that new property. That is the right answer.
You can also do a secondary sort after you have done a fetch. However that means that you are holding a sorted array outside of the NSFetchedResultsController and you will then need to maintain the order of that array as you receive delegate callbacks from the NSFetchedResultsController. This is the second best answer.
If you can change the data model, then add a sort property. Otherwise your view controller code will be more complex because of the second sort.
You can add a comparator to your NSSortDescriptor
example
NSSortDescriptor *sortStates = [NSSortDescriptor sortDescriptorWithKey:#"barcode"
ascending:NO
comparator:^(id obj1, id obj2) {
[obj1 substringFromIndex:[obj1 length] - 1];
[obj2 substringFromIndex:[obj2 length] - 1];
return [obj1 compare: obj2])
}];
I think that you can use transient property in order to achieve what you want:
In order for it to work properly you have to provide implementation of this property in Inventory class.
var lastCharacter: String? {
let characters = barcode!.characters.map { String($0) }
return characters.last?.uppercaseString
}
Having 'lastCharacter' property set up correctly you can create sort descriptor that will allow you to achieve what you want:
NSSortDescriptor(key: "lastCharacter", ascending: true)
So it turns out that my method of trying to sort on a transient property does not work with NSSortDescriptors, the value has to be a real persisted one in the database.
Therefore, my solution was to create a new variable called barcodeReverse in the entity and at the time I enter data into the database for the barcode I also enter a reversed version using this code.
String(tempInventory.barcode!.characters.reverse())
tempInventory is an instance of my coreData class, and barcode a property on it. Simply just use characters.reverse() on the string.
Then you simply do the following:
primarySortDescriptor = NSSortDescriptor(key: "barcodeReverse", ascending: true)
and set frc like so...
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numberendsection",
cacheName: nil)
and finally the inventory extension should look like this.
//This is used for 0000000123 ordering...(uses back number of barcode)
var numberendsection: String? {
let characters = barcodeReverse!.characters.map { String($0) }
return characters.first?.uppercaseString
}
This will create the sections and order correctly using the last digit of the barcode.

NSFetchedResultsController pushes updates without having pushed any inserts

I have a NSFetchedResultsController initialized like so:
lazy var postsResultsController: NSFetchedResultsController = {
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Post")
// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
let predicate = NSPredicate(format: "owner == %#" , "owner",self.user)
fetchRequest.sortDescriptors = [sortDescriptor]
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: StateControl.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
}()
and I set my controller as the delegate. Here is an overview of the delegate methods:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
NSLog("Begin updates")
userPosts.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
NSLog("End updates")
userPosts.endUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch (type) {
case .Insert:
if let indexPath = newIndexPath {
userPosts.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
}
case .Delete:
if let indexPath = indexPath {
userPosts.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
}
case .Update:
print("Update request")
if let indexPath = indexPath {
userPosts.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
case .Move:
if let indexPath = indexPath {
userPosts.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
userPosts.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
}
And below you see my UITableViewDataSource delegate methods:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return postsResultsController.sections?.count ?? 0
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = postsResultsController.sections {
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
else {return 0}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = postsResultsController.objectAtIndexPath(indexPath) as! Post
let cell = tableView.dequeueReusableCellWithIdentifier(ReuseIdentifiers.PostCollectionViewCell, forIndexPath: indexPath) as! PostCell
cell.setup(withPost: post, fromUser: user)
return cell
}
I set myself as the delegate in my tableView's didSet:
var userPosts: PostsTableView! {
didSet {
userPosts.delegate = self
userPosts.dataSource = self
userPosts.scrollEnabled = false
}
}
However, when I arrive to viewDidLoad:, I do the following:
do {
try self.postsResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
userPosts.reloadData()
And yet, my tableView remains completely empty. What I have noticed is that my log shows that userPostsController is pushing update requests but no inserts.
2016-02-29 15:31:52.073 Columize[10373:149421] Begin updates
Update request
2016-02-29 15:31:52.073 Columize[10373:149421] End updates
2016-02-29 15:31:52.074 Columize[10373:149421] Begin updates
Update request
2016-02-29 15:31:52.074 Columize[10373:149421] End updates
So, my best guess is that the NSFetchedResultsController just watches the context and passes on updates (I do a lot of fetching from backend that merge with existing objects, hence the many updates), but it was my understanding that every time I create a new NSFecthedResultsController it would populate and pass on the appropriate delegate calls (mainly inserts).
Furthermore, I'm assuming that means that after a NSManagedObjectContext has interacted with a NSFecthedResultsController, subsequent NSFecthedResultsControllers simply don't push Inserts.
1-Is my reasoning correct?
2-How can I circumvent that? Do I have to create a new context for every NSFecthedResultsController?
UPDATE
So the reason nothing was showing was because my tableView had height of zero (wooops). However nothing will appear if I don't call reloadData after the first fetch, so my question stands, namely, why aren't there any inserts performed by my NSFetchedResultsController?

UISearchBar search function does not work

I've been trying to implement a search function onto this iOS app for quite some time.
I have a JSON file holding all the data, which is then pulled and displayed onto the table view controller.
I've implemented the search bar and followed several tutorials, this one being with the fewest issues - http://www.ioscreator.com/tutorials/add-search-table-view-tutorial-ios8-swift (I've also used this too previously - https://www.youtube.com/watch?v=N9wcKc37ZXI)
My JSON file is structured as follows:
{ "animal":[
{
"no":"001",
"type":"dog",
"breed":"pitbull",
"classification":"mammal",
"sprite":"beast"
},
{
"no":"002",
"type":"dog",
"breed":"bulldog",
"classification":"mammal",
"sprite":"beast"
},
{
"no":"003",
"type":"cat",
"breed":"birman",
"classification":"mammal",
"sprite":"feline"
}
]}
I am calling the JSON data using a struct that is created in a separate file
Animal.swift
struct animalStruct {
static let path = NSBundle.mainBundle().pathForResource("animal", ofType: "JSON")
static let jsonData = NSData(contentsOfFile:path!, options: .DataReadingMappedIfSafe, error: nil)
static var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(jsonData!, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
}
And it is called into the table view controller using the following
AnimalTableViewController
var array : NSArray = Animal.animalStruct.jsonResult["animal"] as NSArray
Code used for the search:
AnimalTableViewController
#IBOutlet var segmentedSortOption: UISegmentedControl!
var array : NSArray = Animal.pokemonStruct.jsonResult["animal"] as NSArray
var filteredAnimal = [String]()
var resultSearchController = UISearchController()
override func viewDidLoad() {
super.viewDidLoad()
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
self.tableView.reloadData()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == self.searchDisplayController?.searchResultsTableView {
return self.filteredAnimal.count
} else {
return self.array.count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell:cell = self.tableView.dequeueReusableCellWithIdentifier("cell") as cell
var upperCasedNames = array[indexPath.row]["name"] as? String
var searchedItem:String
if segmentedSortOption.selectedSegmentIndex == 0 {
if (self.resultSearchController.active) {
//**ISSUE OCCURS HERE**
myCell.name.text = filteredAnimal[indexPath.row]
return myCell
}
else {
myCell.name.text = upperCasedNames?.uppercaseString
return myCell
}
} else if segmentedSortOption.selectedSegmentIndex == 1 {
if let unsortedEvents = Animal.animalStruct.jsonResult["animal"] as? NSArray {
let descriptor = NSSortDescriptor(key: "name", ascending: true, selector: "caseInsensitiveCompare:")
let aToZ = unsortedEvents.sortedArrayUsingDescriptors([descriptor])
upperCasedNames = aToZ[indexPath.row]["name"] as? String
myCell.name.text = upperCasedNames?.uppercaseString
}
}
return myCell
}
func updateSearchResultsForSearchController(searchController: UISearchController)
{
filteredAnimal.removeAll(keepCapacity: false)
let searchPredicate = NSPredicate(format: "SELF CONTAINS[c] %#", searchController.searchBar.text)
let arrays = (array).filteredArrayUsingPredicate(searchPredicate!)
filteredArray = arrays as [[String]]
self.tableView.reloadData()
}
}
My question
The issue I have is that when I attempt to search via the UISearchBar, the moment I click on it, I get the "fatal error: array cannot be bridged from Objective-C". I believe this is happening because the arrays used in the tutorial is a different type compared to the ones I have used.
UPDATE
Firstly, the main issue was that the fact I had a different type of an array compared to the tutorial; so I had changed the var filteredAnimal = [String]() to var filteredArray = [[String:AnyObject]]() since the data source/JSON file had numerous arrays of data. And treat it as AnyObject due to that, which then allowed me to call the specific information from the JSON file.
The reason why I was returned with an empty array was because I did not assign the searched values in the empty array within the updateSearchResultsController method, so I had added the following just before reloading the table data - filteredArray = arrays as [[String:AnyObject]]
This still did not solve the issue as I had to change the if statement within the numberOfRowsInSection from tableView == self.searchDisplayController?.searchResultsTableView to self.resultSearchController.active, which then retrieved me an actual result.
However the search itself is still not working, but majority of the issues have been resolved. When tapping the search bar, it only retrieves the final index of the array, and when searching anything, it removes the value. I will continue to work on it and hopefully resolve the matter soon. Thanks for the help so far!
Below link may help you to solve your issue:
How can I fix "fatal error: can't index empty buffer"
in your code
let arrays = (array).filteredArrayUsingPredicate(searchPredicate!)
will create new reference of arrays.
Since the last update (which has played a major part for the solution) all I did was change the NSPredicate query from SELF CONTAINS[c] %# to name CONTAINS[c] %# and now when searching, it displays the correct matching object based on its name.

Swift updating UITableView with new data

I'm trying to repopulate my UITableView with data from another JSON call.
However my current setup doesn't seem to work, and while there are many identical questions on SO the answers I could find I've already tried.
I'm saving my API data in CoreData entity objects. And I'm filling my UITableView with my CoreData entities.
In my current setup I have 3 different API Calls that has a different amount of data, and of course different values. I need to be able to switch between these 3 datasets, and that's what I'm trying to accomplish now. (so far without progress).
I have a function called "loadSuggestions", which is where I assume my fault lies.
First I check for an internet connection.
I set the managedObjectContext
I check what API I need to call (This is determined before the function is called, and I checked that it works as intended)
I delete all the current data from the entity that it's trying to call. (I also tried to delete the data from the last data the UITableView had loaded. That didn't change anything). I also checked that this works. After deleting the data, I checked that it prints out an empty array, I also tried logging the objects it deletes to make sure.
I then fetch the new data, save it into temporary variables. Then save it to my core data.
Then I make my second API call (dependant on a variable from the first one), fetch that data and save it the same way.
I append the object to the array the UITableView fills it's cells from. (I checked that it prints out correctly as well)
And lastly I reload the tableView. (doesn't change a thing)
Here's the function:
func loadSuggestions() {
println("----- Loading Data -----")
// Check for an internet connection.
if Reachability.isConnectedToNetwork() == false {
println("ERROR: -> No Internet Connection <-")
} else {
// Set the managedContext again.
managedContext = appDelegate.managedObjectContext!
// Check what API to get the data from
if Formula == 0 {
formulaEntity = "TrialFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/entry_weekly.json")
} else if Formula == 1 {
formulaEntity = "ProFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/entry_weekly.json")
} else if Formula == 2 {
formulaEntity = "PremiumFormulaStock"
formulaAPI = NSURL(string: "http://api.com/json/proff_weekly.json")
println("Setting Entity: \(formulaEntity)")
} else if Formula == 3 {
formulaEntity = "PlatinumFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/fund_weekly.json")
}
// Delete all the current objects in the dataset
let fetchRequest = NSFetchRequest(entityName: formulaEntity)
let a = managedContext.executeFetchRequest(fetchRequest, error: nil) as! [NSManagedObject]
for mo in a {
managedContext.deleteObject(mo)
}
// Removing them from the array
stocks.removeAll(keepCapacity: false)
// Saving the now empty context.
managedContext.save(nil)
// Set up a fetch request for the API data
let entity = NSEntityDescription.entityForName(formulaEntity, inManagedObjectContext:managedContext)
var request = NSURLRequest(URL: formulaAPI!)
var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
var formula = JSON(data: data!)
// Loop through the api data.
for (index: String, actionable: JSON) in formula["actionable"] {
// Save the data into temporary variables
stockName = actionable["name"].stringValue
ticker = actionable["ticker"].stringValue
action = actionable["action"].stringValue
suggestedPrice = actionable["suggested_price"].floatValue
weight = actionable["percentage_weight"].floatValue
// Set up CoreData for inserting a new object.
let stock = NSManagedObject(entity: entity!,insertIntoManagedObjectContext:managedContext)
// Save the temporary variables into coreData
stock.setValue(stockName, forKey: "name")
stock.setValue(ticker, forKey: "ticker")
stock.setValue(action, forKey: "action")
stock.setValue(suggestedPrice, forKey: "suggestedPrice")
stock.setValue(weight, forKey: "weight")
// Get ready for second API call.
var quoteAPI = NSURL(string: "http://dev.markitondemand.com/Api/v2/Quote/json?symbol=\(ticker)")
// Second API fetch.
var quoteRequest = NSURLRequest(URL: quoteAPI!)
var quoteData = NSURLConnection.sendSynchronousRequest(quoteRequest, returningResponse: nil, error: nil)
if quoteData != nil {
// Save the data from second API call to temporary variables
var quote = JSON(data: quoteData!)
betterStockName = quote["Name"].stringValue
lastPrice = quote["LastPrice"].floatValue
// The second API call doesn't always find something, so checking if it exists is important.
if betterStockName != "" {
stock.setValue(betterStockName, forKey: "name")
}
// This can simply be set, because it will be 0 if not found.
stock.setValue(lastPrice, forKey: "lastPrice")
} else {
println("ERROR ----------------- NO DATA for \(ticker) --------------")
}
// Error handling
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
// Append the object to the array. Which fills the UITableView
stocks.append(stock)
}
// Reload the tableview with the new data.
self.tableView.reloadData()
}
}
Currently, when I push to this viewController, this function is called in viewDidAppear like so:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
tableView.allowsSelection = true
if isFirstTime {
loadSuggestions()
isFirstTime = false
}
}
It populates the tableView correctly and everything seems to work as planned.
However if I open my slide-out menu and call a function to load different data, nothing happens, here's an example function:
func platinumFormulaTapGesture() {
// Menu related actions
selectView(platinumFormulaView)
selectedMenuItem = 2
// Setting the data to load
Formula = 3
// Sets the viewController. (this will mostly be the same ViewController)
menuTabBarController.selectedIndex = 0
// Set the new title
navigationController?.navigationBar.topItem!.title = "PLATINUM FORMULA"
// And here I call the loadSuggestions function again. (this does run)
SuggestionsViewController().loadSuggestions()
}
Here's the 2 relevant tableView functions:
number of Rows:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stocks.count
}
And cellForRowAtIndexPath, (this is where I set up my cells with the CoreData)
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("com.mySuggestionsCell", forIndexPath: indexPath) as! mySuggestionsCell
let formulaStock = stocks[indexPath.row]
cell.stockNameLabel.text = formulaStock.valueForKey("name") as! String!
cell.tickerLabel.text = formulaStock.valueForKey("ticker") as! String!
action = formulaStock.valueForKey("action") as! String!
suggestedPrice = formulaStock.valueForKey("suggestedPrice") as! Float
let suggestedPriceString = "Suggested Price\n$\(suggestedPrice.roundTo(2))" as NSString
var suggestedAttributedString = NSMutableAttributedString(string: suggestedPriceString as String)
suggestedAttributedString.addAttributes(GrayLatoRegularAttribute, range: suggestedPriceString.rangeOfString("Suggested Price\n"))
suggestedAttributedString.addAttributes(BlueHalisRBoldAttribute, range: suggestedPriceString.rangeOfString("$\(suggestedPrice.roundTo(2))"))
cell.suggestedPriceLabel.attributedText = suggestedAttributedString
if action == "SELL" {
cell.suggestionContainer.backgroundColor = UIColor.formulaGreenColor()
}
if let lastPrice = formulaStock.valueForKey("lastPrice") as? Float {
var lastPriceString = "Last Price\n$\(lastPrice.roundTo(2))" as NSString
var lastAttributedString = NSMutableAttributedString(string: lastPriceString as String)
lastAttributedString.addAttributes(GrayLatoRegularAttribute, range: lastPriceString.rangeOfString("Last Price\n"))
percentDifference = ((lastPrice/suggestedPrice)*100.00)-100
if percentDifference > 0 && action == "BUY" {
lastAttributedString.addAttributes(RedHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference <= 0 && percentDifference > -100 && action == "BUY" {
lastAttributedString.addAttributes(GreenHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference <= 0 && percentDifference > -100 && action == "SELL" {
lastAttributedString.addAttributes(RedHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference == -100 {
lastPriceString = "Last Price\nN/A" as NSString
lastAttributedString = NSMutableAttributedString(string: lastPriceString as String)
lastAttributedString.addAttributes(GrayLatoRegularAttribute, range: lastPriceString.rangeOfString("Last Price\n"))
lastAttributedString.addAttributes(BlackHalisRBoldAttribute, range: lastPriceString.rangeOfString("N/A"))
}
cell.lastPriceLabel.attributedText = lastAttributedString
} else {
println("lastPrice nil")
}
weight = formulaStock.valueForKey("weight") as! Float
cell.circleChart.percentFill = weight
let circleChartString = "\(weight.roundTo(2))%\nWEIGHT" as NSString
var circleChartAttributedString = NSMutableAttributedString(string: circleChartString as String)
circleChartAttributedString.addAttributes(BlueMediumHalisRBoldAttribute, range: circleChartString.rangeOfString("\(weight.roundTo(2))%\n"))
circleChartAttributedString.addAttributes(BlackSmallHalisRBoldAttribute, range: circleChartString.rangeOfString("WEIGHT"))
cell.circleChartLabel.attributedText = circleChartAttributedString
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
I define my appDelegate as the very first thing in my class:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var managedContext = NSManagedObjectContext()
I think that's all the code that could possibly be the cause of the bug. Again I think the most likely cause would be in the loadSuggestions function.
To force update the tableView I also tried calling setNeedsDisplay and setNeedsLayout both on self.view and tableView, neither of which seemed to do anything at all.
Any advice in figuring out why this tableView refuses to update would be an enormous help!
And I apologize for the walls of code, but I havn't been able to find the exact origin of the issue.
This line in the platinumFormulaTapGesture function is incorrect,
SuggestionsViewController().loadSuggestions()
This creates a new instance of SuggestionsViewController, which is not the one you have on screen. You need to get a pointer to the one you have. How you do that depends on your controller hierarchy, which you haven't explained fully enough.

Deleting from UISearchController's filtered search results

I have a tableView sourcing its cell content from CoreData and have been replacing the SearchDisplayController (deprecated) with the new SearchController. I am using the same tableView controller to present both the full list of objects and also the filtered/searched objects.
I have managed to get the search/filtering working fine and can move from the filtered list to detail views for those items, then edit and save changes back successfully to the filtered tableView. My problem is swiping to delete cells from the filtered list causes an run time error. Previously with the SearchDisplayController I could do this easily as I had access to the SearchDisplayController's results tableView and so the following (pseudo) code would work fine:
func controllerDidChangeContent(controller: NSFetchedResultsController) {
// If the search is active do this
searchDisplayController!.searchResultsTableView.endUpdates()
// else it isn't active so do this
tableView.endUpdates()
}
}
Unfortunately no such tableView is exposed for the UISearchController and Im at a loss. I have tried making the tableView.beginUpdates() and tableView.endUpdates() conditional on tableView not being the search tableView but with no success.
For the record this is my error message:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.65/UITableView.m:1582
* EDIT *
My tableView uses a FetchedResultsController to populate itself from CoreData. This tableViewController also the one used by the SearchController to display filtered results.
var searchController: UISearchController!
Then in ViewDidLoad
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
and
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchText = self.searchController?.searchBar.text
if let searchText = searchText {
searchPredicate = searchText.isEmpty ? nil : NSPredicate(format: "locationName contains[c] %#", searchText)
self.tableView.reloadData()
}
}
So far as the error message is concerned, I'm not sure how much I can add. The app hangs immediately after pressing the red delete button (Which remains showing) revealed by swiping. This is the thread error log for 1 - 5. The app seems to hang on number 4.
#0 0x00000001042fab8a in objc_exception_throw ()
#1 0x000000010204b9da in +[NSException raise:format:arguments:] ()
#2 0x00000001027b14cf in -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] ()
#3 0x000000010311169a in -[UITableView _endCellAnimationsWithContext:] ()
#4 0x00000001019b16f3 in iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () at /Users/neilmckay/Dropbox/Programming/My Projects/iLocations/iLocations/LocationViewController.swift:303
#5 0x00000001019b178a in #objc iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () ()
I hope some of this helps.
* EDIT 2 *
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
location.removePhotoFile()
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(location)
var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}
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 {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
return filteredObjects == nil ? 0 : filteredObjects!.count
}
}
// MARK: - NSFetchedResultsController methods
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest = NSFetchRequest()
// Edit the entity name as appropriate.
let entity = NSEntityDescription.entityForName("Location", inManagedObjectContext: self.managedObjectContext!)
fetchRequest.entity = entity
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
if sectionNameKeyPathString1 != nil {
let sortDescriptor1 = NSSortDescriptor(key: sectionNameKeyPathString1!, ascending: true)
let sortDescriptor2 = NSSortDescriptor(key: sectionNameKeyPathString2!, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
} else {
let sortDescriptor = NSSortDescriptor(key: "firstLetter", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
}
var sectionNameKeyPath: String
if sectionNameKeyPathString1 == nil {
sectionNameKeyPath = "firstLetter"
} else {
sectionNameKeyPath = sectionNameKeyPathString1!
}
// 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: sectionNameKeyPath, cacheName: nil /*"Locations"*/)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
var error: NSError? = nil
if !_fetchedResultsController!.performFetch(&error) {
fatalCoreDataError(error)
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController? = nil
func controllerWillChangeContent(controller: NSFetchedResultsController) {
if searchPredicate == nil {
tableView.beginUpdates()
} else {
(searchController.searchResultsUpdater as LocationViewController).tableView.beginUpdates()
}
// 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 LocationViewController).tableView
}
switch type {
case .Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
return
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath) {
var tableView = UITableView()
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).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)")
if searchPredicate == nil {
let cell = tableView.cellForRowAtIndexPath(indexPath) as LocationCell
let location = controller.objectAtIndexPath(indexPath) as Location
cell.configureForLocation(location)
} else {
let cell = tableView.cellForRowAtIndexPath(searchIndexPath) as LocationCell
let location = controller.objectAtIndexPath(searchIndexPath) as Location
cell.configureForLocation(location)
}
case .Move:
println("*** NSFetchedResultsChangeMove (object)")
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
if searchPredicate == nil {
tableView.endUpdates()
} else {
(searchController.searchResultsUpdater as LocationViewController).tableView.endUpdates()
}
}
The problem arises because of a mismatch between the indexPath used by the fetched results controller and the indexPath for the corresponding row in the tableView.
Whilst the search controller is active, the existing tableView is reused to display the search results. Hence your logic to differentiate the two tableViews:
if searchPredicate == nil {
tableView = self.tableView
} else {
tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}
is unnecessary. It works, because you set searchController.searchResultsUpdater = self when you initialise the searchController, so there is no need to change it, but the same tableView is used in either case.
The difference lies in the way the tableView is populated whilst the searchController is active. In that case, it looks (from the numberOfRowsInSection code) as though the filtered results are all displayed in one section. (I assume cellForRowAtIndexPath works similarly.) Suppose you delete the item at section 0, row 7, in the filtered results. Then commitEditingStyle will be called with indexPath 0-7, and the following line:
let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
will try to get the object at index 0-7 from the FRC. But the item at index 0-7 of the FRC might be a completely different object. Hence you delete the wrong object. Then the FRC delegate methods fire, and tell the tableView to delete the row at index 0-7. Now, if the object really deleted was NOT in the filtered results, then the count of rows will be unchanged, even though a row has been deleted: hence the error.
So, to fix it, amend your commitEditingStyle so that it finds the correct object to delete, if the searchController is active:
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
var location : Location
if searchPredicate == nil {
location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
} else {
let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
return self.searchPredicate!.evaluateWithObject($0)
}
location = filteredObjects![indexPath.row] as Location
}
location.removePhotoFile()
let context = self.fetchedResultsController.managedObjectContext
context.deleteObject(location)
var error: NSError? = nil
if !context.save(&error) {
abort()
}
}
}
I haven't been able to test the above; apologies if some errors slipped in. But it should at least point in the right direction; hope it helps. Note that similar changes may be required in some of the other tableView delegate/datasource methods.

Resources