Swift sorting Relationship in CoreData - swift

I would love to get help on this one since I've been on it for a while.
So I fetch my Sections like this and they all get in the right order, but the relationship "Todoitem" they have do not come in the same order. They get all in the correct Section but in wrong order.
I wanna get the Todoitems like this:
Section[
Todoitem "first created",
Todoitem "second created",
Todoitem "third created"...]
Instead they come like this when I create them, always random:
Section[
Todoitem "second created",
Todoitem "third created",
Todoitem "first created"...]
You guys in here are awesome and if someone would like to take the time to help me I would be more than tankful!
I'm working in Swift Xcode, Storyboard.
func fetchSections() {
let startOfDay = Calendar.current.startOfDay(for: currentDate)
var components = DateComponents()
components.day = 1
components.second = -1
let endOfDay = Calendar.current.date(byAdding: components, to: startOfDay)
let fetchRequest : NSFetchRequest<Section> = Section.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "date >= %# AND date <= %#", startOfDay as NSDate, endOfDay! as NSDate)
do {
self.tableViewData = try context.fetch(fetchRequest)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
//error
}
}
Fixed it with this:
extension Section {
var sortedItems: [TodoItem] {
guard let items = todoitem as? Set<TodoItem> else {
return []
}
return items.sorted { (item1, item2) in
return item1.date ?? Date() < item2.date ?? Date()
}
}
}
and in cell for row:
let text = self.tableViewCoreData[indexPath.section].sortedItems[indexPath.row - 1].todo
cell.myLabel.text = text

