I'm building an app using Swift with Core Data. At one point in my app, I want to have a UITableView show all the objects of a type currently in the Persistent Store. Currently, I'm retrieving them and showing them in the table view using an NSFetchedResultsController. I want the table view to be sorted by a computed property of my NSManagedObject subclass, which looks like this:
class MHClub: NSManagedObject{
#NSManaged var name: String
#NSManaged var shots: NSSet
var averageDistance: Int{
get{
if shots.count > 0{
var total = 0
for shot in shots{
total += (shot as! MHShot).distance.integerValue
}
return total / shots.count
}
else{
return 0
}
}
}
In my table view controller, I am setting up my NSFetchedResultsController's fetchRequest as follows:
let request = NSFetchRequest(entityName: "MHClub")
request.sortDescriptors = [NSSortDescriptor(key: "averageDistance", ascending: true), NSSortDescriptor(key: "name", ascending: true)]
Setting it up like this causes my app to crash with the following message in the log:
'NSInvalidArgumentException', reason: 'keypath averageDistance not found in entity <NSSQLEntity MHClub id=1>'
When I take out the first sort descriptor, my app runs just fine, but my table view isn't sorted exactly how I want it to be. How can I sort my table view based on a computed property of an NSManagedObject subclass in Swift?
As was pointed out by Martin, you cannot sort on a computed property. Just update a new stored property of the club every time a shot is taken.
Related
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.
The issue being faced is my inability to access an entity's attributes when it is a relationship to the entity I am fetching.
In relation to my app, I am creating a fitness tracking app, I have a detail table view controller with my tracked activities. When I tap a cell the view segues into another View Controller to display a map of the tracked locations.
Using the fetched results controller I am fetching "Entity1". When I tap a cell 'I think' I want to segue & pass "Entity2" attribute values into another view controller. Except the relationship from "Entity1" to "Entity2" is a "To Many" relationship and in the core data properties for "Entity1",
extension Entity1
{
// instead of "Entity2" being represented as
// #NSManaged var entity2: Entity2?
//it is represented as
#NSManaged var entity2: NSOrderedSet?
}
thus i can not access "Entity2" properties.
How I fetch "Entity1":
func fetchEntity1ResultsController( _ context: NSManagedObjectContext )
{
let request: NSFetchRequest<Entity1> = NSFetchRequest( entityName: "Entity1" )
request.sortDescriptors = [ NSSortDescriptor( key: "timestamp", ascending: false ) ]
fetchedResultsController = NSFetchedResultsController( fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil )
fetchedResultsController?.delegate = self
do
{
try fetchedResultsController?.performFetch()
}
catch
{
print("Couldn't fetch results controller")
}
}
How I would attempt to access "Entity2" properties.
private func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
guard let entity1 = fetchedResultsController?.object(at: indexPath) else { return }
entity1.entity2.//No core data properties listed here
}
To conclude: My question is - How can I tap a cell that is the "Entity1" description where "Entity2" is in relationship to and access "Entity2" attribute values to display that data in my map view controller.
Since this is a to-many relationship, what you're calling entity2 is a set of multiple instances of Entity2. That's what the "many" part of "to-many" means here-- one Entity1 is related to a collection of multiple Entity2 instances. You can't access Entity2 attributes on a set, because it's a collection of more than one instance. To access Entity2 attributes you first need to select one object from the set.
How you do that depends on how your app is supposed to work. You have an NSOrderedSet, and it has a variety of options for selecting one of the objects it contains. You could ask for the first object, or the last one, or the Entity2 at a specific location in the ordered set. There are other options besides these; see the NSOrderedSet documentation for more info on them.
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 am new to iOS development using Swift language.
I am using CoreData in my app as a database option.
I am using NSFetchRequest for fetching records from the table. I can retrieve all records from the table, but I can't retrieve a specific row from the same table.
I have checked for the solutions online and other forums, but I am unable to get a proper solution for this.
I want to implement this in Swift only. I don't want to add the sqlite library or Bridging-wrapper (Objective - C) which will be my last option to implement this.
Any links or tutorials or suggestions will be helpful.
NSFetchRequest also support NSPredicate. With NSPredicate you can choose which exactly rows or row you need from Core Data.
More about NSPredicate - https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSPredicate_Class/
You can fetch a desired row by executing a fetch request, casting it as an array, and simply using subscript to retrieve an index. Or you can narrow down your results using a unique id and a predicate and getting the object at index 0.
if let results = managedObjectContext?.executeFetchRequest(fetchRequest, error: nil) as? [SomeClass] {
let someObject = results[someIndex]
}
As Pavel said, use NSPredicate. Here is a working example from my code, it might help you to get started ;) I added some comments below relevant lines
var req = NSFetchRequest(entityName: "YourDBTable")
// your db-table goes here
req.sortDescriptors = [NSSortDescriptor(key: "timeMillis", ascending: true)]
// if you want to sort the results
let start = NSNumber(longLong:int64StartValue)
// you need to convert your numbers to NSNumber
let end = NSNumber(longLong:int64EndValue)
let pred = NSPredicate(format:"timeMillis>=%# AND timeMillis <=%# AND foreignTableObject=%#",start,end, foreignTableObject)
// if you have relationships, you can even add another NSManagedObject
// from another table as filter (I am doing this with foreignTableObject here)
req.predicate = pred
var fetchedResults = managedContext?.executeFetchRequest(req,error: &error) as! [YourDBTable]?
// YourDBTable-class was created using the Xcode data model -> Editor -> Create NSManagedObject SubClass... - option
if let results = fetchedResults {
// iterate through your results
}
New to swift and mobile at all.
Created the default Master project in Xcode and trying to have a table (tableView) of categories where each category has an int next to it
House 1
Child 4
Food 5
That's it.
The default cells are just time stamp like so
let sortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false)
let sortDescriptors = [sortDescriptor]
I presume I will want a hash But i can't even set something other than a timestamp in that field
tried
newManagedObject.setValue(1, forKey: "timeStamp")
instead of
newManagedObject.setValue(NSDate.date(), forKey: "timeStamp")
get an error
Unacceptable type of value for attribute: property = "timeStamp"; desired type = NSDate; given type = __NSCFNumber; value = 1.'
just FYI, my master controller is like so
MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate