FetchedResultsController and data for custom header section - ios

I am using the fetchedResultsController with the sectionNameKeyPath as below.
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: "relName.APropertyName", cacheName: nil)
the section name key is the relationship to the parent table and its one of the property name in the parent table.
I have a custom section header by overriding the below
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
in this header i would like to access the parent entity and its few other properties ( not just the property mentioned in the sectionNameKeyPath)
I have not enforced any uniqueness on the parent entity with the property "APropertyName" .
I would like to query the parent entity when i write the custom header for the section. How do I achieve this?
Thanks

I used the one to many relationship with the parent and child and used the "objectID" as the sectionNameKeyPath while declaring the fetchedResultsController.
Below was the deceleration of fetchedResultsController.
let fetchRequest = NSFetchRequest(entityName: "Child")
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: "relParent.objectID", cacheName: nil)
Once the fetch is complete and ready to display the header information on the cell I used
fetchedResultsController.sections?[section].objects property to traverse to the parent. below is the code to render the custom header cell.
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerCell = tableView.dequeueReusableCellWithIdentifier("headerCell") as? ChildEntityHeaderCell
if let cell = headerCell {
if let sectionData = fetchedResultsController.sections?[section] {
if sectionData.objects != nil && sectionData.objects!.count > 0 {
if let child = sectionData.objects?[0] as? ChildEntity , parent = child.relChild // child entity has inverse relationship with the parent [ two way relationship]
{
if let name = parent.PropertyA {
cell.LabelField.text = name
}
}
}
}
}
return headerCell
}

Related

How to setup a collectionView (or TableView) with NSFetchedResultsController and multiple fetched entities

