Inserting rows with multiple NSFetchedResultsController's - uitableview

I am downloading information from the internet and using the data to create entities in Core Data. I am trying to sort the entities (The entities are TV Shows, the data is from Trakt) by the airDate attribute of a TVEpisode entity that has a relationship to the TVShow entity. The TVShow entity only has this relationship to the show if the show data has an episode that is airing at a future date from the current time.
So the way I want to sort the data is:
Top: Shows that have a upcomingEpisode relationship, sorted by the airDate attribute of the upcomingEpisode, ordered ascendingly.
Middle: Shows that have no upcomingEpisode relationship but will be returning.
Bottom: Shows that have no upcomingEpisode relationship and that are ended/cancelled
Here are the issues I am running into getting this to work.
Issue 1: Using 1 NSFetchedResultsController
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [airDateSort, titleSort];
upcomingShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: "upcomingShows")
upcomingShowsResultsController.delegate = self;
var error: NSError? = nil
if (!upcomingShowsResultsController.performFetch(&error)) {
println("Error: \(error?.localizedDescription)")
}
Using this NSFetchedResultsController will put all TVShow entities with no upcomingEpisode relationship on top, sorted all by title, I need the dead shows sorted by title on the very bottom and returning shows sorted by title in the middle.
Issue 2: Using multiple NSFetchedResultsController's
func setupUpcomingShowsFetchedResultsController() {
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [airDateSort, titleSort];
let predicate = NSPredicate(format: "upcomingEpisode != nil")
fetchRequest.predicate = predicate
upcomingShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: "upcomingShows")
upcomingShowsResultsController.delegate = self;
var error: NSError? = nil
if (!upcomingShowsResultsController.performFetch(&error)) {
println("Error: \(error?.localizedDescription)")
}
}
func setupReturningShowsFetchedResultsController() {
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [titleSort];
let predicate = NSPredicate(format: "status == 'returning series'")
let predicate2 = NSPredicate(format: "upcomingEpisode == nil")
fetchRequest.predicate = NSCompoundPredicate.andPredicateWithSubpredicates([predicate!, predicate2!])
returningShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
returningShowsResultsController.delegate = self;
var error: NSError? = nil
if (!returningShowsResultsController.performFetch(&error)) {
println("Error: \(error?.localizedDescription)")
}
}
func setupDeadShowsFetchedResultsController() {
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [titleSort]
let endedShowsPredicate = NSPredicate(format: "status == 'ended'")
fetchRequest.predicate = endedShowsPredicate
deadShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
deadShowsResultsController.delegate = self;
var deadShowsError: NSError? = nil
if (!deadShowsResultsController.performFetch(&deadShowsError)) {
println("Error: \(deadShowsError?.localizedDescription)")
}
}
These work for what I want, but only when the data is already downloaded and in Core Data. When the app first launches and downloads the data it crashes every time because the number of rows in a section are not the same as what the table is expecting. I did manipulate the index paths that the NSFetchedResultsControllerDelegate gives in the didChangeObject function, and I printed out index's that are being inserted. The count that I did in any section was equal to how many the table view says it was expecting but it throws an error every time. This is how I am handling the method for multiple NSFetchedResultsController's
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
let section = sectionOfFetchedResultsController(controller)
let indexPathsComputed = [NSIndexPath(forRow: indexPath?.row ?? 0, inSection: section)]
let newIndexPathsComputed = [NSIndexPath(forRow: newIndexPath?.row ?? 0, inSection: section)]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
switch type {
case NSFetchedResultsChangeType.Insert:
self.tableView.insertRowsAtIndexPaths(newIndexPathsComputed, withRowAnimation: .Automatic)
case NSFetchedResultsChangeType.Delete:
self.tableView.deleteRowsAtIndexPaths(indexPathsComputed, withRowAnimation: .Automatic)
case NSFetchedResultsChangeType.Move:
self.tableView.deleteRowsAtIndexPaths(indexPathsComputed, withRowAnimation: .Automatic)
self.tableView.insertRowsAtIndexPaths(newIndexPathsComputed, withRowAnimation: .Automatic)
case NSFetchedResultsChangeType.Update:
if let index = indexPathsComputed[0] {
if let cell = self.tableView.cellForRowAtIndexPath(index) as? ShowTableViewCell {
self.configureCell(cell, indexPath: index)
}
}
else {
println("No cell at index path")
}
}
})
}
If the crashes could be fixed, this would be the best way to achieve what I want to do.
Issue 3: Using multiple Array's
func reloadShowsArray() {
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [airDateSort, titleSort];
let predicate = NSPredicate(format: "upcomingEpisode != nil")
fetchRequest.predicate = predicate
var error: NSError?
showsArray = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [TVShow]
if let error = error {
println(error)
}
}
func reloadDeadShows() {
let fetchRequest = NSFetchRequest(entityName: "TVShow")
let titleSort = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [titleSort]
let endedShowsPredicate = NSPredicate(format: "status == 'ended'")
fetchRequest.predicate = endedShowsPredicate
var error: NSError?
deadShows = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [TVShow]
if let error = error {
println(error)
}
}
This solves the crashing and works after the data is downloaded and while the data is being downloaded. But when using this, I have to call self.tableView.reloadData() when the data is downloaded, and the entities just pop into the table view with no animation, and I really want the animations from insertRowsAtIndexPaths because it looks better and is a better experience. I tried calling reloadShowsArray() and then using the find() function with the entity to get the index so I could use insertRowsAtIndexPaths, but it returns nil every time for the index, even though the entity was saved with the context before that. Also the cells will not get automatically reloaded or moved around like with NSFetchedResultsController
So what is the best way to handle this, and how can I get the desired sorting with the animations?

