Grouping CoreData by Date() in SwiftUI List as sections - ios

My goal:
I want to be able to group CoreData Todo items by their dueDate ranges. ("Today", "Tomorrow", "Next 7 Days", Future")
What I attempted...
I tried using #SectionedFetchRequest but the sectionIdentifier is expecting a String. If it's stored in coreData as a Date() how do I convert it for use? I received many errors and suggestions that didn't help. This also doesn't solve for the date ranges like "Next 7 Days". Additionally I don't seem to even be accessing the entity's dueDate as it points to my ViewModel form instead.
#Environment(\.managedObjectContext) private var viewContext
//Old way of fetching Todos without the section fetch
//#FetchRequest(sortDescriptors: []) var todos: FetchedResults<Todo>
#SectionedFetchRequest<String, Todo>(
entity: Todo.entity(), sectionIdentifier: \Todo.dueDate,
SortDescriptors: [SortDescriptor(\.Todo.dueDate, order: .forward)]
) var todos: SectionedFetchResults<String, Todo>
Cannot convert value of type 'KeyPath<Todo, Date?>' to expected argument type 'KeyPath<Todo, String>'
Value of type 'NSObject' has no member 'Todo'
Ask
Is there another solution that would work better in my case than #SectionedFetchRequest? if not, I'd like to be shown how to group the data appropriately.

You can make your own sectionIdentifier in your entity extension that works with #SectionedFetchRequest
The return variable just has to return something your range has in common for it to work.
extension Todo{
///Return the string representation of the relative date for the supported range (year, month, and day)
///The ranges include today, tomorrow, overdue, within 7 days, and future
#objc
var dueDateRelative: String{
var result = ""
if self.dueDate != nil{
//Order matters here so you can avoid overlapping
if Calendar.current.isDateInToday(self.dueDate!){
result = "today"//You can localize here if you support it
}else if Calendar.current.isDateInTomorrow(self.dueDate!){
result = "tomorrow"//You can localize here if you support it
}else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 0{
result = "overdue"//You can localize here if you support it
}else if Calendar.current.dateComponents([.day], from: Date(), to: self.dueDate!).day ?? 8 <= 7{
result = "within 7 days"//You can localize here if you support it
}else{
result = "future"//You can localize here if you support it
}
}else{
result = "unknown"//You can localize here if you support it
}
return result
}
}
Then use it with your #SectionedFetchRequest like this
#SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.dueDateRelative, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<String, Todo>
Look at this question too
You can use Date too but you have to pick a date to be the section header. In this scenario you can use the upperBound date of your range, just the date not the time because the time could create other sections if they don't match.
extension Todo{
///Return the upperboud date of the available range (year, month, and day)
///The ranges include today, tomorrow, overdue, within 7 days, and future
#objc
var upperBoundDueDate: Date{
//The return value has to be identical for the sections to match
//So instead of returning the available date you return a date with only year, month and day
//We will comprare the result to today's components
let todayComp = Calendar.current.dateComponents([.year,.month,.day], from: Date())
var today = Calendar.current.date(from: todayComp) ?? Date()
if self.dueDate != nil{
//Use the methods available in calendar to identify the ranges
//Today
if Calendar.current.isDateInToday(self.dueDate!){
//The result variable is already setup to today
//result = result
}else if Calendar.current.isDateInTomorrow(self.dueDate!){
//Add one day to today
today = Calendar.current.date(byAdding: .day, value: 1, to: today)!
}else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 0{
//Reduce one day to today to return yesterday
today = Calendar.current.date(byAdding: .day, value: -1, to: today)!
}else if Calendar.current.dateComponents([.day], from: today, to: self.dueDate!).day ?? 8 <= 7{
//Return the date in 7 days
today = Calendar.current.date(byAdding: .day, value: 7, to: today)!
}else{
today = Date.distantFuture
}
}else{
//This is something that needs to be handled. What do you want as the default if the date is nil
today = Date.distantPast
}
return today
}
}
And then the request will look like this...
#SectionedFetchRequest(entity: Todo.entity(), sectionIdentifier: \.upperBoundDueDate, sortDescriptors: [NSSortDescriptor(keyPath: \Todo.dueDate, ascending: true)], predicate: nil, animation: Animation.linear)
var sections: SectionedFetchResults<Date, Todo>
Based on the info you have provided you can test this code by pasting the extensions I have provided into a .swift file in your project and replacing your fetch request with the one you want to use

