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.
Related
Im getting user's input and creating CoreData entity for it, and put in to the tableView, but before i need to designate some properties for entity by making a couple network requests. And there is a problem. First entity is always nil (and create a empty cell for table view) but if i save context and open app again - here it is, entity is ok. If i create second (and so on) entity it works fine. Its also seems like table always create am empty cell for start and then delete it and create proper cell…
I cant get why completion handlers doesn't work properly and network requests makes asynchronously. Please help me to understand.
Creation metod starts in textFieldShouldReturn
import UIKit
import CoreData
class ViewController: UIViewController, UITextFieldDelegate {
var managedObjectContext: NSManagedObjectContext?
lazy var fetchedResultsController: NSFetchedResultsController<Word> = {
let fetchRequest: NSFetchRequest<Word> = Word.fetchRequest()
let createdSort = NSSortDescriptor(key: "created", ascending: false)
let idSort = NSSortDescriptor(key: "id", ascending: false)
fetchRequest.sortDescriptors = [createdSort, idSort]
var fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: AppDelegate.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
private var hasWords: Bool {
guard let fetchedObjects = fetchedResultsController.fetchedObjects else { return false }
return fetchedObjects.count > 0
}
let creator = WordManager.shared
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
managedObjectContext = AppDelegate.viewContext
fetchWords()
updateView()
}
func updateView() {
tableView.isHidden = !hasWords
messageLabel.isHidden = hasWords
}
............
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let managedObjectContext = managedObjectContext else { return false }
guard let title = textField.text, !title.isEmpty, !title.hasPrefix(" ") else {
showAllert(title: "Warning!", message: "Title may not to be nil!")
return false
}
let word = Word(context: managedObjectContext)
guard let fetchedWordsCount = fetchedResultsController.fetchedObjects?.count else { return false }
creator.createWord(title: title, counter: fetchedWordsCount, word: word)
self.updateView()
textField.text = nil
return true
}
func fetchWords() {
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Cant fetch words. Error \(error.localizedDescription)")
}
}
}
Creating entity
import CoreData
import UIKit
class WordManager {
static var shared = WordManager()
private init() {}
private var api = DictionaryAPI()
private var api2 = MeaningAPI()
func createWord(title: String, counter: Int, word: Word) {
self.api.fetch(word: title, completion: { translation in
self.api2.fetchTranscription(word: title, completion: { transcription in
word.id = Int16(counter) + 1
word.title = title
word.translation = translation
word.transcription = "[\(transcription)]"
word.created = NSDate()
})
})
}
}
Nothing interesting in tableView
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchedResultsController.sections else { return 0 }
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = fetchedResultsController.sections?[section] else { return 0 }
return section.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath) as! MainCell
let word = fetchedResultsController.object(at: indexPath)
cell.configure(with: word)
return cell
}
}
Configure cell method
func configure(with word: Word) {
titleField.text = word.title
translationField.text = word.translation
transcriptionLabel.text = word.transcription
totalCounter.text = "\(word.showCounter)"
correctCounter.text = "\(word.correctCounter)"
wrongCounter.text = "\(word.wrongCounter)"
}
Fetched Results Delegate
extension ViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
updateView()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
case .insert:
guard let indexPath = newIndexPath else { return }
tableView.insertRows(at: [indexPath], with: .fade)
case .move:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
guard let newIndexPath = newIndexPath else { return }
tableView.insertRows(at: [newIndexPath], with: .fade)
default:
break
}
}
}
Problem:
The data you have in core data is not in a state that is ready to be shown in the app.
The NSFetchedResultsController just happens to pick this data and shows it in the app.
Approach:
When a user creates a records, the user wants to see the changes immediately in the app and shouldn't have to wait for the app to update in the server to see the changes. So a user would still be able to create records even when there is no network connectivity.
Create a child context from view context
Use a background thread to upload to the server and when update records on to the child context.
Save child context only when the data is in a consistent state.
NSFetchedResultsController should be using the view context so would be unaware of the child context records.
When you save the child context, the NSFetchedResultsController would pick these records.
Thanks for considering to help me! so I am getting this error in the console, How should I start going about fixing it?
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a non-nil fetchRequest and managedObjectContext'
*** First throw call stack:
(0x183ff51b8 0x182a2c55c 0x18630cb90 0x1000f4eb0 0x1000f46e8 0x1000f43c8 0x1000f411c 0x1000f40e4 0x101189218 0x10118a048 0x1000f41b4 0x1000fbb94 0x1000fbc94 0x189eaa924 0x189eaa4ec 0x18a2354e4 0x18a1fc6d0 0x18a1f8b44 0x18a13bfdc 0x18a12dd50 0x189e9d0b4 0x183fa20c0 0x183f9fcf0 0x183fa0180 0x183ece2b8 0x185982198 0x189f157fc 0x189f10534 0x1000f69d0 0x182eb15b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
These are the two swift files relevant to my error:
EntryController.swift
import Foundation
import CoreData
class EntryController {
static let shared = EntryController()
var fetchResultsController: NSFetchedResultsController<Entry>
init() {
let request: NSFetchRequest<Entry> = Entry.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: true)
request.sortDescriptors = [sortDescriptor]
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: CoreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
(try? fetchResultsController.performFetch())
}
//CRUD
func add(name: String, text: String) {
_ = Entry(name: name, text: text)
saveToPersistanceStorage()
}
func remove(entry: Entry) {
let moc = CoreDataStack.context
moc.delete(entry)
saveToPersistanceStorage()
}
func update(entry: Entry, name: String, text: String) {
entry.name = name
entry.text = text
saveToPersistanceStorage()
}
func saveToPersistanceStorage() {
let moc = CoreDataStack.context
(try? moc.save())
}
}
EntryTableListViewController.swift
import UIKit
import CoreData
class EntrylistTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
EntryController.shared.fetchResultsController.delegate = self
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let entries = EntryController.shared.fetchResultsController.fetchedObjects else {return 0}
return entries.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "entryCell", for: indexPath)
let entry = EntryController.shared.fetchResultsController.object(at: indexPath)
cell.textLabel?.text = entry.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let entry = EntryController.shared.fetchResultsController.object(at: indexPath)
EntryController.shared.remove(entry: entry)
}
}
//MARK: NSFetchedResultsControllerDelegate
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
guard let newIndexPath = newIndexPath else {return}
tableView.insertRows(at: [newIndexPath], with: .automatic)
case .delete:
guard let indexPath = indexPath else {return}
tableView.deleteRows(at: [indexPath], with: .automatic)
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else {return}
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.insertRows(at: [newIndexPath], with: .automatic)
case .update:
guard let indexPath = indexPath else {return}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toDetailSegue" {
if let detailVC = segue.destination as? EntryDetailViewController,
let selectedRow = tableView.indexPathForSelectedRow {
let entry = EntryController.shared.fetchResultsController.object(at: selectedRow)
detailVC.entry = entry
}
}
}
}
The app creates that error as soon as the button is pressed from the main menu to segue into the EntryTableListViewController.swift. Any help will be greatly appreciated!
Additionally, here's my CoreDataStack code
import Foundation
import CoreData
enum CoreDataStack{
static let container: NSPersistentContainer = {
let appName = Bundle.main.object(forInfoDictionaryKey: (kCFBundleNameKey as String)) as! String
let container = NSPersistentContainer(name: appName)
container.loadPersistentStores() { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
static var context: NSManagedObjectContext { return container.viewContext }
}
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)
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
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