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.
Related
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.
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
I want to store some data downloaded from a remote file in CoreData. his data may already have been downloaded before and then the changes need to be added to core data.
I use the code displayed here. In three places I use a Fetchrequest to check if the data is already in the database. I use a predicate with the request. In the code I added comments to distinguish between them. If the item is already in coreData I update the data, else I instantiates a new item. The code to add the item to core data is in a convenience init in the class. This works fine. The problem I am getting is this:
The first time in this code id equals 0 so the schoolFetch is performed. Adding the predicate nr 1 gives no error and the school class is instantiated with its id set to 0.
Second time id = 1. This means a locationItem, with location being a part of the school, is checked for and instantiated. To add the location to the school a fetchrequest is performed to get the schoolInstance. Predicate 2 is used. This gives no error although the previous added schoolInstance is not retrieved, but it is if the predicate is not used (commented out).
After the schoolInstance is retrieved I use a fetchrequest to check if the location is already in coredata. If not it is instantiated, else its data is updated. The predicate nr 3 gives a run-time error stating nothing but EXC_BAD_ACCESS (code=1, address = 0x1). No error description is given in the debugger window.
So I have two questions:
a) why does predicate nr 2 not return the previous inserted item?
b) why does predicate nr 3 give an error?
I structured this code this way as it is possible to first receive the information on the location and after that the information on the school.
if id == 0 {
//get school
let schoolFetch = NSFetchRequest<StreekgidsSchool>(entityName: "School")
schoolFetch.predicate = NSPredicate(format: "locationId == %#", id) // predicate 1
do {
let fetchedSchool = try streekgidsModel?.context.fetch(schoolFetch)
if let school = fetchedSchool?.first{
school.name = name
school.locationId = id
school.colorValue = color
schoolInstance = school
}
else {
//create new entry in coredata for school
schoolInstance = StreekgidsSchool(name: name, context: (streekgidsModel?.context)!)
schoolInstance?.locationId = id
schoolInstance?.colorValue = color
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
}
else {
//check if school already is defined
let schoolFetch = NSFetchRequest<StreekgidsSchool>(entityName: "School")
schoolFetch.predicate = NSPredicate(format: "locationId == %#", 0) //predicate 2
do {
let fetchedSchool = try streekgidsModel?.context.fetch(schoolFetch)
if let school = fetchedSchool?.first{
schoolInstance = school
}
else {
//create new entry in coredata for school
schoolInstance = StreekgidsSchool(id: 0, context: (streekgidsModel?.context)!)
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
//get location
let locationFetch = NSFetchRequest<StreekgidsLocation>(entityName: "Location")
locationFetch.predicate = NSPredicate(format: "locationId == %#", id) //predicate 3
do {
let fetchedLocation = try streekgidsModel?.context.fetch(locationFetch)
if let location = fetchedLocation?.first{
location.name = name
location.locationId = id
location.colorValue = color
location.school = schoolInstance
}
else {
//create new entry in coredata for location
let locationInstance = StreekgidsLocation(name: name, context: (streekgidsModel?.context)!)
locationInstance.locationId = id
locationInstance.colorValue = color
}
} catch {
fatalError("Failed to fetch Schooldata: \(error)")
}
}
Exactly. The %# is expecting a Foundation object as the argument but an Int is not an object. So your id (set to zero) is being interpreted as nil. That is also why your predicate 2 is returning nothing and why you retrieve the result when you comment it out. Only your third predicate throws a run-time error but in fact all three of your predicates are working incorrectly, the first two are simply not falling over in an immediately visible way. Have you tried substituting %# for %d.
And as you point out %u for unsigned integers.
In my app when data is synced i can get 20k entries (from given timestamp) from the server that should be synced to the local device. For every entry i try to fetch it (if it exist already) and if doesn't i create new. The problem is that the whole operation is too slow - for 20k on iphone 5 is 10+ mins. Another solution that i though is to delete all entries from the given timestamp and create new entries for all returned entries and there will be no need to perform fetch for every single entry ? If someone have any advice will be nice. Here is sample code for the current state:
var logEntryToUpdate:LogEntry!
if let savedEntry = CoreDataRequestHelper.getLogEntryByID(inputID: inputID, fetchAsync: true) {
logEntryToUpdate = savedEntry
} else {
logEntryToUpdate = LogEntry(entity: logEntryEntity!, insertInto: CoreDataStack.sharedInstance.saveManagedObjectContext)
}
logEntryToUpdate.populateWithSyncedData(data: row, startCol: 1)
Here is the actual request method:
class func getLogEntryByID(inputID:Int64, fetchAsync:Bool) ->LogEntry? {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
if let fetchResults = try mocToFetch.fetch(logEntryRequest) as? [LogEntry] {
if ( fetchResults.count > 0 ) {
return fetchResults[0]
}
return nil
}
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return nil
}
Another thing that i tried is to check the count for specific request but again is too slow.
class func doesLogEntryExist(inputID:Int64, fetchAsync:Bool) ->Bool {
let logEntryRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "LogEntry")
logEntryRequest.predicate = NSPredicate(format: "inputId == %#", NSNumber(value: inputID as Int64))
//logEntryRequest.resultType = .countResultType
logEntryRequest.fetchLimit = 1
do {
let mocToFetch = fetchAsync ? CoreDataStack.sharedInstance.saveManagedObjectContext : CoreDataStack.sharedInstance.managedObjectContext
let count = try mocToFetch.count(for: logEntryRequest)
if ( count > 0 ) {
return true
}
return false
} catch let error as NSError {
NSLog("Error fetching Log Entries by inputID from core data !!! \(error.localizedDescription)")
}
return false
}
Whether fetching the instance or getting the count, you're still doing one fetch request per incoming record. That's going to be slow, and your code will be spending almost all of its time performing fetches.
One improvement is to batch up the records to reduce the number of fetches. Get multiple record IDs into an array, and then fetch all of them at once with a predicate like
NSPredicate(format: "inputId IN %#", inputIdArray)
Then go through the results of the fetch to see which IDs were found. Accumulate 50 or 100 IDs in the array, and you'll reduce the number of fetches by 50x or 100x.
Deleting all the entries for the timestamp and then re-inserting them might be good, but it's hard to predict. You'll have to insert all 20,000. Is that faster or slower than reducing the number of fetches? It's impossible to say for sure.
Based on Paulw11's comment, I came up with the following method to evaluate Structs being imported into Core Data.
In my example, I have a class where I store search terms. Within the search class, create a predicate which describes the values of the stuff within my array of structs.
func importToCoreData(dataToEvaluateArray: [YourDataStruct]) {
// This is what Paul described in his comment
let newDataToEvaluate = Set(dataToEvaluateArray.map{$0.id})
let recordsInCoreData = getIdSetForCurrentPredicate()
let newRecords = newDataToEvaluate.subtracting(recordsInCoreData)
// create an empty array
var itemsToImportArray: [YourDataStruct] = []
// and dump records with ids contained in newRecords into it
dataToEvaluateArray.forEach{ record in
if newRecords.contains(record.id) {
itemsToImportArray.append(record)
}
}
// THEN, import if you need to
itemsToImportArray.forEach { struct in
// set up your entity, properties, etc.
}
// Once it's imported, save
// You can save each time you import a record, but it'll go faster if you do it once.
do {
try self.managedObjectContext.save()
} catch let error {
self.delegate?.errorAlert(error.localizedDescription, sender: self)
}
self.delegate?.updateFetchedResultsController()
}
To instantiate recordsInCoreData, I created this method, which returns a Set of unique identifiers that exist in the managedObjectContext:
func getIdSetForCurrentPredicate() -> Set<String> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "YourEntity")
// searchQuery is a class I created with a computed property for a creating a predicate. You'll need to write your own predicate
fetchRequest.predicate = searchQuery.predicate
var existingIds: [YourEntity] = []
do {
existingIds = try managedObjectContext.fetch(fetchRequest) as! [YourEntity]
} catch let error {
delegate?.errorAlert(error.localizedDescription, sender: self)
}
return Set<String>(existingIds.map{$0.id})
}
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