It is throwing the error because that is what you told it to do. #SectionedFetchRequest sends a tuple of the type of the section identifier and the entity to the SectionedFetchResults, so the SectionedFetchResults tuple you designate has to match. In your case, you wrote:
SectionedFetchResults<String, Todo>
but what you want to do is pass a date, so it should be:
SectionedFetchResults<Date, Todo>
lorem ipsum beat me to the second, and more important part of using a computed variable in the extension to supply the section identifier. Based on his answer, you should be back to:
SectionedFetchResults<String, Todo>
Please accept lorem ipsum's answer, but realize you need to handle this as well.
On to the sectioning by "Today", "Tomorrow", "Next 7 Days", etc.
My recommendation is to use a RelativeDateTimeFormatter and let Apple do most or all of the work. To create a computed variable to section with, you need to create an extension on Todo like this:
extension Todo {
#objc
public var sections: String {
// I used the base Xcode core data app which has timestamp as an optional.
// You can remove the unwrapping if your dates are not optional.
if let timestamp = timestamp {
// This sets up the RelativeDateTimeFormatter
let rdf = RelativeDateTimeFormatter()
// This gives the verbose response that you are looking for.
rdf.unitsStyle = .spellOut
// This gives the relative time in names like today".
rdf.dateTimeStyle = .named
// If you are happy with Apple's choices. uncomment the line below
// and remove everything else.
// return rdf.localizedString(for: timestamp, relativeTo: Date())
// You could also intercept Apple's labels for you own
switch rdf.localizedString(for: timestamp, relativeTo: Date()) {
case "now":
return "today"
case "in two days", "in three days", "in four days", "in five days", "in six days", "in seven days":
return "this week"
default:
return rdf.localizedString(for: timestamp, relativeTo: Date())
}
}
// This is only necessary with an optional date.
return "undated"
}
}
You MUST label the variable as #objc, or else Core Data will cause a crash. I think Core Data will be the last place that Obj C lives, but we can pretty easily interface Swift code with it like this.
Back in your view, your #SectionedFetchRequest looks like this:
#SectionedFetchRequest(
sectionIdentifier: \.sections,
sortDescriptors: [NSSortDescriptor(keyPath: \Todo.timestamp, ascending: true)],
animation: .default)
private var todos: SectionedFetchResults<String, Todo>
Then your list looks like this:
List {
ForEach(todos) { section in
Section(header: Text(section.id.capitalized)) {
ForEach(section) { todo in
...
}
}
}
}

You can use this method for achive that,
like this:
func formattedDate () -> String? {
let RFC3339DateFormatter = DateFormatter()
RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX")
RFC3339DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
RFC3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
let date1 = RFC3339DateFormatter.date(from: date.formatted()) ?? Date()
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
// ES Spanish Locale (es_ES)
dateFormatter.locale = Locale.current//Locale(identifier: "es_ES")
return dateFormatter.string(from: date1) // Jan 2, 2001
}

Related

Swift: Sorting Core Data child entities on fetch based on Date

