'Array<(Object)>' is not convertible to 'Bool' - ios

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

Related

NSFetchedResultsController getting crash while displaying in Tableview

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.

NSFetchedResultsControllerDelegate not triggered when adding first item to DB.Triggered only when I added a second item

My application has two tab bars. The first one presents a list of games added on view controller and save them on the core data database. Switching on the second tab/view reads from the database and presents it inside a table view. I implemented the NSFetchedResultsControllerDelegate with a fetch method. When I add the first item to the context on the first tab and switch to second tab, FRC delegate methods (controllerWillChangeContent(_:), controller(_:didChange:at:for:newIndexPath:), controllerDidChangeContent(_:)) are not getting called and the table view is empty while I can see arrayOfGamesCount = 1. But when I add a second item, I can see all FRC delegate methods are getting call when I switch to second tab bar. And TableView display one rows while arrayOfGamesCount = 2
The first tab bar have 2 view controllers.(AddGameViewController and WelcomeViewController) AddGameVC is used to grab data from textfields and send it to welcomeVC.
import UIKit
import CoreData
class WelcomeViewController: UIViewController,SendGameDataDelegate, UIAdaptivePresentationControllerDelegate {
var games : [Game]? = []
var gamesMo: [GameMo]? = []
var gamed: GameMo?
var game : Game?
func ShouldSendGame(game: Game) {
self.game = game
print("\(game)")
games?.append(game)
}
#IBAction func endWLButton(_ sender: UIButton) {
saveDataToCoreData()
print("number of games from gamesMoCount is \(gamesMo?.count ?? 0)")
games?.removeAll()
reloadCollectionViewData()
}
func saveDataToCoreData (){
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
gamed = GameMo(context: appDelegate.persistentContainer.viewContext)
if games != nil {
for game in games! {
gamed?.goal = Int32(game.goal ?? 0 )
gamed?.rivalGoal = Int32(game.rivalGoal ?? 0)
gamed?.shot = Int32(game.shots ?? 0)
gamed?.rivalShot = Int32(game.rivalGoal ?? 0)
gamed?.rivalCorners = Int32(game.rivalsCorner ?? 0)
gamed?.corners = Int32(game.corners ?? 0)
gamesMo?.append(gamed!)
}
print("Saving data to context ....")
appDelegate.saveContext()
}
}
}
}
extension WelcomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return games?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let gameIndex = games?[indexPath.row] {
let userGameScore = gameIndex.goal
let rivalGameScore = gameIndex.rivalGoal
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FormCell", for: indexPath) as? FormCollectionViewCell {
cell.setCell(userScores: userGameScore!, rivalScores: rivalGameScore! )
return cell
}
}
return UICollectionViewCell ()
}
}
The second tab bar have only one VC: AllWLeagueController used to display items from the the database.
import UIKit
import CoreData
class AllWLeagueController : UITableViewController {
var fetchRequestController : NSFetchedResultsController<GameMo>!
var arrayOfGamesModel : [[GameMo]]? = []
var gameMo: GameMo?
var gamesMo: [GameMo] = []
override func viewDidLoad() {
validation(object: arrayOfGamesModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchRequest()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("arrayOfGamesModelcount est \(arrayOfGamesModel?.count ?? 0)")
return arrayOfGamesModel?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let weekL = arrayOfGamesModel?[indexPath.row] {
if let cell = tableView.dequeueReusableCell(withIdentifier: "WL") as? AllWLeaguesTableViewCell {
let winCounts = WLManager.winCountMethod(from: weekL)
let lossCounts = WLManager.lossCountMethod(from:weekL)
cell.setOulet(win: winCounts, loss: lossCounts, rankName: rankString)
cellLayer(with: cell)
return cell
}
}
}
extension AllWLeagueController: NSFetchedResultsControllerDelegate {
func fetchRequest () {
let fetchRequest = NSFetchRequest<GameMo>(entityName: "Game")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "win", ascending: true)]
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate){
let context = appDelegate.persistentContainer.viewContext
// fetch result controller
fetchRequestController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchRequestController.delegate = self
do{
try fetchRequestController.performFetch()
if let fetchedObjects = fetchRequestController.fetchedObjects {
gamesMo = fetchedObjects
print("Fetech Request Activated")
print(gamesMo)
}
}catch{
fatalError("Failed to fetch entities: \(error)")
}
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView beginupdates")
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
print("insert")
tableView.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
print("delete")
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath {
print("update")
tableView.reloadRows(at: [indexPath], with: .fade)
}
default:
tableView.reloadData()
}
if let fetchedObjects = controller.fetchedObjects {
gamesMo = fetchedObjects as! [GameMo]
print("we are about to append arrayOfGamesModel")
arrayOfGamesModel?.append(gamesMo)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView endupdates")
tableView.endUpdates()
}
}
You are making a fatal mistake. In saveDataToCoreData only one instance is created and then it's being overwritten with the game data in each iteration of the array. So your array gamesMo may contain multiple items but it's always the same instance and only one instance is saved into the context.
Replace saveDataToCoreData with
func saveDataToCoreData (){
let appDelegate = UIApplication.shared.delegate as! AppDelegate
guard let games = games else { return }
for game in games {
let newGame = GameMo(context: appDelegate.persistentContainer.viewContext)
newGame.goal = Int32(game.goal ?? 0 )
newGame.rivalGoal = Int32(game.rivalGoal ?? 0)
newGame.shot = Int32(game.shots ?? 0)
newGame.rivalShot = Int32(game.rivalGoal ?? 0)
newGame.rivalCorners = Int32(game.rivalsCorner ?? 0)
newGame.corners = Int32(game.corners ?? 0)
gamesMo.append(newGame)
}
print("Saving data to context ....")
appDelegate.saveContext()
}
Another bad practice is to create new fetch results controllers in viewWillAppear. It's highly recommended to create one controller as lazy instantiated property – as well as the managed object context – for example
lazy var context : NSManagedObjectContext = {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}()
lazy var fetchRequestController : NSFetchedResultsController<GameMo> = {
let fetchRequest = NSFetchRequest<GameMo>(entityName: "Game")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "win", ascending: true)]
// fetch result controller
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
if let fetchedObjects = frc.fetchedObjects {
self.gamesMo = fetchedObjects
print("Fetech Request Activated")
print(gamesMo)
}
} catch{
fatalError("Failed to fetch entities: \(error)")
}
return frc
}()
Force unwrapping AppDelegate is perfectly fine. Your app won't even launch if AppDelegate was missing.
I recommend also to use less ambiguous variable names. games, gamesMo, gamed and game look very similar and can cause confusion.

Swift synchronise network request

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.

deleting an object after editing a property in realm raises RLMException reason: 'Index 0 is out of bounds (must be less than 0)'

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)

NSFetchedResultsController does not see inserts (or updates)

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

Resources