Sorting data by multiple key values - ios

I have a basic list app with two entities Folder and Item with a to many relationship, the folder subclass has a function called itemsArray() which returns the Items in a sorted array by creationDate which are displayed UITableView.
Item has two properties called date of type NSDate and another of type bool called completed. I have created a function where I can mark the cell item object as completed which changes the cell text color to grey.
Is there a way I can also sort it by its completed value ? For e.g the completed value of true or false should separate all the completed items and then also sort it by the creationDate.
The end result should be where the completed items are at the bottom of the UITableView and in order of date as well as the non completed items are in order of date. As if completion is higher priority than the date but still orders by creationDate
Heres my current function with sort descriptor to return an array;
func itemsArray() -> [Item] {
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: true)
return checklist.sortedArrayUsingDescriptors([sortDescriptor]) as! [Item]
}
PS: I tried some work arounds such as when marking as completed updating the date so it is at the bottom. This separates the completed items with the non completed. But when unmarking the item it remains at the bottom also this isn't a good long term solution as I may want to change the sorting to alphabetical order and completion.

You can add another NSSortDescriptor to the function:
func itemsArray() -> [Item] {
let sortByCompleted = NSSortDescriptor(key: "completed", ascending: true)
let sortByCreationDate = NSSortDescriptor(key: "creationDate", ascending: true)
return checklist.sortedArrayUsingDescriptors([sortByCompleted, sortByCreationDate]) as! [Item]
}
Play with the ascending value to get true first or false first. Or the Swifty way of doing things:
func itemsArray() -> [Item] {
return checklist.sort {
if $0.completed == $1.completed {
return $0.creationDate < $1.creationDate
}
return $0.completed < $1.completed
}
}

Related

SwiftUI CoreData Filtered List Deletion Fails Unpredictably

