I have a very basic NSFetchedResultsController that shows the data to the user in a UITableView, allows the user to add a new entity and so forth.
However, whenever I add a new entity, my app crashes (or sometimes just warns) with the following message:
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (2), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
Notice that even thought the numberOfRows have been updated from 2 to 3, the insertion/deletion thing still says (0 inserted, 0 deleted). So my best understanding is that the NSFetchedResultsController is not noticing the changes or something.
My code for NSFetchedResultsController is:
func fetch(frcToFetch: NSFetchedResultsController) {
do {
try frcToFetch.performFetch()
} catch {
return
}
}
func fetchRequest() -> NSFetchRequest {
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "ItemInfo")
// Add Sort Descriptors
let nameSortDescriptor = NSSortDescriptor(key: "iName", ascending: true)
fetchRequest.sortDescriptors = [nameSortDescriptor]
return fetchRequest
}
func getFRC() -> NSFetchedResultsController {
if let context = self.context{
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: context, sectionNameKeyPath: "iName.stringGroupByFirstInitial", cacheName: nil)
fetchedResultsController.delegate = self
}
return fetchedResultsController
}
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:
if let indexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Update:
if let indexPath = indexPath {
let cell = tableView.cellForRowAtIndexPath(indexPath)! as UITableViewCell
configureCell(cell, atIndexPath: indexPath)
}
break;
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break;
}
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
return 0
}
And the code to insert new record is:
let entity: NSEntityDescription.entityForName("ItemInfo", inManagedObjectContext: self.context!)!
let record = NSManagedObject(entity: entity, insertIntoManagedObjectContext: self.context!)
record.setValue(name, forKey: "iName")
record.setValue(self.billingMode.text, forKey: "iBillingMode")
do {
// Save Record
try record.managedObjectContext?.save()
try self.context!.save()
// Dismiss View Controller
dismissViewControllerAnimated(true, completion: nil)
} catch {
let saveError = error as NSError
print("\(saveError), \(saveError.userInfo)")
// Show Alert View
showAlertWithTitle("Unexpected Error", message: "Your data could not be saved. Please try again later.", cancelButtonTitle: "Done")
}
Note that the self.context variable is passed from the actual or master view controller that has the NSFetchedResultsController.
Note that the problem is with the number of sections not the number of rows. You need to implement:
(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
Related
I am getting server response as like below
[[“image_url": https://someurl1, "title": Title1], ["image_url": https://someurl2, "title": Title2], ["image_url": https://someurl3, "title": Title3], ["image_url": https://someurl4, "title": Title4]]
I am storing this data to core data by loop.
So, I am trying to fetch this data from core data using NSFetchedResultsController and I am trying to display in Tableview
func saveDataToDataBase(json: [[String: Any]]) {
for eachData in json {
Categories.saveCategories(jsonResponse: eachData, completionHandler: { [weak self] success in
if (success) {
}
})
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.fetchData()
}
}
func fetchData() {
fetchedResultsController = NSFetchedResultsController(fetchRequest: allCategoriesData(), managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController?.delegate = self
do {
try fetcheResultsController()!.performFetch()
} catch let error as NSError {
print("Could not fetch. \(error), \(error.localizedDescription)")
}
}
func allCategoriesData() -> NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: HomeKeyConstant.Entity_Categories)
let sortDescriptor = NSSortDescriptor(key: HomeKeyConstant.categories_Id, ascending: true)
fetchRequest.predicate = nil
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
// UITableviewDelegate & Data Source methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
guard let sectionCount = fetchedResultsController.sections?.count else {
return 0
}
return sectionCount
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.fetchedResultsController == nil {
} else {
if let sectionData = fetchedResultsController.sections?[section] {
return sectionData.numberOfObjects
}
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = homeTableView.dequeueReusableCell(withIdentifier: HomeKeyConstant.key_Cell_Identifier, for: indexPath) as! HomeTableViewCell
self.configureCell(cell, at: indexPath)
return cell
}
func configureCell(_ cell: HomeTableViewCell?, at indexPath: IndexPath?) {
let category = fetchedResultsController.object(at: indexPath!) as! Categories
cell?.tableTitleLabel.text = category.value(forKey: HomeKeyConstant.categories_Title) as? String
cell?.tableDescriptionLabel.text = category.value(forKey: HomeKeyConstant.ctegories_Description) as? String
}
// MARK: - FetchedResultsController Delegate
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
homeTableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
homeTableView.endUpdates()
}
func controller(controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .insert:
homeTableView.insertSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .automatic)
case .delete:
homeTableView.deleteSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .automatic)
default: break
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
homeTableView.insertRows(at: [(newIndexPath! as IndexPath)], with: .automatic)
case .delete:
homeTableView.deleteRows(at: [(indexPath! as IndexPath)], with: .automatic)
default: break
}
}
Actually, I am storing 4 indexes of data. While fetching its showing as 50 indexes and crashig
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (50) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
I have a doubt, In my tableview I dont have edit/delete/insert/update options. Just I have to fetch data from database and I have to show in tableview.
So, these NSFetchedResultsControllerDelegate methods are required to implement or not required?
How to fix this crash?
I think the problem here is you you save data to your database. When you saved, you don't wait for them to finish and update your TableviewController. So if you have a little data to save. This code will work otherwise, it's will be crashed.
I have updated this question as I found another key to this problem. It seems that when I add something to CoreData, the tableview does not reload the data, even though I can print out the objects that CoreData has saved. So I know the data is correct, but the function
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell never gets called.
It's also worth noting that if I stop the application and reopen it , the table view displays the correct amount of cells and information. So it seems the the data only loads property on initial build.
I have a UITableView that is being populated by CoreData and also using a FetchedResultsController. I added the FRC delegate methods and I tried to force the tableView.reloadData() method, but that doesn't seem to work. The data shows up, if I stop the build and rebuild the project.
Here are my delegate methods that I am using:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
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: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
tableview.reloadData()
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
When I come back to this View I would like to force the tableview to reload its data.
ViewwillAppear method:
override func viewWillAppear(_ animated: Bool) {
// load the data
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
fetchRequest.predicate = NSPredicate(format:"statement.#sum.amountOwed >= 0")
let sort = NSSortDescriptor(key: #keyPath(Person.name), ascending: true)
fetchRequest.sortDescriptors = [sort]
positiveFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: nil, cacheName: nil)
do{
try positiveFetchedResultsController.performFetch()
}catch let error as NSError{
print("Fetching error: \(error), \(error.userInfo)")
}
let negativFetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
negativFetchRequest.predicate = NSPredicate(format:"statement.#sum.amountOwed < 0")
let negativeSort = NSSortDescriptor(key: #keyPath(Person.name), ascending: true)
negativFetchRequest.sortDescriptors = [negativeSort]
negativeFetchedResultsController = NSFetchedResultsController(fetchRequest: negativFetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: nil, cacheName: nil)
do{
try negativeFetchedResultsController.performFetch()
}catch let error as NSError{
print("Fetching error: \(error), \(error.userInfo)")
}
positiveFetchedResultsController.delegate = self
negativeFetchedResultsController.delegate = self
print("\(positiveFetchedResultsController.fetchedObjects!.count) positive fetch count")
//print("\(positiveFetchedResultsController.fetchedObjects![0].statement!.count) positive statements count")
print("\(negativeFetchedResultsController.fetchedObjects!.count) negative fetch count")
//print("\(negativeFetchedResultsController.fetchedObjects![0].statement!.count) negative statements count")
tableView.reloadData()
}
Here is my cellForRowAt method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "personCell", for: indexPath) as! PersonTableViewCell
switch(indexPath.section) {
case 0:
//print("this is section 0")
let person = positiveFetchedResultsController.object(at: indexPath)
cell.personName.text = person.name
//print("\(person.name!) is the name")
//print("\(person.statement!.count) postive person statement count")
if(person.statement!.count == 0){
print("default")
cell.statementAmount.text = "$0.00"
}
else{
//print("\(person.name!) has \(person.statement!.count) statement count")
let amountTotal = person.value(forKeyPath: "statement.#sum.amountOwed") as? Decimal
//print("\(amountTotal!) this is the total")
cell.statementAmount.text = convertStringToDollarString(amountToConvert: String(describing: amountTotal!))
}
case 1:
print("this is section 1")
//print("\(negativeFetchedResultsController.object(at: [indexPath.row,0])) objects fetched")
//print("\(indexPath.section) section number")
//print("\(indexPath.row) row number")
let person = negativeFetchedResultsController.fetchedObjects![indexPath.row]
cell.personName.text = person.name
print("\(person.name!) is the name")
print("\(person.statement!.count) negative person statement count")
if(person.statement!.count == 0){
cell.statementAmount.text = "$0.00"
}
else{
//print("\(person.name!) has \(person.statement!.count) statement count")
let amountTotal = person.value(forKeyPath: "statement.#sum.amountOwed") as? Decimal
print("\(amountTotal!) this is the total")
cell.statementAmount.text = String(describing: amountTotal!)
cell.statementAmount.text = convertStringToDollarString(amountToConvert: String(describing: amountTotal!))
}
cell.backgroundColor = Styles.redColor();
let bgColorView = UIView()
bgColorView.backgroundColor = Styles.darkRedColor()
cell.selectedBackgroundView = bgColorView
default: cell.personName.text = "hello"
}
return cell
}
maybe try this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
}
To do this you can simply add fetchedResultsController.delegate = nil in viewWillAppear function before calling table reloadData.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchedResultsController.delegate = nil
self.tableView.reloadData()
}
I have an entity named ToDoItem. Which has three properties:
#NSManaged var toDoString: String?
#NSManaged var creationDate: NSDate?
#NSManaged var isComplete: NSNumber?
I'm trying to create a NSFetchedResultsController that will display with two section depending on the isComplete variable. I'm able to do that successfully buy doing this:
lazy var fetchedResultsController: NSFetchedResultsController = {
let questionAnswerFetchRequest = NSFetchRequest(entityName: "ToDoItem")
let questionAnswerFetchSortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
questionAnswerFetchRequest.sortDescriptors = [questionAnswerFetchSortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: questionAnswerFetchRequest,
managedObjectContext: (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext,
sectionNameKeyPath:"isComplete",
cacheName: nil)
frc.delegate = self
return frc
}()
I've also implemented the delete methods like this:
//
// MARK: Fetched Results Controller Delegate Methods
//
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
func controller(controller: NSFetchedResultsController,
didChange sectionInfo: NSFetchedResultsSectionInfo,
atSectionIndex sectionIndex: Int,
for type: NSFetchedResultsChangeType) {
switch (type) {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
break
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
break
default:
break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch (type) {
case .Insert:
if let indexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
break;
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}
break;
case .Move:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
break;
}
}
The problem is that when I update the isComplete property I get this error:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of sections. The number of sections contained in the table view after
the update (1) must be equal to the number of sections contained in
the table view before the update (0), plus or minus the number of
sections inserted or deleted (0 inserted, 0 deleted). with userInfo
(null)
Which I understand what this is saying, I just don't understand why it causing that error. I've implemented the delegate methods per the documentation.....
I've updated my NSFetchedResultsController creation to add another sortDescriptor per #wain's answer like so:
lazy var fetchedResultsController: NSFetchedResultsController = {
let questionAnswerFetchRequest = NSFetchRequest(entityName: "ToDoItem")
let isCompleteSortDescriptor = NSSortDescriptor(key: "isComplete", ascending: false)
let creationDateSortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
questionAnswerFetchRequest.sortDescriptors = [isCompleteSortDescriptor, creationDateSortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: questionAnswerFetchRequest,
managedObjectContext: (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext,
sectionNameKeyPath:"isComplete",
cacheName: nil)
frc.delegate = self
return frc
}()
I've added some breakpoints and it turns out that this delegate method is never being called:
func controller(controller: NSFetchedResultsController,
didChange sectionInfo: NSFetchedResultsSectionInfo,
atSectionIndex sectionIndex: Int,
for type: NSFetchedResultsChangeType) {
switch (type) {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
break
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
break
default:
break
}
}
Which is leading to the same error as posted above. Any ideas on why that would never be called??
You need to add a sort descriptor for isComplete. It should be the first sort descriptor, then have your date one.
Basically, the sort and the sections need to be compatible, and yours currently aren't.
I'm making a simple application where user can add habits and complete theme using swift and realm for database
Everything is working fine except when I edit the state and delete the object
The application crashes with RLMException reason: 'Index 0 is out of bounds (must be less than 0)'
I noticed that this only happens when the item is the only cell in tableView
I'd appreciate if anyone could help me with this as I've been struggling with it for the entire day
The Habit Object is:
class Habit: Object {
dynamic var id = 0
dynamic var name = ""
dynamic var state = ""
convenience init(name: String) {
self.init()
self.id = self.incrementaID()
self.name = name
self.state = "in Progress"
}
override class func primaryKey() -> String? {
return "id"
}
private func incrementaID() -> Int {
let realm = try! Realm()
let value = realm.objects(Habit).map{$0.id}.maxElement() ?? 0
return value + 1
}}
I'm using RealmSwift, SwiftFetchedResultsController to automatically update a tableView, swift 2 and Xcode 7
Here is the TableViewController relevant code in MyHabitsViewController
override func viewDidLoad() {
super.viewDidLoad()
// Get the default Realm
realm = try! Realm()
let predicate = NSPredicate(value: true)
let fetchRequest = FetchRequest<Habit>(realm: realm, predicate: predicate)
let sortDescriptor = SortDescriptor(property: "name", ascending: true)
let sortDescriptorSection = SortDescriptor(property: "state", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptorSection, sortDescriptor]
self.fetchedResultsController = FetchedResultsController<Habit>(fetchRequest: fetchRequest, sectionNameKeyPath: "state", cacheName: nil)
self.fetchedResultsController!.delegate = self
self.fetchedResultsController!.performFetch()
}
FetchedResultsControllerDelegate methods:
func controllerWillChangeContent<T : Object>(controller: FetchedResultsController<T>) {
tableView.beginUpdates()
}
func controllerDidChangeSection<T : Object>(controller: FetchedResultsController<T>, section: FetchResultsSectionInfo<T>, sectionIndex: UInt, changeType: NSFetchedResultsChangeType) {
let indexSet = NSIndexSet(index: Int(sectionIndex))
switch changeType {
case .Insert:
tableView.insertSections(indexSet, withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(indexSet, withRowAnimation: .Fade)
case .Update:
tableView.reloadSections(indexSet, withRowAnimation: .Fade)
case .Move:
tableView.deleteSections(indexSet, withRowAnimation: .Fade)
tableView.insertSections(indexSet, withRowAnimation: .Fade)
}
}
func controllerDidChangeObject<T : Object>(controller: FetchedResultsController<T>, anObject: SafeObject<T>, indexPath: NSIndexPath?, changeType: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch changeType {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent<T : Object>(controller: FetchedResultsController<T>) {
tableView.endUpdates()
}
UITableViewDelegate & UITableViewDataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController!.numberOfSections()
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return fetchedResultsController!.titleForHeaderInSection(section)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.fetchedResultsController!.numberOfRowsForSectionIndex(section)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("HabitInfoCell", forIndexPath: indexPath) as! HabitInfoCell
let habit = self.fetchedResultsController!.objectAtIndexPath(indexPath)!
cell.configure(habit)
// add delete button
let deleteButton = MGSwipeButton() {
try! self.realm.write {
self.realm.delete(habit)
}
}
cell.leftButtons = [deleteButton]
// add complete button
let completeButton = MGSwipeButton() {
try! self.realm.write {
habit.state = "Completed"
}
}
cell.rightButtons = [completeButton]
return cell
}
This error is shown when you pass an index greater than the total count present in Realm Object.
Check whether you Realm DB contains that entry which you are displaying on Tableview.
Download Realm Browser on Mac: Link
I had the same problem, I observed that the entry was not made to Realm DB.
Thinking that Realm already has the entry, I tried to fetch. Thus resulting in
RLMException reason: 'Index 0 is out of bounds (must be less than 0)'
Log Home Directory on console to get the realm.db file using this code:
let path = NSHomeDirectory().appending("/Documents/")
print(path)
Say there's a class called Post (as in a forum post).
class Post: NSManagedObject {
#NSManaged var id: NSNumber
#NSManaged var title: String?
#NSManaged var body: String?
#NSManaged var voteCount: NSNumber?
#NSManaged var thread: Thread?
}
And a Thread class.
class Thread: NSManagedObject {
#NSManaged var topic: String
#NSManaged var id: NSNumber
#NSManaged var posts: NSSet
}
A thread contains a set of Post objects.
From the local core data database I retrieve an array of Thread objects to var threads = [Thread]().
Now I need to filter out posts in threads that have a vote count of more than 0. In other words I need an array of Thread objects excluding the Posts with 0 votes.
Here's what I've tried so far.
var filteredThreads = threads.filter() { $0.posts.filter() { $0.voteCount > 0 } }
But I get the error 'Array<(Post)>' is not convertible to 'Bool'.
It seems I cannot use nested filters like this. Or am I doing it wrong?
Edit: I also tried the below code. No compile time errors but it doesn't filter the returned results array as expected.
threads = items.filter() {
for post in $0.posts.allObjects as [Post] {
if post.voteCount!.integerValue > 0 {
return true
}
}
return false
}
I uploaded a Xcode project here demonstrating the issue. Any help is appreciated.
Thanks.
Attempt 2: I tried iterating through the Thread objects I receive and filter out the Posts inside it. But I don't know how to add the filtered array back to the Thread object.
if let items = viewModel.getThreads() {
for thread in items {
let posts = thread.posts.allObjects as [Post]
var s = posts.filter() {
if $0.voteCount!.integerValue > 0 {
return true
} else {
return false
}
}
}
}
Your method cannot work. You cannot filter the Thread objects in such a
way that you get an array of "modified" Thread objects which are only
related to the Posts with positive vote count. Filtering gives always a
subset of the original objects, but those objects are not modified.
The proper way to achieve what you want is to fetch all Post objects
with positive vote count, and group them into sections according to
their Thread.
The easiest way to do so is a NSFetchedResultsController.
Here is a quick-and-dirty version of your ForumViewController that
uses a fetched results controller. Most of it is the boilerplate code
that you get with a standard "Master-Detail Application + Core Data"
in Xcode. It certainly can be improved but hopefully should get you
on the right track.
To make this compile in your sample project, managedObjectContext
in the ForumViewModel needs to be a public property.
Alternatively, you can move the fetched results controller creation
into the ForumViewModel class.
// ForumViewController.swift:
import UIKit
import CoreData
class ForumViewController: UITableViewController, NSFetchedResultsControllerDelegate {
private let viewModel = ForumViewModel()
var fetchedResultsController: NSFetchedResultsController {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
// Fetch "Post" objects:
let fetchRequest = NSFetchRequest(entityName: "Post")
// But only those with positive voteCount:
let predicate = NSPredicate(format: "voteCount > 0")
fetchRequest.predicate = predicate
// First sort descriptor must be the section key:
let topicSort = NSSortDescriptor(key: "thread.topic", ascending: true)
// Second sort descriptor to sort the posts in each section:
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [topicSort, titleSort]
// Fetched results controller with sectioning according the thread.topic:
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: viewModel.managedObjectContext,
sectionNameKeyPath: "thread.topic", cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
var error: NSError? = nil
if !_fetchedResultsController!.performFetch(&error) {
abort()
}
return _fetchedResultsController!
}
var _fetchedResultsController: NSFetchedResultsController? = nil
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections?.count ?? 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
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
}
func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) {
let post = self.fetchedResultsController.objectAtIndexPath(indexPath) as Post
cell.textLabel!.text = post.title
cell.detailTextLabel!.text = "\(post.voteCount!)"
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionInfo = self.fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo
return sectionInfo.name
}
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
default:
return
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
default:
return
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
}
Result:
$0.posts.filter() { $0.voteCount > 0 } returns an array of Post that voteCount is positive. You have to check the count of it:
var filteredThreads = threads.filter() {
$0.posts.filter({ $0.voteCount > 0 }).count > 0
// ^^^^^^^^^^
}
But, this unconditionally iterates all posts. Instead, you should return true as soon as possible:
var filteredThreads = threads.filter() {
for p in $0.posts {
if p.voteCount > 0 {
return true
}
}
return false
}
That function inside filter must return something with type 'Bool'
I think this following code might come handy for you
var filteredThreads = threads.filter({
var result = false;
for (var i = 0; i<$0.posts.count;i++) {
if ($0.posts[i].voteCount > 0){
result = true;
}
}
return result
})
This blog post might become usefull