Predicate is for filtering data only. You probably need to use an NSSortDescriptor to get the output you desire.
After this line:
fetchRequest.predicate = NSPredicate(format: "date >= %# AND date <= %#",
startOfDay as NSDate, endOfDay! as NSDate)
Add these lines:
let dateSortDescriptor = NSSortDescriptor(key: "date", ascending: true)
fetchRequest.sortDescriptors = [dateSortDescriptor]
Update
As Joakim Danielson rightly mentioned in the comments, the sorting cannot be directly applied on sections as the date resides in the ToDoItems entity.
Since you are creating a fetch request using the sections entity, I am not sure you can apply the sort descriptor directly to your to do items entity (atleast to my knowledge).
I would do the following updates to get your desired goal:
First change your var tableViewData to be compatible with this solution
// I am assuming Section and ToDoItems are names of your entities
// and NOT the name of the relationship between them so create this
var tableViewData: [Section: ToDoItem] = [:]
Then make some updates in the fetching
// Since you said this works fine, I do not make any changes to these two lines
let fetchRequest : NSFetchRequest<Section> = Section.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "date >= %# AND date <= %#",
startOfDay as NSDate, endOfDay! as NSDate)
// Temp container to build the desired sorted results
var sectionItems: [Section: [ToDoItems]] = [:]
do {
// Retrieve the sections
let sections = try context.fetch(fetchRequest)
// Loop over all the sections you retrieve
for section in sections
{
// Initialize a sort descriptor for the date attribute in the ToDoItems
// entity. Change it to the right name if it is not date
let sortDescriptor = [NSSortDescriptor(key: "date", ascending: true)]
// Change hasToDoItems to your correct relationship name
if let sortedItems = section.hasToDoItems?
.sortedArray(using: sortDescriptor) as? [ToDoItem]
{
sectionItems[section] = sortedItems
}
}
// Assign the tableViewData with the sorted data
self.tableViewData = sectionItems
// Reload your data
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Give this a go and let me know if this gives you the desired results or comment with your results and I'll amend the code accordingly.

Fixed it with this:
extension Section {
var sortedItems: [TodoItem] {
guard let items = todoitem as? Set<TodoItem> else {
return []
}
return items.sorted { (item1, item2) in
return item1.date ?? Date() < item2.date ?? Date()
}
}
}
and in cell for row:
let text = self.tableViewCoreData[indexPath.section].sortedItems[indexPath.row - 1].todo
cell.myLabel.text = text
Got help from a friend of mine, thanks anyways!

Related

how to modify my Core Data attributes of an entity but on all the elements of my database

hello community I am a novice and this is my first question.
how to change all the attributes of an entity and be able to change all my Core Data elements,
because I can only change the first attribute of an entity but not all my data records.
Here in this function I can only change the name
and then I get this following error has the line:
let objectUpdate = test[0] : Thread 1: Fatal error: Index out of range
func updateData() {
var newName = ""
var newPrenom = ""
newName = name.text!
newPrenom = prenom.text!
let managedContext = AppDelegate.viewContext
let fetchRequest : NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "name = %#", newName)
do {
fetchRequest.predicate = NSPredicate(format: "prenom = %#", newPrenom)
let test = try! managedContext.fetch(fetchRequest) as! [NSManagedObject]
let objectUpdate = test[0]
objectUpdate.setValue(newName,forKey: "name")
objectUpdate.setValue(newPrenom, forKey: "prenom")
do {
try managedContext.save()
}
catch {
print(error)
}
} catch {
print(error)
}
}
There are a number of ways we can avoid this error.
Unwrapping optional .first value
Swift's Collection gives us safe way to get first item, simply by accessing the first property on a given collection. It will return an Optional<Element> value so we need to unwrap it first either by using if let of guard let
if let object = test.first {
// do something with object
}
or
guard let object = test.first else { return }
// do something with object
Checking if value at index exists
It's often a good idea to check for a specific index within the indices property before accessing the value behind it.
if test.indices.contains(0) {
let object = test[0]
// do something with object
}
These hints should prevent your code from crashing again.
Other Suggestions
This is not really safe or clean:
var newName = ""
var newPrenom = ""
newName = name.text!
newPrenom = prenom.text!
We can make it much cleaner and most importantly safer by using a guard statement
guard let newName = name.text, let newPrenom = prenom.text else { return }
Two important things happened here:
No more force-unwrapping the optional values of text [which could cause a crash]
The properties are now immutable, meaning we can be sure that what we are saving to the CoreDate is what was retreived at the beginning of the function
Since the line:
let test = try! managedContext.fetch(fetchRequest) as! [NSManagedObject]
is already wrapped in the do-catch clause, you can safely remove forced try! and replace it with try.
let test = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
Let's use types! On this line you create a NSFetchRequest object for some entity named "Person".
let fetchRequest : NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Person")
I am guessing CoreData have generated for you a NSManagedObject subclass, named Person. If this is true, you could rewrite it like this:
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
With the previous tip implemented, we can now get rid of as! [NSManagedObject] from this line:
let test = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
Since the NSFetchRequest object is now nicely typed, we can take advantage of it by rewriting it like this:
let test: [Person] = try managedContext.fetch(fetchRequest)
So we are using proper types now? cool! Lets now improve this:
objectUpdate.setValue(newName,forKey: "name")
objectUpdate.setValue(newPrenom, forKey: "prenom")
by rewriting this and using properties on Person object
objectUpdate.name = newName
objectUpdate.prenom = newPrenom
No need for introducing second level of do-catch clause, since we are already in one!
do {
try managedContext.save()
}
catch {
print(error)
}
you can easily replace it with just the save() call, like this:
try managedContext.save()
Are you sure these predicates are what you want?
fetchRequest.predicate = NSPredicate(format: "name = %#", newName)
fetchRequest.predicate = NSPredicate(format: "prenom = %#", newPrenom)
What I can read from them is that you are fetching Person object where the name is newName and prenom is newPrenom and then you update it with the same exact values? Are you using some kind of identification of users? like id: Int or id: UUID? It would make much more sense to write something like this
let id: Int = // ID of the user you are currently editing
fetchRequest.predicate = NSPredicate(format: "id == \(id)")
if you are not using any id's, you could try storing the initial values of name and prenom
// in cell declaration - set when you configure your cell
var initialName: String?
var initialPrenom: String?
// then in your function:
fetchRequest.predicate = NSPredicate(format: "name = %#", initialName)
fetchRequest.predicate = NSPredicate(format: "prenom = %#", initialPrenom)
But I just noticed you also override you first predicate with the second one. You need to use NSCompoundPredicate
fetchRequest.predicate = NSCompoundPredicate(
type: .and, subpredicates: [
NSPredicate(format: "name = %#", initialName),
NSPredicate(format: "prenom = %#", initialPrenom)
]
)
Suggested version
func updateData() {
guard let newName = name.text, let newPrenom = prenom.text else { return }
let managedContext = AppDelegate.viewContext
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
fetchRequest.predicate = NSCompoundPredicate(
type: .and, subpredicates: [
NSPredicate(format: "name = %#", initialName),
NSPredicate(format: "prenom = %#", initialPrenom)
]
)
do {
let objects: [Person] = try managedContext.fetch(fetchRequest)
guard let object = objects.first else { return }
object.name = newName
object.prenom = newPrenom
try managedContext.save()
} catch {
print(error)
}
}
If the index 0 is out of range, it means that the array is empty. Before accessing it, add
if test.isEmpty{
return //the fetch request didn't return any values
}