As per comments, I suspect the three-FRC method causes problems because one FRC calls controller:didChangeContent (which triggers tableView.endUpdates) while another FRC is still processing updates. To overcome this, implement a counter which is incremented in controller:willChangeContent and decremented in controller:didChangeContent. The tableView beginUpdates should only be called if the counter is zero, and endUpdates only when the counter returns to zero. That way, the endUpdates will only be called when all three FRCs have completed processing their updates.
If possible, I would also avoid the dispatch_async, since it could result in the table updates occurring outside the beginUpdates/endUpdates cycle.

Related

How do I use a search bar to sort my NSFetchedResults data without it crashing on fetchedRC.performFetch(), while updating tableview? UPDATED

My data loads fine before trying to search but then as soon as I start typing into the search bar it crashes when calling try fetchedRC.performFetch()
I have tried Googling and changing the code around but nothing seems to be working.
This is my setup for getting the data from the one-to-many relationship with the data in CoreData.
func setupFetchedResultsController() {
//fetchedRC = nil
let request = Item.fetchRequest() as NSFetchRequest<Item>
if !currentSearchText.isEmpty && !(currentSearchText == " ") {
request.predicate = NSPredicate(format: "list.name CONTAINS[c] \(currentSearchText)", parentObj)
} else {
request.predicate = NSPredicate(format: "list = %#", parentObj)
}
//list CONTAINS[c] \(currentSearchText)
let sort = NSSortDescriptor(key: #keyPath(Item.isComplete), ascending: true)
request.sortDescriptors = [sort]
do {
fetchedRC = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
try fetchedRC.performFetch()
self.tableView.reloadData()
} catch let error as NSError {
print(error.localizedDescription)
}
fetchedRC.delegate = self
}
This is my updateSearchResults function
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
currentSearchText = text
//fetchedRC = nil
setupFetchedResultsController()
}
I have it set up so there is one parent list and many child items. What I expect to happen is to have the children be show that contain what the user enters into the search bar. So as the user types more into the search bar it updates and refines the search more. What actually happens is that it crashes when calling the try fetchedRC.performFetch(). What would be a way to solve this problem? I am new to doing this with FetchedResultsController as well.
This is the error message it gives when it crashes too: CoreData: error: SQLCore dispatchRequest: exception handling request: <NSSQLFetchRequestContext: 0x28180cee0> , unimplemented SQL generation for predicate : (list CONTAINS[c] A) (LHS and RHS both keypaths) with userInfo of (null)
And as a note I also know that where it says list.name in request.predicate, is not proper I just did that as a place holder hoping it would be easier to understand.
I found the solution to my problem, hopefully this helps anyone else with this problem too.
func setupFetchedResultsController() {
let request = Item.fetchRequest() as NSFetchRequest<Item>
//This line simplified just to help it be more reliable
if (currentSearchText.count > 0) {
// Line Updated and Fixed Problem
request.predicate = NSPredicate(format: "list = %# && name CONTAINS %#", parentObj, currentSearchText)
} else {
request.predicate = NSPredicate(format: "list = %#", parentObj)
}
let sort = NSSortDescriptor(key: #keyPath(Item.isComplete), ascending: true)
request.sortDescriptors = [sort]
do {
fetchedRC = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
try fetchedRC.performFetch()
self.tableView.reloadData()
} catch let error as NSError {
print(error.localizedDescription)
}
fetchedRC.delegate = self
}

How to sort related objects in CoreData?

I have a many to many relationship in Core Data with posts and tags. Each tag has many posts and each post has many tags. I have a view controller where I want to display, for a particular tag, all of the posts associated with it.
I do this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
//3
fetchRequest.predicate = NSPredicate(format: "Tag.name == %#", tag.name!)
// let sort = NSSortDescriptor(key: "timeStamp", ascending: true)
// fetchRequest.sortDescriptors = [sort]
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil) as? NSFetchedResultsController<Tag>
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
tag = fetchedResultsController.object(at: IndexPath(item: 0, section: 0))
tag = Tag(context: managedContext)
let posts = tag.posts
My posts object at the bottom is a Set of objects - unordered. I want an array of posts ordered by timestamp that all belong to this particular tag and Im not sure how to create that.
I know usually to sort items by timestamp you would introduce an NSSortDescriptor like I did in step 3 (but is commented out). But I believe this would sort my fetch request (Tag) by the Tag's timestamp. Each tag doesn't have a timestamp and what I really want to sort is the posts associated with that tag. So I don't believe this is the way to do this.
How do I go about this? I think I could just use swift but it feels like there must be a Core Data way to sort related objects by timestamp
Edit: Updated approach with Paulw11's advice:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let tagFetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
let postFetchRequest: NSFetchRequest<Post> = Post.fetchRequest()
//3
tagFetchRequest.predicate = NSPredicate(format: "%K == %#", #keyPath(Tag.name), tag.name!)
do {
let results = try managedContext.fetch(tagFetchRequest)
tag = results.first
} catch let error as NSError {
print("Tag Fetch error: \(error) description: \(error.userInfo)")
}
guard let tag = tag else { return }
postFetchRequest.predicate = NSPredicate(format: "%# IN Post.tags", tag)
let sort = NSSortDescriptor(key: "timeStamp", ascending: true)
postFetchRequest.sortDescriptors = [sort]
fetchedResultsController = NSFetchedResultsController(fetchRequest: postFetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
posts = fetchedResultsController.fetchedObjects as! [Post]
firstPostDate = posts.first?.timeStamp as Date?
lastPostDate = posts.last?.timeStamp as Date?
for (i,post) in posts.enumerated() {
let date = post.timeStamp as Date?
let day = date?.days(from: Calendar.current.startOfDay(for: firstPostDate!))
indexMap[day!] = IndexPath(row: i, section: 0)
}
This gives this error message:
2018-11-29 15:36:56.261887-0800 SweatNetOffline[31250:17419187] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'unimplemented SQL generation for predicate : (<Tag: 0x60000216b2a0> (entity: Tag; id: 0xc36a752f5b05e34c <x-coredata://F02C5E33-89B9-4814-9D1B-8C74CAEC7DA1/Tag/p79> ; data: {
mostRecentThumbnail = nil;
mostRecentUpdate = "2018-12-07 21:47:44 +0000";
name = test;
posts = (
"0xc36a752f5af9e34e <x-coredata://F02C5E33-89B9-4814-9D1B-8C74CAEC7DA1/Post/p48>"
);
uuid = nil;
}) IN Post.tags)'
Looks like its inserting that entire object into the query string. This seems wrong but maybe I need to format the object differently?
The key was to use request.predicate = NSPredicate(format: "%# IN self.tags", tag)
as opposed to request.predicate = NSPredicate(format: "%# IN Post.tags", tag)
The context of the predicate is the Post so it no longer needs to be accessed I suppose, it is assumed. Self.tags, tags, and SELF.tags all work.