Introduction
I'm making a calendar app in which I store events using Core Data.
Its composed of: DateKey as the parent (with a one-to-many relationship) to CalendarEventModel. The concept is that DateKey holds a yyyy-dd-MM date string and all events that occur that day are added as a child to that DateKey as CalendarEventModel in a NSOrderedSet. (I'm using Class Definition, none of the entities are abstract.). CalendarEventModel being the entity containing information about one calendar event.
What I try to accomplish
Everything works as intended except that I can't sort my fetched results. When I fetch the DateKeys relevant for the current Calendar I simply can't get them to sort like this:
I want to sort the CalendarEventModel in each DateKey after the CalendarEventModels attribute startDate in ascending order ($0.startDate < $1.startDate). Then have the CalendarEventModels marked isAllDay = false before those with isAllDay = true.
Issue/Question
I've put some clarifications in comment in the code below.
I can't get NSSortDescriptor to work properly ( my attempt is commented in the code below
I've read about NSOrderedSet which the calendarEvents attribute of DateKey is, but I haven't found out how to use it for my sorting criteria.
How would you solve this sorting?
Let me know if more information is needed.
My code
fileprivate func loadEventsFromCoreData() {
dateFormatter.dateFormat = "yyyy dd MM"
let startDate = Date()
var predicatesForFetch : [NSPredicate] = []
var dateStringArray : [String] = []
var fetchResultsArray : [DateKey]?
for num in 0...9 {
let newDate = startDate.add(TimeChunk(seconds: 0, minutes: 0, hours: 0, days: num, weeks: 0, months: 0, years: 0))
let datePredicate = dateFormatter.string(from: newDate)
dateStringArray.append(datePredicate)
predicatesForFetch.append(NSPredicate(format: "dateInfo == %#", datePredicate))
}
setupSectionArray(eventArray: dateStringArray)
// I'm getting the correct DateKeys and their events.
let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicatesForFetch)
let eventFetch : NSFetchRequest<DateKey> = DateKey.fetchRequest()
eventFetch.predicate = compoundPredicate
// I've tried specifying this sortDescriptor and added it to the eventFetch:
// let descriptor = NSSortDescriptor(key: "calendarEvents", ascending: true) { ($0 as! CalendarEventModel).startDate!.compare(($1 as! CalendarEventModel).startDate!) }
// let sortDescriptors = [descriptor]
// eventFetch.sortDescriptors = sortDescriptors
do {
fetchResultsArray = try coreDataContext.fetch(eventFetch)
} catch {
print("Core Data initial fetch failed in Calendar Controller: \(error)")
}
guard fetchResultsArray != nil else { return }
// Xcode doesn't recognize the .sort() function which does not have a return value... and I don't think its a good idea to use the return function at all since I will have to delete all children and re add the sorted for each fetch.
for eventArray in fetchResultsArray! {
eventArray.calendarEvents?.sorted(by: { ($0 as! CalendarEventModel).startDate!.compare(($1 as! CalendarEventModel).startDate!) == .orderedAscending })
eventArray.calendarEvents?.sorted { ($0 as! CalendarEventModel).isAllDay && !($1 as! CalendarEventModel).isAllDay }
}
events = fetchResultsArray!
}
Thanks for reading my question.
Some notes:
Basically you cannot sort dates in string format "yyyy dd MM", either use "yyyy MM dd" or – highly recommended – Date type.
You cannot sort a relationship in place. To-many relationships are Sets which are unordered. By the way declare the relationship as native Set<CalendarEventModel> rather than unspecified NSSet and as non-optional.
Don't use a compound predicate, use one predicate and don't fetch DateKey records, fetch CalendarEventModel records with predicate dateKey.dateInfo >= startDate && dateKey.dateInfo <= endDate and add there the two sort descriptors.
Fetching filtered and sorted records once is much more efficient than get the relationship items and sort them manually.
Edit:
To get midnight of today and + 9 days use
let calendar = Calendar.current
let now = Date()
let nowPlus9Days = calendar.date(byAdding: .day, value: 9, to: now)!
let startDate = calendar.startOfDay(for: now)
let endDate = calendar.startOfDay(for: nowPlus9Days)
This code fetches all sorted CalendarEventModel in the date range and groups the array to a [Date : [CalendarEventModel]] dictionary
let predicate = NSPredicate(format: "dateKey.dateInfo >= %# && dateKey.dateInfo <= %#", startDate as CVarArg, endDate as CVarArg)
let sortDescriptors = [NSSortDescriptor(key: "startDate", ascending: true), NSSortDescriptor(key: "isAllDay", ascending: true)]
let eventFetch : NSFetchRequest<CalendarEventModel> = CalendarEventModel.fetchRequest()
eventFetch.predicate = predicate
eventFetch.sortDescriptors = sortDescriptors
do {
let fetchResultsArray = try coreDataContext.fetch(eventFetch)
let groupedDictionary = Dictionary(grouping: fetchResultsArray, by: {$0.startDate})
} catch {
print("Core Data initial fetch failed in Calendar Controller: \(error)")
}
Don't execute good code after the catch block. Put all good code in the do scope after the fetch.