I have read here that the way to fetch multiple entities with 1 NSFetchedResultsController is to use a parent child inheritance model. I have such a model:
https://imgur.com/a/nckHzvr
As you can see, TextPost,VideoPost, and ImagePost all have Skill as a parent entity. I am trying to make a single collectionView for which all three children show up. I am a little confused as to how to set the delegate methods though...
Here is the code for the view controller
class Timeline2ViewController: UIViewController {
#IBOutlet var postsCollectionView: UICollectionView!
var skillName: String?
fileprivate lazy var skillFetchedResultsController: NSFetchedResultsController<Skill> = {
let appDelegate =
UIApplication.shared.delegate as? AppDelegate
let managedContext =
appDelegate?.persistentContainer.viewContext
let request: NSFetchRequest<Skill> = NSFetchRequest(entityName: "Skill")
request.predicate = NSPredicate(format: "name == %#", self.skillName!)
let timeSort = NSSortDescriptor(key: "timeStamp", ascending: true)
request.sortDescriptors = [timeSort]
let skillFetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedContext!, sectionNameKeyPath: nil, cacheName: nil)
return skillFetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
do {
try skillFetchedResultsController.performFetch()
} catch let error as NSError {
print("SkillFetchError")
}
}
}
extension Timeline2ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let sectionInfo = skillFetchedResultsController.sections?[section] else { return 0 }
return sectionInfo.numberOfObjects
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = postsCollectionView.dequeueReusableCell(withReuseIdentifier: "pcell", for: indexPath) as? PostViewCell else { return fatalError("unexpected Index Path")
}
let post = skillFetchedResultsController[indexPath.row] /* This is the line im not sure about
cell.background
return cell
}
}
Since only 1 entity is actually, returned, I am not sure how to access an element at a specific index path. For instance skillFetchedResultsController[indexPath.row] would I think only have 1 entity - the skill itself. I really want to be accessing its children. Do I have to somehow subclass skillFetchedResultsController and return only the children Im interested in?
Edit: with #pbasdf suggestions - I have this model:
Now when I create an entity like so:
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let textPost = NSEntityDescription.insertNewObject(forEntityName: "TextPost", into: managedContext) as! TextPost
textPost.text = "test text post"
try! managedContext.save()
and I setup my fetched results controller to look at "Post2" like so:
fileprivate lazy var skillFetchedResultsController: NSFetchedResultsController<Post2> = {
let appDelegate =
UIApplication.shared.delegate as? AppDelegate
let managedContext =
appDelegate?.persistentContainer.viewContext
let request: NSFetchRequest<Post2> = NSFetchRequest(entityName: "Post2")
// request.predicate = NSPredicate(format: "skill = %#", self.skill!)
let timeSort = NSSortDescriptor(key: "timeStamp", ascending: true)
request.sortDescriptors = [timeSort]
let skillFetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedContext!, sectionNameKeyPath: nil, cacheName: nil)
return skillFetchedResultsController
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
do {
try skillFetchedResultsController.performFetch()
} catch _ as NSError {
print("SkillFetchError")
}
}
I see no returned results in:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let sectionInfo = skillFetchedResultsController.sections?[section] else { return 0 }
return sectionInfo.numberOfObjects
}
Do I somehow need to link the two? Like create the Post object and the TextPost object at the same time? When I try and fetch TextPost objects directly it works.
I think the problem lies in your model. You should create a Post entity, and make it the parent entity of TextPost, VideoPost, and ImagePost. If your subentities have any attributes in common, move them from the subentities to the Post entity. Then establish a one-many relationship from Skill to Post.
Your FRC should fetch Post objects (which will by default include all the subentities), using a predicate if necessary to restrict it to those Post objects related to your desired Skill object, eg.
NSPredicate(format:"skill.name == %#",self.skillName!)

Multiple Fetch Requests?

I currently have a UITableView with 2 sections that uses a NSFetchedResultsController. I am trying to work out how I display different entities in the different sections. I have a FOLDER objects and then also TAG objects. I am wanting to display all of these in each section, i.e. Section 1 all FOLDER, Section 2 all TAGS.
The relationship goes:
FOLDER (one to many)-> MOVIE (many to many)-> TAGS
How do I achieve this? Am I needing 2 separate tableView's or to use a single tableView with 2 different fetch requests? Please help!
EDIT: Fetch and tableView cellForRowAt code.
private let appDelegate = UIApplication.shared.delegate as! AppDelegate
private let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
private var fetchedRC: NSFetchedResultsController<Folder>!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refresh()
}
private func refresh() {
do {
let request = Folder.fetchRequest() as NSFetchRequest<Folder>
request.predicate = NSPredicate(format: "name CONTAINS[cd] %#", query)
let sort = NSSortDescriptor(keyPath: \Folder.name, ascending: true)
request.sortDescriptors = [sort]
do {
fetchedRC = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchedRC.delegate = self
try fetchedRC.performFetch()
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "folderCell", for: indexPath) as! FolderTableViewCell
let folder = fetchedRC.object(at: indexPath)
cell.update(with: folder)
cell.layer.cornerRadius = 8
cell.layer.masksToBounds = true
return cell
}
Use 2 FRC for your 2 sections.
One gets fed by your folder fetchrequest and the other by the tags, all in one tableview. Your tableview delegate methods take care of what you want to access. This is quite easy to handle that way. It only gets more complicated if you have more than just 2 sections.
That way your tableview delegate knows by section == 0 or 1 which FRC to access.

NSFetchedResultsController not working with transient property for sectionNameKeyPath

Working fine in Swift 3 with Xcode8.3
I have a project ongoing which has core data for saving messages.
It sorts messages according to time and sections them according to day.
Here's how:
let request = NSFetchRequest(entityName: "Message")
let sortDiscriptor = NSSortDescriptor(key: "time", ascending: true)
request.sortDescriptors = [sortDiscriptor]
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: mainThreadMOC, sectionNameKeyPath: "sectionTitle", cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
Here is transient property:
var sectionTitle: String? {
//this is **transient** property
//to set it as transient, check mark the box with same name in data model
return time!.getTimeStrWithDayPrecision()
}
Using it as:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
let n = sectionInfo.numberOfObjects
return n
}
It always gives 0 sections and sectionTitle property never getting called.
This setup was/is working correctly with Swift3 in Xcode8.3.
Even this is working with Swift3.2 in Xcode9-beta.
But if I switch to Swift4 in Xcode9-beta, it's not working.
Add #objc to the transient property, so:
#objc var sectionTitle: String? {
//this is **transient** property
//to set it as transient, check mark the box with same name in data model
return time!.getTimeStrWithDayPrecision()
}
I just switched 'Swift 3 #objc inference' in the build settings to 'on' and all works fine again.

Displaying tableView items by CoreData attribute

I am trying to group section 0 my tableView by the "category" attribute of an item.
Example:
Drinks: (item.category = header)
Dr. Prepper
Coke
Pepsi
Kitchen: (item.category = header)
Pots
Pans...etc.
CrossOff(header)
items
I still want section1 to be the item.slcross (or the last section if each group has to be their own section...and it doesn't have to be grouped).
When I change the secondarySortDescriptor key from "slitem" to "slcategory" and use the sectionHeader code below, it returns "nil". I also tried using
let sectionHeader2 = "\(item.valueForKeyPath("slcategory"))" but still had the same effect with both "slitem" and "slcategory".
Do I have to use a sort descriptor for each category or is there a way to make it pull the category attribute for the item and group the like categories together?
FRC set up:
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc : NSFetchedResultsController = NSFetchedResultsController()
var selectedItem : List?
func itemFetchRequest() -> NSFetchRequest{
let fetchRequest = NSFetchRequest(entityName: "List")
let primarySortDescription = NSSortDescriptor(key: "slcross", ascending: true)
let secondarySortDescription = NSSortDescriptor(key: "slitem", ascending: true)
fetchRequest.sortDescriptors = [primarySortDescription, secondarySortDescription]
fetchRequest.predicate = NSPredicate(format:"slist == true")
return fetchRequest
}
func getFetchRequetController() ->NSFetchedResultsController{
frc = NSFetchedResultsController(fetchRequest: itemFetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "slcross", cacheName: nil)
return frc
}
TableViewHeaders:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String?{
let entityDescription = NSEntityDescription.entityForName("List", inManagedObjectContext: moc)
let item = List(entity: entityDescription!, insertIntoManagedObjectContext: moc)
let sectionHeader = "\(item.slcategory)"
let sectionHeader1 = "Items in Cart - #\(frc.sections![section].numberOfObjects)"
if (frc.sections!.count > 0) {
let sectionInfo = frc.sections![section]
if (sectionInfo.name == "0") {
return sectionHeader2
} else {
return sectionHeader1
}
} else {
return nil
}
}
There are a few ways to do this, but probably the easiest is to add a new method to your NSManagedObject subclass. The method returns a string which will be used as the title for the section; so if slcross is false, it returns the value of slcategory, and if slcross is true it returns "True":
func sectionIdentifier() -> String {
if (self.slcross) {
return "True"
} else {
return "\(self.slcategory)"
}
}
(Note this code goes in your List class definition, not your view controller).
In the view controller, use this sectionIdentifier as the sectionNameKeyPath for your FRC:
frc = NSFetchedResultsController(fetchRequest: itemFetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
For that to work, it is imperative that the objects are sorted correctly: first by slcross, then by slcategory:
let primarySortDescription = NSSortDescriptor(key: "slcross", ascending: true)
let secondarySortDescription = NSSortDescriptor(key: "slcategory", ascending: true)
Finally, amend your titleForHeaderInSection to use the section name (which the FRC gets from sectionIdentifier), but replacing the "True" with your computed string:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String?{
if (frc.sections!.count > 0) {
let sectionInfo = frc.sections![section]
if (sectionInfo.name == "True") {
return "Items in Cart - #\(sectionInfo.numberOfObjects)"
} else {
return sectionInfo.name
}
} else {
return nil
}
}
From Apple Docs...
When you initialize the fetch results controller, you provide four parameters: .....
....Optionally, a key path on result objects that returns the section name. The controller uses the key path to split the results into sections (passing nil indicates that the controller should generate a single section).....
After creating an instance, you invoke performFetch: to actually execute the fetch.
If you want to sort the sections by category then you need to make the sectionNameKeyPath argument in the NSFetchedResultsController init to be your "category" property instead of your "slcross" property.
When I've used this in the past I've also included that same property that I've set as the sectionNameKeyPath in my sort descriptors, but not sure if that is actually needed or not.
Hope I have answered your question?

Re-order cells in UITableView with swift and fetchedResultsController

I have a UITableView in swift where the app allows users to re-order the cells, but it keeps crashing during the re-order.
I have this data model:
class Person: NSManagedObject {
#NSManaged var name: String
#NSManaged var mood: String
}
I have setup a bool variable to check when the user hits the edit button to see if the tableview.setEdititing is set to true, because this indicates a change in order is about to happen:
var userDrivenDataChange : Bool = false
Then for each of the fetched results controller delegate methods before I go ahead I always check if there are user driven changees to the object - for example...
func controllerWillChangeContent(controller: NSFetchedResultsController) {
if userDrivenDataChange{
return
}
tblView.beginUpdates()
}
Now, this is the bit I'm really struggling with...
In the moveRowAtIndexPath function, my logic is to get the object the user has just moved, and then get the section that the user wants to move the line to. Then create a new Person NSManagedObject but set its "mood" field to the new section name, then delete the object at the old indexPath.
func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
userDrivenDataChange = true
var context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: context)
var secInfo = fetchedResultController.sections![destinationIndexPath.section] as NSFetchedResultsSectionInfo
var personToAmend = fetchedResultController.objectAtIndexPath(sourceIndexPath) as Person
var copyOfPerson = Person(entity: entity!, insertIntoManagedObjectContext: context)
copyOfPerson.name = personToAmend.name
copyOfPerson.mood = secInfo.name!
context.deleteObject(personToAmend)
var err : NSError?
if !context.save(&err){
println(err)
}
userDrivenDataChange = false
}
So this doesn't work, honestly can't figure out why!
Any help would be greatly appreciated!
Thanks,
Jamie
p.s. Just incase you need to see it, my fetched results controller is:
lazy var fetchedResultController : NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Person")
let context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
let sort = NSSortDescriptor(key: "mood", ascending: true)
fetchRequest.sortDescriptors = [sort]
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: "mood", cacheName: nil)
frc.delegate = self
return frc
}()
----EDIT:
When a user reorders rows from one section to another, the fetchedResultsController always adds the row to the beginning of the section instead of the actual index path represented by destinationIndexPath.
So I guess my question becomes: Is there a function to either edit the index Path of the object I insert, or to add an object at a specific index path?

Resources