I am initialising NSFetchedResultsController with following code
self.fetchedResultsController = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: "article.name",cacheName: nil
)
Here articles can have same name.So article.name sectionNameKeyPath is non unique.In this case fetchedResultsController will have only single section.
How can I handle multiple section with same section name?
For the sectionKeyPath return a UUID for the section (article.articleId or the like). Then when you display the title for the header in section don't display the named returned from the fetchedResultsController (self.fetchedResultsController.sections[section].name), rather get the first object in the section and figure out what you really want to display based on that (self.fetchedResultsController.sections[section].objects.firstObject.article.name).
Related
Some context:
I've been away from iOS programming for 5+ years and boy howdy have things changed. Just to make it more exciting, I'm trying Swift at the same time.
I'm trying to build a relatively simple iOS app using the Master-Detail App template and am at the point where I'm trying to add sections to the data. My (Core Data) data model is pretty simple at this point - some Locations (these are the sections) each of which contain some Containers. These are linked in the obvious way (a Location have a one-to-many reference to Containers, and Containers have a one-to-one reference to its Location).
I suspect that I'm heading to a place where I'm going to want the fetchedResultsController.object(at: indexPath) to return the Container corresponding with the indexPath but I'm also going to want the fetchedResultsController.sections[section] to return a Location.
The code
This is pretty much the code that comes from the app template (with minor modifications to use my Location type as the generic ResultType for NSFetchedResultsController - that might might be a mistake; maybe it should be a Container or even a NSManagedObject - we'll get to that in a minute).
var fetchedResultsController: NSFetchedResultsController<Location> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<Location> = Location.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
return _fetchedResultsController!
}
My questions
I'm new to Swift and I'm struggling to understand what the code that the Xcode template created for me. So I have a few questions:
What do we call this pattern? Anonymous classes (apparently not)? Closures? Knowing what it's called will help me do a better job of searching for related answers instead of wasting your time on these noob questions.
What is it actually defining? What I suspect it's doing is defining an implementation for init and returning an instance of NSFetchedResultsController that has all the default implementations. If that's not completely right, help me understand it a little better (pointing at something to read is also helpful).
How should I go about overriding methods of NSFetchedResultsController when using this pattern? Or is that something where I need to create a real subclass.
What type should I be using as the generic ResultType and why? This is a little off topic, but what the heck, maybe you'll take pity on me and bump me along another step of the journey.
It's basically called a computed property.
It checks if an instance of NSFetchedResultsController exists. If not it creates one and configures it.
There is nothing to override. However you should implement the delegate methods.
The generic type is correct. It avoids a lot of type casting.
I doubt this is the code which comes from the app template. The Swift equivalent of the objective-c-ish pattern with the instance variable and the nil-check is a lazy instantiated property. The body is called once the first time the property is accessed.
lazy var fetchedResultsController : NSFetchedResultsController<Location> = {
let fetchRequest: NSFetchRequest<Location> = Location.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
controller.delegate = self
do {
try controller.performFetch()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
return controller
}()
To add sections you have to specify the sectionNameKeyPath parameter and add an appropriate second sort descriptor.
The table view data source methods should look like
func numberOfSections(in tableView: UITableView) -> Int {
return self.fetchedResultsController.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.fetchedResultsController.sections?[section].numberOfObjects ?? 0
}
I have a table Student which has two attributes Name and DateOfBirth.
I want to display names of students in a UITableView in descending order of their DateOfBirth that is younger one on top and older on bottom of the list.
I have created a FetchRequest that fetches all the students and displays in descending order of their DateOfBirth.
let studentFetchRequest = Student.fetchRequest()
let dateSortDescriptor = NSSortDescriptor(key: #keyPath(Student.dateOfBirth), ascending: false)
studentFetchRequest.sortDescriptors = [dateSortDescriptor]
let frc = NSFetchedResultsController<Student>(fetchRequest: studentFetchRequest, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil)
frc.performFetch()
Now, I have a requirement that I need to display only last 15 students. Considering Student table has 50 records.
I tried changing the sortDescriptor's ascending property to true and added fetchLimit of 15 to my studentFetchRequest to fetch only last 15 students.
It fetches the last 15 students but it reverses the order in which they are being listed in UITableView. i.e. older students on top and younger on the bottom.
How can I modify the fetchRequest and still be able to use a FetchedResultsController to display the last 15 students in my UITableView?
I need to use FetchedResultsController because the actual table is far complex, I have simplified the question to explain the situation.
I'm building an expense tracker where an Expense can belong to only one Category but can have multiple Tags. This is my object graph:
In the screen where I list all the expenses in a table view, I want the expenses to be grouped by date (the sectionDate), and then by Category (or, using a segmented control, by Tag). This is the intended UI:
I can already make an NSFetchedResultsController query all expenses, section them by date, then by category, but I can't get the (1) total for the category and (2) the list of expenses in it. How might I proceed to do that? This is my current code:
let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> = {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Expense")
fetchRequest.resultType = .dictionaryResultType
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: #keyPath(Expense.sectionDate), ascending: false)
]
fetchRequest.propertiesToFetch = [
#keyPath(Expense.sectionDate),
#keyPath(Expense.category)
]
fetchRequest.propertiesToGroupBy = [
#keyPath(Expense.sectionDate),
#keyPath(Expense.category)
]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: Global.coreDataStack.viewContext,
sectionNameKeyPath: #keyPath(Expense.sectionDate),
cacheName: nil)
return fetchedResultsController
}()
A should forewarn that I've never done this, but personally I would set about it as follows:
Don't use propertiesToGroupBy: it forces you to use the .dictionaryResultType which means you can only access the underlying managed objects by executing a separate fetch.
Instead, add another computed property to the relevant NSManagedObject subclass, combining the sectionDate and the category.name. This property will be used as the sectionNameKeyPath for the FRC, so that the FRC will establish a section in the tableView for each unique combination of sectionDate and category name.
Add category.name as another sort descriptor for the fetch underlying the FRC. This will ensure that the Expense objects fetched by the FRC are in the correct order (ie. all Expense objects with the same sectionDate and category name are together).
Add a section header view for each section. The name property for the section (from the FRC) will include both the sectionDate and the category name. In most cases, you can strip out and ignore the sectionDate, displaying only the category name and corresponding total (see below). But for the very first section, and indeed the first section for any given sectionDate, add an additional view (to the section header view) showing the sectionDate and overall total for that sectionDate.
Working out whether a given section is the first section for the sectionDate is a little tricky. You could retrieve the name for the previous section and compare the sectionDates.
To collapse/expand the sections, maintain an array holding the collapsed/expanded state of each section. If the section is collapsed, return 0 in the tableView numberOfRowsInSection datasource method; if expanded use the figure provided by the FRC.
For the category totals, iterate through the objects array for the relevant section (or use a suitable .reduce to achieve the same).
For the sectionDate totals, filter the fetchedObjects for the FRC to include only the Expense objects for the relevant sectionDate, and then iterate or .reduce the filtered array.
I am happy to add or amend if any of that needs clarification.
I appreciate #pbasdf's answer, but I feel that I'll have a hard time wrapping my head around the solution after a long time of not looking at the code.
What I've come around to doing is instead of fetching Expense objects, I defined a new entity for the subsections themselves (CategoryGroup, and I will also make a TagGroup) and fetch those entities instead. These entities have references to the Expense objects that they contain, and the Category or the Tag that represents the group. This is my (partially complete) data model:
And my NSFetchedResultsController is now far simpler in code:
let fetchedResultsController: NSFetchedResultsController<CategoryGroup> = {
let fetchRequest = NSFetchRequest<CategoryGroup>(entityName: "CategoryGroup")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: #keyPath(CategoryGroup.sectionDate), ascending: false)
]
return NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: Global.coreDataStack.viewContext,
sectionNameKeyPath: #keyPath(CategoryGroup.sectionDate),
cacheName: "CacheName")
}()
The downside is that I now have to write extra code to make absolutely sure that the relationships among the entities are correctly defined whenever an Expense or a Category is created/updated/deleted, but that's an acceptable tradeoff for me as it is easier to comprehend in code.
I have CoreData model like this:
Parcel can have only one company, but company can have multiple parcels to deliver.
I have three companies in database preloaded. I have created table view with sections and loading data via NSFetchedResultsController.
I'm configuring it like this:
let fetchRequest = NSFetchRequest(entityName: EnityNames.PackageInfoEnityName)
// Add Sort Descriptors
let sortDescriptor = NSSortDescriptor(key: PackageInfoKeyPaths.Company, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Initialize Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: PackageInfoKeyPaths.Company, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
When I'm launching app on simulator I have three sections(as expected), i want to have sections as companies displayed, so I'm using relationship as section name key path:
number of sections 3
FedEx
UPS
DHL
I have created popover where I can add new entries to database to populate list with additional data. This is the code I'm using to create and save new data:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let package =
NSEntityDescription.insertNewObjectForEntityForName(EnityNames.PackageInfoEnityName, inManagedObjectContext: managedObjectContext) as! PackageInfo
let formatter = NSNumberFormatter()
formatter.numberStyle = .DecimalStyle
package.parcelNumber = formatter.numberFromString(parcelNumberTextField.text!) ?? 0;
package.createdAt = NSDate()
let company = avaialbleCompanies![companyPickerView.selectedRowInComponent(0)]
package.company_relation = company
company.addPackageToCompany(package)
appDelegate.saveContext()
the companies array is passed in prepare for segue to my little popover to let user choose only companies that are inside database. After this I'm getting something strange:
number of sections 4
FedEx
UPS
UPS
Why it is adding new Section? It should just add new item to existing section!
and terrifying error:
PackageChecker[5336:281349] 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 (4) must be equal to the number of sections contained in the table view before the update (3), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
Important part can be that I'm getting this:
updated index path 1x0
and I'm using fetchedResultsController.sections?.count to get number of sections.
If I restart the app on sim my newly added record is present in proper section inside list. Why it is not updating properly on runtime?
P.S. I have:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
Edit:
If I remove sectionNameKeyPath: PackageInfoKeyPaths.Company - adding works perfectly. Can You help me with this sections? Maybe I'm configuring them poorly.
The crash is occurring because the FRC is creating a new section, but your code does not currently create a corresponding tableView section. If you implement the NSFetchedResultsControllerDelegate method:
controller:didChangeSection:atIndex:forChangeType:
that should fix the crash.
But I'm afraid I can't see why the FRC is creating a new section.
I'm trying to translate some Objective-C code that was essentially lazy loading a variable multiple times. The code was similar to the following:
-(NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
//...code to build the fetchedResultsController with a new predicate
Whenever they wanted to rebuild the fetchedResultsController to use a new predicate, they would just set it to "nil" and call it, and it would rebuild it with a new predicate.
I'm struggling to do this same task in Swift. As far as I can tell, Swift lazy variables become normal variables after they are called for the first time. This is causing issues for me because if I try to set my swift variable back to nil, and recall it, it doesn't rebuild but remains nil.
The working code to load my fetchedResultsController as a lazy varaible is below. I've tried changing it to a computed property by adding a check if its nil and have it within a get block, but that hasn't worked. Any ideas?
lazy var taskController : NSFetchedResultsController? = {
var subtaskRequest = NSFetchRequest(entityName: "Subtasks")
var segIndex = self.segmentedControl.selectedSegmentIndex
subtaskRequest.predicate = NSPredicate(format: "task.category.name == %#", self.segmentedControl.titleForSegmentAtIndex(segIndex)!)
subtaskRequest.sortDescriptors = [NSSortDescriptor(key: "task.englishTitle", ascending: true), NSSortDescriptor(key: "sortOrder", ascending: true)]
let controller = NSFetchedResultsController(fetchRequest: subtaskRequest, managedObjectContext:
self.managedObjectContext!, sectionNameKeyPath: "task.englishTitle", cacheName: nil)
controller.delegate = self
return controller
}()
You can create something similar to the Objective-C method using a computed property backed by an optional variable.
var _fetchedResultsController: NSFetchedResultsController?
var fetchedResultsController: NSFetchedResultsController {
get {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
//create the fetched results controller...
return _fetchedResultsController!
}
}
lazy just implements a very specific memoization pattern. It's not as magical as you'd sometimes like it to be. You can implement your own pattern to match your ObjC code pretty easily.
Just make a second private optional property that holds the real value. Make a standard (non-lazy) computed property that checks the private property for nil and updates it if it's nil.
This is pretty much identical to the ObjC system. In ObjC you had two "things," one called _fetchedResultsController and the other called self.fetchedResultsController. In Swift you'll have two things, one called self.fetchedResultsController and the other called self._cachedFetchedResultsController (or whatever).