Why is this Label not showing the correct value? Swift

I wrote a code to take data from my CoreDate Entity to show the highest Integer as the value at a Highscore label. I don't understand why it is not working? I tried it with or without a extra function...
func loadHighscore() {
//Kontext identifizieren
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
//Anfrage stellen
let context = appDelegate.persistentContainer.viewContext
let entityName = "PushUps"
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
do {
let results = try context.fetch(request)
for result in results {
guard let count = (result as! NSManagedObject).value(forKey: "highScore") as? Int16 else {
return
}
}
if count > highScore {
highscoreLabel.text = "Highscore: \(count)"
highScoreChanged(newHighScore: Int16(count))
// Console statement:
print("New Highscore: \(count)")
}
} catch {
print("error")
}
}
func highScoreChanged(newHighScore: Int16) {
highscoreLabel.text = "Highscore: \(newHighScore)"
}
}
Your approach is a bit strange.
A better approach is to load the data sorted descending by highScore so the first item is the item with the highest value.
It's highly recommended to take advantage of the generic Core Data types and to use dot notation rather the KVC value(forKey
func loadHighscore() {
//Kontext identifizieren
// delegate can be forced unwrapped. The app doesn't even launch if AppDelegate doesn't exist
let appDelegate = UIApplication.shared.delegate as! AppDelegate
//Anfrage stellen
let context = appDelegate.persistentContainer.viewContext
let entityName = "PushUps"
// Use a specific fetch request
let request = NSFetchRequest<PushUps>(entityName: entityName)
// add a sort descriptor to sort the items by highScore descending
request.sortDescriptors = [NSSortDescriptor(key: "highScore", ascending: false)]
do {
// results is an array of PushUps instances, no type cast needed
let results = try context.fetch(request)
if let result = results.first, result.highScore > highScore {
highScore = result.highScore
print("New Highscore: \(highScore)")
}
} catch {
print(error)
}
highscoreLabel.text = "Highscore: \(highScore)"
}
The function highScoreChanged is not needed either. If the saved highscore is higher than the current value (property highScore) the property is updated and at the end of the method the text field is updated with the value of the property highScore.
Be sure to execute the label update in main queue. In other way it may not be done.

what is the best way to condition a tableview data and row count depending on a particular attribute from an array of NSManagedObject

I have an array of nsmanagedobject with a date attribute. I'm trying to make my table view load only the objects that have an attribute of date of today. what's the best way to do set the number of rows and set the cells based on a property from any nsmanagedobject array. I can work around it with multiple arrays but that will make my code such a message especially with my contextual actions. any help or tip is appreciated :) I have multiple tableviews inside one tableview controller and I have one array to populate them with.
EDIT:
Another way to do this, which based on your code, might most likely not be the optimal way to do this is to use a filter function such as follows:
First, add this function to a Date extension:
extension Date {
func isSameDate(_ comparisonDate: Date) -> Bool {
let order = Calendar.current.compare(self, to: comparisonDate, toGranularity: .day)
return order == .orderedSame
}
}
Then filter your existing array (keep in mind that unless you store that filtered array, you will be filtering multiple times - inside numberOfRows and cellForRow at):
let todaysItems = items.filter{ $0.isSameDate(Date()) }
You can do that by simply creating a function:
func loadTodaysItems() -> [Item] {
let request: NSFetchRequest<Item> = Item.fetchRequest()
let startDate = Date().startOfDay
let endDate = Date().endOfDay
request.predicate = NSPredicate(format: "date != nil AND date >= %# AND date <= %#", startDate as NSDate, endDate as NSDate)
/// (Optional) if you want to sort items by date
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
do {
return try context.fetch(request)
} catch {
print("Error fetching data from context: \(error)")
}
return []
}
Also, make sure you add this extension:
extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
var endOfDay: Date {
var components = DateComponents()
components.day = 1
components.second = -1
return Calendar.current.date(byAdding: components, to: startOfDay)!
}
}