Intermittent crash when performing fetch on core data

I have a published app that is occasionally crashing when performing a core data fetch. I know where the crashes are occurring, however I can't figure out why they are happening.
The app uses a tab view controller, and the crash occurs in a tab that has a tableView as the initial view. Because I need this tableView to update whenever data is changed elsewhere in the app, I'm performing a fetch in the viewWillAppear method.
Here is the relevant code:
lazy var fetchedResultsController: NSFetchedResultsController<Day> = {
let request: NSFetchRequest<Day> = Day.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
request.predicate = NSPredicate(format: "calories > %#", "0")
let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
return frc
}()
override func viewWillAppear(_ animated: Bool) {
do {
try fetchedResultsController.performFetch()
} catch {
print("Could not fetch results")
}
tableView.reloadData()
}
And here is an image of the call stack.
I haven't been able to recreate the crash on my device or on the simulator, so I really don't know how to go about fixing this bug. I'd appreciate any advice on solving this.
Here's a screenshot of the calories attribute in the core data model.
Here's the class method for creating a Day entity.
class func dayWithInfo(date: Date, inManagedContext context: NSManagedObjectContext) -> Day {
let defaults = UserDefaults.standard
let formatter = DateFormatter()
formatter.dateStyle = .full
let dateString = formatter.string(from: date)
let request: NSFetchRequest<Day> = Day.fetchRequest()
request.predicate = NSPredicate(format: "dateString = %#", dateString)
if let day = (try? context.fetch(request))?.first {
if let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date()) {
// Update the macro goals if the date is the current or future date
if date > yesterday {
day.proteinGoal = defaults.double(forKey: Constants.UserDefaultKeys.proteinValueKey)
day.carbohydrateGoal = defaults.double(forKey: Constants.UserDefaultKeys.carbohydratesValueKey)
day.lipidGoal = defaults.double(forKey: Constants.UserDefaultKeys.lipidsValueKey)
day.fiberGoal = defaults.double(forKey: Constants.UserDefaultKeys.fiberValueKey)
day.calorieGoal = defaults.double(forKey: Constants.UserDefaultKeys.caloriesValueKey)
}
}
return day
} else {
let day = Day(context: context)
// Set the date as a string representation of a day
day.dateString = dateString
day.date = date
// Set the calorie and macronutrient goals from user defaults
day.proteinGoal = defaults.double(forKey: Constants.UserDefaultKeys.proteinValueKey)
day.carbohydrateGoal = defaults.double(forKey: Constants.UserDefaultKeys.carbohydratesValueKey)
day.lipidGoal = defaults.double(forKey: Constants.UserDefaultKeys.lipidsValueKey)
day.fiberGoal = defaults.double(forKey: Constants.UserDefaultKeys.fiberValueKey)
day.calorieGoal = defaults.double(forKey: Constants.UserDefaultKeys.caloriesValueKey)
// Creat meals and add their ralationship to the day
if let meals = defaults.array(forKey: Constants.UserDefaultKeys.meals) as? [String] {
for (index, name) in meals.enumerated() {
_ = Meal.mealWithInfo(name: name, day: day, slot: index, inManagedContext: context)
}
}
return day
}
}
Allow me to posit a theory about what I think is happening. I might be making incorrect assumptions about your project, so please correct anything I get wrong. You likely understand all the below, but I'm trying to be thorough, so please excuse anything obvious.
You have a calories attribute of your model class that is implemented as an optional attribute with no default at the model level. Something like the below.
You also do not implement func validateCalories(calories: AutoreleasingUnsafeMutablePointer<AnyObject?>, error: NSErrorPointer) -> Bool in your managed object subclass, so it's possible to save an instance with a nil (remember that Core Data is still Objective-C, so your Double Swift attribute is an NSNumber in Core Data, so nil is perfectly valid for an optional attribute).
You can specify that an attribute is optional — that is, it is not required to have a value. In general, however, you are discouraged from doing so — especially for numeric values (typically you can get better results using a mandatory attribute with a default value — in the model — of 0). The reason for this is that SQL has special comparison behavior for NULL that is unlike Objective-C's nil. NULL in a database is not the same as 0, and searches for 0 will not match columns with NULL.
At this point, it would be easy to confirm the hypothesis. Simply edit the SQLite database used for Core Data by your app in the simulator and set the calories attribute of a single record to null and see if you crash in the same way.
If so, do a Core Data migration on your next release and remove the Optional designation on the model attribute and provide a Default value of zero.