I'm struggling with a SwiftUI app with SwiftUI life cycle where I have created a fairly standard Core Data list and have added a search field to filter the list. Two views - one with a predicate and one without. The unfiltered list generation works as expected including swipe to delete. The filtered list appropriately displays the filtered list, but on swipe to delete I get completely unpredictable results. Sometimes the item disappears, sometimes it pops back on the list, sometimes the wrong item is deleted. I cannot discern any pattern.
Here's the view:
struct MyFilteredListView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.horizontalSizeClass) var sizeClass
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \InvItem.name, ascending: true)],
animation: .default) private var invItems: FetchedResults<InvItem>
var fetchRequest: FetchRequest<InvItem>
init(filter: String) {
//there are actually a bunch of string fields included - just listed two here
fetchRequest = FetchRequest<InvItem>(entity: InvItem.entity(), sortDescriptors: [], predicate: NSPredicate(format: "category1 CONTAINS[c] %# || name CONTAINS[c] %# ", filter, filter))
}
var body: some View {
let sc = (sizeClass == .compact)
return List {
ForEach(fetchRequest.wrappedValue, id: \.self) { item in
NavigationLink(destination: InvItemDetailView(invItem: item)) {
InvItemRowView(invItem: item)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: sc ? 100 : 200)
.padding(.leading, 10)
}//link
}
.onDelete(perform: deleteInvItems)
}//list
}
private func deleteInvItems(offsets: IndexSet) {
withAnimation {
offsets.map { invItems[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this - raise an alert
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
Core Data is pretty standard:
class InvItem: NSManagedObject, Identifiable {}
extension InvItem {
#NSManaged var id: UUID
#NSManaged var category1: String?
#NSManaged var name: String
//bunch more attributes
public var wrappedCategory1: String {
category1 ?? "No category1"
}
//other wrapped items
}//extension inv item
extension InvItem {
static func getAllInvItems() -> NSFetchRequest<InvItem> {
let request: NSFetchRequest<InvItem> = InvItem.fetchRequest() as! NSFetchRequest<InvItem>
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
}//extension
I'm guessing there is something about the offsets behavior in deleteInvItems(offsets: IndexSet) that I don't understand, but
I have not been able to find anything on this. This same code works as expected in the unfiltered list.
Any guidance would be appreciated. Xcode Version 12.2 beta (12B5018i) iOS 14
First Edit:
I figured out the pattern. Apparently the IndexSet refers to the entire entity, not the filtered items. Say for example that the unfiltered list has 10 items and the filtered list has 3 items. When I delete the third item in the filtered list the result is the deletion of the third item in the unfiltered list, so unless those two are the same, the third item reappears on the filtered list and the third item on the unfiltered list is deleted from Core Data. If I then delete the third item on the filtered list again, the fourth item on the original list is deleted (since the third is already gone).
So the question becomes - how do I get a reference to the object I want
to delete - IndexSet does not work.
It is likely because your ForEach is using a wrappedValue which may or may not be updated.
https://developer.apple.com/documentation/swiftui/binding/wrappedvalue
I suggest you look at the code that is provided when you create a new project (you are using some of it) so you can adjust and have more accurate data.
ForEach(items) { item in
Also, when deleting you are mixing the offset from fetchRequest and items in invItems stick with only one.
If you want to be able to dynamically filter the list I have had better luck using a FetchedResultsController wrapped in an ObservedObject
https://www.youtube.com/watch?v=-U-4Zon6dbE

Updating and Editing an Entity in CoreData

I am developing an app in which the user can create a List which will contain a number of Items. My CoreData Model is as follows.
The code below gets the list.contains relationship and store it as an itemsOnList Set and then populate a UITableView with each Item in the Set
func getItemsOnList(){
let app = UIApplication.shared.delegate as! AppDelegate
let context = app.persistentContainer.viewContext
//fetchRequest to get the List
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "List")
let predicate = NSPredicate(format: "title == %#", listName)
fetchRequest.returnsObjectsAsFaults = false
fetchRequest.predicate = predicate
if let fetchResults = try? context.fetch(fetchRequest){
if fetchResults.count > 0 {
for listEntity in fetchResults {
let list = listEntity as! List
print(list.title as Any)
itemsOnListSet = list.contains!
}
}
}
}
I want the user to be able to change either the name or quantity of the item.
My first way of doing this was since i already have the NSSet of Items i would just change the Item i want and then save the old list.contains with the new one.
Will that be a wise way to do it or is there an easier and simpler way of doing this?
Thanks
Rename contains to items so it describes what it is. (edit: also rename belongsTo to list)
So to access the items linked to a list it will just be list.items, much clearer.
Now if you want to edit an item just grab it out of that set and edit away, the relationships don't change. You only need to touch list.items again if you wanted to add or remove an item.

Rearranging order of TableView cells using Core Data

I have a custom UITableView class that handles animations for table view, and have tested the following code with an Array that is displayed in a table view.
tableView.didMoveCellFromIndexPathToIndexPathBlock = {(fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) -> Void in
self.objectsArray.exchangeObjectAtIndex(toIndexPath.row, withObjectAtIndex: fromIndexPath.row)
}
This works fine with a basic array, but I actually want to rearrange a managed object of a type NSSet. So in my file I have declared the following which creates returns an array of type Item which is used by the table view.
Folder class function:
func itemArray() -> [Item] {
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
return iist.sortedArrayUsingDescriptors([sortDescriptor]) as! [Item]
}
Table View Controller declaration
var itemArray = [Item]()
itemArray = folder.itemArray() //class function to return a NSArray of the NSSET [Item]
I am trying to make it so when rearranging the cells it changes the order of the NSSet so it is saved when the app reloads, does anyone have any suggestions ?
By definition, NSSets do not have an order so there is no native way for you to preserve the order of the NSSet. I am not saying it is not possible but you cannot do it the way you are thinking.
From Apple's NSSet Documentation:
The NSSet, NSMutableSet, and NSCountedSet classes declare the programmatic interface to an unordered collection of objects.
You will need to convert your NSSet to NSMutableArray and reference that array instead of the NSSet.

Sorting NSFetchedResultsController by Swift Computed Property on NSManagedObjectSubclass

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.

Swift how to have a TableView of hashs

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

Resources