Predicate for NSNumber = 0

I have the following code fetching results from core data to populate a tableview.
private lazy var fetchedResultsController: NSFetchedResultsController = {
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "MileageLog")
// Add Sort Descriptors
let dateSort = NSSortDescriptor(key: "logDate", ascending: true)
let mileSort = NSSortDescriptor(key: "mileage", ascending: true)
fetchRequest.sortDescriptors = [dateSort, mileSort]
//// Create a new predicate that filters out any object that
//// have not been exported.
// let predicate = NSPredicate(format: "wasExported == %#", 0)
//// Set the predicate on the fetch request
// fetchRequest.predicate = predicate
let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedObjectContext = delegate.managedObjectContext
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
Without the predicate the tableview is populated with all records sorted correctly. Uncommenting the two lines to bring the predicate into force returns no results at all. My dataset has 4 records with wasExported == 1 and 3 with wasExported == 0... wasExported is a Boolean but in core data it is stored as NSNumber..... what have I done wrong?
You are using the wrong format specifier in your predicate. You want:
let predicate = NSPredicate(format: "wasExported == %d", 0)
%# is for object pointers. With %#, the 0 is interpreted as the nil pointer.

filter NSFetchedResultsController result swift 2

I want automatic updates on tableview..For that i have used NSFetchedResultsControlleras
lazy var fetchedResultsController: NSFetchedResultsController = {
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Student")
// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: "grade", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
}()
But i have to filter the results after loading as of user selection as..
#IBAction func onSegmentValueChanged(sender: UISegmentedControl)
{
if sender.selectedSegmentIndex == 0{
//filter by grade
let fetchRequest = NSFetchRequest(entityName: "Student")
let sortDescriptor = NSSortDescriptor(key: "grade", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let filterfetchResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
let predicate = NSPredicate(format: "address = %#","Russia")
filterfetchResultsController.fetchRequest.predicate = predicate
self.tableView.reloadData()
}else{
let fetchRequest = NSFetchRequest(entityName: "Student")
let sortDescriptor = NSSortDescriptor(key: "grade", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let filterfetchResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
let predicate = NSPredicate(format: "grade = %d",10)
filterfetchResultsController.fetchRequest.predicate = predicate
self.tableView.reloadData()
}
}
This however reload the tableView but data is not filtered.how do i solve this problem?This is the demo project i am working on.Or should i use the normal way passing the results and reloading tableView as of normal way to solve this problem?
You must use self.fetchedResultsController, this is your datasource, not filterfetchResultsController: thereĀ“s no connection.
You must do a performFetch() to execute the changed predicates
This works for me:
#IBAction func onSegmentValueChanged(sender: UISegmentedControl)
{
if sender.selectedSegmentIndex == 1{
//filter by address
let predicate = NSPredicate(format: "address == %#","Russia")
self.fetchedResultsController.fetchRequest.predicate = predicate
}else{
let predicate = NSPredicate(format: "grade = %d",10)
self.fetchedResultsController.fetchRequest.predicate = predicate
}
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
self.tableView.reloadData()
}

Reorder rows and save update to core data

I never imagined it'd be so hard to reorder a table and save the new order to Core Data (and perhaps I'm overthinking it). The bit of code below is throwing me the following error: "The number of rows contained in an existing section after the update must be equal to the number of rows contained in that section before the update."
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
initializeFetchedResultsController()
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context2: NSManagedObjectContext = appDel.managedObjectContext
let request2 = NSFetchRequest(entityName: "Activities")
let activityOrderSort = NSSortDescriptor(key: "activityOrder", ascending: true)
request2.sortDescriptors = [activityOrderSort]
let predicate = NSPredicate(format: "date == %#", date)
request2.predicate = predicate
var fetchResults2: [NSManagedObject]
do {
try fetchResults2 = (appDel.managedObjectContext.executeFetchRequest(request2) as! [NSManagedObject])
if fromIndexPath.row > toIndexPath.row {
for i in toIndexPath.row..<fromIndexPath.row {
fetchResults2[i].setValue(i+1, forKey: "activityOrder")
}
fetchResults2[fromIndexPath.row].setValue(toIndexPath.row, forKey: "activityOrder")
}
if fromIndexPath.row < toIndexPath.row {
for i in fromIndexPath.row + 1...toIndexPath.row {
fetchResults2[i].setValue(i-1, forKey: "activityOrder")
}
fetchResults2[fromIndexPath.row].setValue(toIndexPath.row, forKey: "activityOrder")
}
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
do {
try appDel.managedObjectContext.save()
} catch let error as NSError {
print("Saving error: \(error.localizedDescription)")
}
initializeFetchedResultsController()
}
Here is the initializeFetchedResultsController() code for reference:
func initializeFetchedResultsController() {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
context = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Activities")
let orderSort = NSSortDescriptor(key: "activityOrder", ascending: true)
fetchRequest.sortDescriptors = [orderSort]
let predicate = NSPredicate(format: "date == %#", date)
fetchRequest.predicate = predicate
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.context,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
I tried to pull as much as I could from the answer from a previous post here: Save new order to core data after the using the tableView:moveRowAtIndexPath:toIndexPath: method. Also, I have the canEditRowAtIndexPath function set up and it appears to be functioning properly. Is there anything obvious that I'm messing up in the provided code? Is there an easier solution? Thanks for the support.

Resources