Core Data Query, most recent car per year?

In my model I have a Car entity.
Car
year
createDate
name
I want to run a fetch that returns all most recent cars per year where name is not nil (name can be nil).
So, if I have 3 cars with year 2000 and 5 cars with year 2010, I want to get a total of 2 cars out of the fetch request (the most recent for year 2000 and the most recent for year 2010).
I am not sure how to write the proper predicate to achieve this. I looked in the returnsDistinctResults option, but I am not sure that is the right path go go down and it also said that it only works with NSDictionaryResultType which does not work for me.
What would be the proper query/predicate to get this done?
You would be better off simply running over each record, with an algorithm.
Allowing for your Car class, define the following.
struct C {
var year: Int
var created: Date
var name: String?
}
var cars = [C(year: 2009, created: Date(), name: nil), C(year: 2009, created: Date(), name: "Green"), C(year: 2010, created: Date(), name: "Green"), C(year: 2010, created: Date(), name: "Orange"), C(year: 2010, created: Date(), name: "Orange")]
var carsPerYear = [Int: [String: Int]]()
for car in cars {
if let name = car.name {
var info: [String: Int]? = nil
if carsPerYear.keys.contains(car.year) {
info = carsPerYear[car.year]
} else {
info = [String: Int]()
}
if !info!.keys.contains(name) {
info![name] = 0
}
info![name] = info![name]! + 1
carsPerYear[car.year] = info
}
}
for year in carsPerYear.keys {
let info = carsPerYear[year]!
let sorted = info.keys.sorted{ info[$0]! > info[$1]! }
print(year, sorted.first!)
}
Gives output
2009 Green
2010 Orange
Sometimes SQL or CoreData cannot solve the problems easily, it's best to just write sort algorithm.
You can achieve this, if say you're passing an array of years into an algorithm, and expecting a result in the form of a dictionary, with each year you passed in as a key for an object that corresponds to the most recent car of that year.
ie:
let years : [Int] = [2000, 2005]
//The ideal method:
func mostRecentFor<T>(years: [Int]) -> [String:T]
Assuming you only use a small number of search years, and that your database has a small number of cars, you shouldn't have to worry about threading. Otherwise, for this approach you'd have to use concurrency, or a dedicated thread for the fetching to avoid blocking the main thread. For testing purposes you shouldn't need to.
To accomplish this, you would use a method that goes along the following line.
First, a number predicate should be used on the year like other answers have suggested.
For this you would use the date format for NSPredicate:
NSPredicate(format: "date == %#", year)
Your model should, therefore, have the year as a parameter and the actual nsdate as another. You would then use a fecthLimit parameter set to 1, and a sortDescriptor applied to the nsDate key parameter. The combination of the later two will ensure that only the youngest or oldest value is returned for the given year.
func mostRecentFor(years: [Int]) -> [String:Car] {
let result : [String:Car] = [:]
for year in years {
let request: NSFetchRequest<CarModel> = CarModel.fetchRequest()
request.predicate = NSPredicate(format: "date == %# AND name != nil", year)
request.fetchLimit = 1
let NSDateDescriptor: let sectionSortDescriptor = NSSortDescriptor(key: "nsDate", ascending: true)
let descriptors = [NSDateDescriptor]
fetchRequest.sortDescriptors = descriptors
do {
let fetched = try self.managedObjectContext.fetch(request) as
[CarModel]
guard !fetched.isEmpty, fetched.count == 1 else {
print("No Car For year: \(year)")
return
}
result["\(year)"] = new[0].resultingCarStruct //custom parameter here.. see bellow
} catch let err {
print("Error for Year: \(year), \(err.localizedDescription)")
}
}
return result
}
We use ascending set to true here, because we want the first value, the one actually returned due to the fetchLimit value of 1, to be the earliest car of the said year, or the one with the smallest timestamp for it's nsDate key.
This Should give you a simple fetch query for small datastores and testing. Also, creating a method or parameter to interpret CarModel into it's Car struct counterpart could make it simpler to go from CD to your memory objects.
So you'd search for your cars like so:
let years = [1985, 1999, 2005]
let cars = mostRecentFor(years: years)
// cars = ["1985": Car<year: 1985, name: "Chevrolet", nsDate: "April 5th 1985">, "1999" : Car<year: 1999, name: "Toyota Corolla", nsDate: "February 5th 1999">]
// This examples suggests that no cars were found for 2005