Coredata NSpredicate predicate date in swift

A old classic question but I got.
func callThisDay(startDate:Date, endDate:Date) -> [Login]{
var datas = [Login]()
let fetchRequest:NSFetchRequest<Login> = Login.fetchRequest()
let predicate = NSPredicate(format: "date >= %# AND date < %#", argumentArray: [startDate, endDate])
fetchRequest.predicate = predicate
do{
let allData = try viewContext.fetch(Login.fetchRequest())
for data in allData{
datas.append(data as! Login)
}
}catch{
print(error)
}
return datas
}
And it always return all data. How can I just got a day?
Of course it always returns all data because you are ignoring the custom fetch request.
Replace
let allData = try viewContext.fetch(Login.fetchRequest())
with
let allData = try viewContext.fetch(fetchRequest)
The method can be simplified
func callThisDay(startDate:Date, endDate:Date) -> [Login] {
let fetchRequest : NSFetchRequest<Login> = Login.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "date >= %# AND date < %#", startDate as NSDate, endDate as NSDate)
do {
return try viewContext.fetch(fetchRequest)
} catch {
print(error)
return []
}
}

Core Data - Create Initial Data on First Open, Then Load it into NSTableView - OSX

i need to insert the days of the week Mon -> Sunday into core data, because you cant set data into core data from interface builder im doing this when the app first opens like this...
let firstLaunchCheck = NSUserDefaults(suiteName: "nonya-buisness ;)")
if firstLaunchCheck?.boolForKey("firstLaunch") == false {
print("First Launch")
let appDel = NSApplication.sharedApplication().delegate as! AppDelegate
let context = appDel.managedObjectContext
let addDay = NSEntityDescription.insertNewObjectForEntityForName("Weekdays", inManagedObjectContext: context)
let dayIndex = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var dayNumber = 0
for day in dayIndex {
print(day)
addDay.setValue(day, forKey: "dayName")
addDay.setValue(dayNumber, forKey: "dayNumber")
if dayNumber >= 5 {
addDay.setValue(true, forKey: "dayHoliday")
} else {
addDay.setValue(false, forKey: "dayHoliday")
}
do {
try context.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
dayNumber++
}
firstLaunchCheck?.setBool(true, forKey: "firstLaunch")
firstLaunchCheck?.synchronize()
} else {
print("App has already been opened")
}
Im storing the dayName and the dayNumber (eg, Wednesday is 2) and also adding wheather or not the day is a holiday or not. By default its set to Saturday and sunday (This can be changed by the user later)
Next in my view did load method im trying to load my data, this is where im getting stuck i think...
var TimetableData = [NSManagedObject]()
super.viewDidLoad() {
let appDel = NSApplication.sharedApplication().delegate as! AppDelegate
let context = appDel.managedObjectContext
let request = NSFetchRequest(entityName: "Weekdays")
request.sortDescriptors = [NSSortDescriptor(key: "dayNumber", ascending: false)]
do {
let results = try context.executeFetchRequest(request)
TimetableData = results as! [NSManagedObject]
print(TimetableData.count)
DayTableView.reloadData()
} catch {
print("There was a problem retrieving saved data")
}
}
The actually representing the data in a table view i dont think is the problem, but heres the code anyway.
if tableView == DayTableView {
let savedData = TimetableData[row]
let cell : NextDayCell = DayTableView.makeViewWithIdentifier("nextDayCell", owner: self) as! NextDayCell
if savedData.valueForKey("dayHoliday") as? Bool == true {
cell.cellDayString.stringValue = savedData.valueForKey("dayName") as! String + " Weekday"
} else {
cell.cellDayString.stringValue = savedData.valueForKey("dayName") as! String
}
cell.dayNumberS.stringValue = "\(day)"
day++
return cell
}
For some reason all this code is only loading 1 thing from core data, and that is Sunday. Help me ;P thanks people in advance
You do one insertNewObjectForEntityForName and change the properties of this object for each day. The solution is to do insertNewObjectForEntityForName for each day.
If the question here is really about CoreData, then I'm sure there will be another answer very soon.
If the real requirement is about accessing the days of the week, there is a simpler approach
let dateFormatter = NSDateFormatter()
// dateFormatter.weekdaySymbols = "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
for dayIndex in days
{
print(dateFormatter.weekdaySymbols[dayIndex])
}