Using CoreData how to configure a fetch request to find upcoming birthdays?

I’m using Xcode 7.3 and Swift 2.2 with an NSFetchedResultsController. Is it possible to create the required fetch request configured with a predicate and sort descriptors to solve this problem?
Given a Person entity that has a birthDate attribute how do I configure a fetch request to return upcoming birthdays? Here’s what I have tried:
I created a transient attribute called birthDateThisYear and configured it to return the person’s birthday this year. But I discovered that you can’t use a transient attribute in a fetch request with Core Data.
I tried the accepted answer here by denormalizing with birthDateDayNumber and birthDateMonthNumber attributes along with a custom setter for birthDate but I couldn’t figure out the predicate. What if today was Dec 31? Somehow it would need to wrap around to include Jan, Feb, and Mar.
I read that it could be done with expressions and comparison predicates. But I couldn’t figure out a solution. Anyone got this working?
I thought it work to create a denormalized attribute called birthDateInYear2000 but, again, that suffers from the same overlap problem.
Any ideas?
Suggestion:
Fetch all birthdays.
Map the birthdays to the next occurrence from now
let calendar = NSCalendar.currentCalendar()
let nextBirthDays = birthdays.map { (date) -> NSDate in
let components = calendar.components([.Day, .Month], fromDate: date)
return calendar.nextDateAfterDate(NSDate(), matchingComponents: components, options: .MatchNextTime)!
}
Sort the dates
let sortedNextBirthDays = nextBirthDays.sort { $0.compare($1) == .OrderedAscending }
Now sortedNextBirthDays contains all upcoming birthdays sorted ascending.
In Core Data you could fetch records as dictionary with birthday and objectID (or full name), create a temporary struct, map and sort the items and get the person for the objectID (or use the full name) – or you even could apply the logic to an NSManagedObject array
Edit
Using an NSFetchedResultsController you can sort the table view only if the information about the next birthday is stored in the persistent store (assuming it's the MySQL-store), because you can't apply sort descriptors including keys pointing to transient or computed properties.
The best place to update the nextBirthDate property is just before creating the NSFetchedResultsController instance of the view controller.
Create an (optional) attribute nextBirthDate in the entity Person
Create a extension of NSDate to calculate the next occurrence of a date from now
extension NSDate {
var nextOccurrence : NSDate {
let calendar = NSCalendar.currentCalendar()
let components = calendar.components([.Day, .Month], fromDate: self)
return calendar.nextDateAfterDate(NSDate(), matchingComponents: components, options: .MatchNextTime)!
}
}
In the closure to initialize the NSFetchResultsController instance add code to update the nextBirthDate property of each record
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Person")
do {
let people = try self.managedObjectContext.executeFetchRequest(fetchRequest) as! [Person]
people.forEach({ (person) in
let nextBirthDate = person.birthDate.nextOccurrence
if person.nextBirthDate == nil || person.nextBirthDate! != nextBirthDate {
person.nextBirthDate = nextBirthDate
}
})
if self.managedObjectContext.hasChanges {
try self.managedObjectContext.save()
}
} catch {
print(error)
}
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptors = [NSSortDescriptor(key:"nextBirthDate", ascending: true)]
fetchRequest.sortDescriptors = sortDescriptors
...
If the view controller can edit the birthDate property don't forget to update the nextBirthDate property as well to keep the view in sync.
my suggestion is similar:
fetch all customers/persons
filter all birthdates that range 3 days before and 3 days after today
Use closure like the following:
func fetchBirthdayList(){
// this example considers all birthdays that are like
// |---TODAY---|
// Each hyphen represents a day
let date = Date()
let timeSpan = 3
let cal = Calendar.current
nextBirthDays = self.list.filter { (customer) -> Bool in
if let birthDate = customer.birthDate {
let difference = cal.dateComponents([.day,.year], from: birthDate as! Date, to: date)
return (( (difference.day! <= timeSpan) || ((365 - difference.day!) <= timeSpan) ) && difference.day! >= 0)
}
return false
}
}
If you want, you can order the result afterwards.
Cheers
Oliver

How to sort multiple array based on an array swift?

I would like to sort multiple array based on an array which is array of NSDate
var date = [NSDate]()
var name = [String]()
var job = [String]()
i would like to sort name and job based on the date. Example Like
date = [2016-04-02 01:03:42 +00002,2016-03-02 01:03:42 +0000,2016-05-02 01:03:42 +0000]
name = [john,alex,danson]
job = [engineer,programmer,swimmer]
i would like to sort the date from oldest to the latest then i would like to sort the name and job based on the date . Result Will be Like
date = [2016-03-02 01:03:42 +0000 ,2016-04-02 01:03:42 +0000 , 2016-05-02 01:03:42 +0000 ] //Being Sorted
name = [alex,john,danson]
job = [programmer,engineer,swimmer]
How can i do it ?
extension NSDate {
var day: Int { return NSCalendar.currentCalendar().components(.Day, fromDate: self).day }
}
let result = zip(zip(date, name), jobs).map { ($0.0.0, $0.0.1, $0.1) }.sort { $0.0.day < $1.0.day }
print(result)
This should do it. If you need an explanation, I'll try to explain.
If you want your existing array's to be sorted:
name = name.enumerate().sort { date[$0.index].day < date[$1.index].day }.map { $0.element }
jobs = jobs.enumerate().sort { date[$0.index].day < date[$1.index].day }.map { $0.element }
However, doing this is not safe because if the size of name or jobs is bigger than date, it will crash. You'll have to build some safety inside before using this.
What I forgot to mention is, you could also get the "original" arrays sorted by extracting it from result:
name = result.map { $0.1 }
jobs = result.map { $0.2 }
This is a programming problem instead of Swift problem.
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
let dateStrings = ["2016-03-02 01:04:42 +0000","2016-04-02 01:03:42 +0000" , "2016-05-02 01:03:42 +0000" ]
let date = dateStrings.map {
dateString in formatter.dateFromString(dateString)}
let name = ["alex","john","danson"]
let job = ["programmer","engineer","swimmer"]
let combine = date.enumerate().map {
index, date in
return (date!,name[index],job[index])
}
let result = combine.sort{
$0.0.compare($1.0) == NSComparisonResult.OrderedDescending
}

Resources