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
}
I would like to find out the index of an array by an NSDate.
Ill tried:
var sectionsInTable = [NSDate]()
let indexOfElement = sectionsInTable.indexOf(date) // where date is an NSDate in my sectionsInTable Array
print(indexOfElement)
But ill always get false
How is it possible to get the index of an NSDate from an array?
Thanks in advance.
If you have exact copies of NSDate objects, your code should work:
let date = NSDate()
let date2 = date.copy() as! NSDate
var sectionsInTable: [NSDate] = [date]
let indexOfElement = sectionsInTable.indexOf(date2)
print(indexOfElement)
//prints: Optional(0)
Your approach should work fine. This code produces an index of 2:
let s = 31536000.0 // Seconds per Year
var tbl = [NSDate]()
tbl.append(NSDate(timeIntervalSince1970: 40*s)) // 0
tbl.append(NSDate(timeIntervalSince1970: 41*s)) // 1
tbl.append(NSDate(timeIntervalSince1970: 42*s)) // 2
tbl.append(NSDate(timeIntervalSince1970: 43*s)) // 3
let date = NSDate(timeIntervalSince1970: 42*s)
let indexOfElement = tbl.indexOf(date)
The most likely reason that you are not getting the proper index is that your search NSDate has a time component that does not match the time component in NSDate objects in the array. You can confirm that this is the case by printing both objects, and verifying their time component.
Since comparison depends on how deep you want to go with date time thing. I think you should just loop through your date array and compare if it's equal and return that index.
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
I'd like to add my dictionary with array in it to my arrayOfDictionary, i tried the suggested answer on this link, but it didn't work for me. I'm having problem on appending object in my dictionary.
Here's my code:
func formatADate() {
var dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = NSDateFormatterStyle.LongStyle
var journalDictionary = Dictionary<String, [Journal]>()
for index in 0..<self.sortedJournal.count {
let dateString = dateFormatter.stringFromDate(self.sortedJournal[index].journalDate)
var oldDateString = ""
// Split Date
var dateStringArr = split(dateString){$0 == " "}
var newDateString = "\(dateStringArr[0]) \(dateStringArr[2])"
// Append journal to corresponding dates
// journalDictionary[newDateString]? += [self.sortedJournal[index]]
// journalDictionary[newDateString]!.append(self.sortedJournal[index])
journalDictionary[newDateString]?.append(self.sortedJournal[index])
// add to array of journal
if !(newDateString == oldDateString) || (oldDateString == "") {
arrayOfJournalDictionary.append(journalDictionary)
}
}
println(arrayOfJournalDictionary)
}
With this line of code, it gives me empty object.
journalDictionary[newDateString]?.append(self.sortedJournal[index])
Any suggestion on how I can accomplish/fix this? I'm pretty amateur in Swift, so any help would be appreciated! Thank you.
Woah, date magic with string splits and stuff, yeiks. You already are using NSDateFormatters, for the wrong reasons though. Since journalDate is a date, use the formatter to give you the exact string you want from that date, dont let it give you some generic string and split and concatenate and argh... NOBODY will understand what you are trying to do there.
Back to the question:
Your journalDictionary is empty, if you write
journalDictionary[newDateString]?.append(self.sortedJournal[index])
You tell the program to read at "index" newDateString and if there is something take that and append a new value to it.
Do you see the mistake already?
if there is something -> You have no logic yet to insert the initial empty [Journal] for any given key.
At some point in your code you have to set up the array like so:
journalDictionary["myKey"] = []
Or probably more likely:
if journalDictionary[newDateString] == nil {
journalDictionary[newDateString] = [self.sortedJournal[index]]
} else {
journalDictionary[newDateString]?.append(self.sortedJournal[index])
}
I'm a beginner in Swift and coding in general. Right now I'm trying to develop the piece of the code to set up the time limit for the action only once a day.
#IBAction func yesButtonPressed(sender: AnyObject) {
//To retrive the control date value. First time it has nil value
var controlDate = NSUserDefaults.standardUserDefaults().objectForKey("controlDate") as? NSDate
//To check current date to compare with
var currentDate = NSDate()
// To check if time interval between controlDate and currentDate is less than 1 day
var timeInterval = controlDate?.timeIntervalSinceNow
var dayInSeconds = 24 * 3600
if timeInterval < dayInSeconds {
//show alert with message "You've done it recently. Pls wait a bit"
} else {
//perfome the action
//update the value of NSUserDefaults.standardUserDefaults().objectForKey("controlDate") with current time stamp
}
}
Instead of checking if the controlTime var has nil value to catch the App first time running I was trying to develop some shorter, universal code for both case, first time and the rest times when the controlDate var will be saved in UserDefaults.
Nevertheless it doesn't work properly (( I'd appreciate your help a lot!
dayInSeconds is the wrong type to make the comparison. Type inference is determining that it should be an Int while timeIntervalSinceNow is a Double under the covers. And thus, you can't definitively compare an Int and a Double with accuracy.
Create dayInSeconds in this way so that it's type is inferred as a Double, rather than an Int.
var controlDate = NSUserDefaults.standardUserDefaults().objectForKey("controlDate") as? NSDate
var currentDate = NSDate()
var timeInterval = controlDate?.timeIntervalSinceNow
//*** HERES THE CHANGE***
var dayInSeconds = 24.0 * 3600
if timeInterval < dayInSeconds {
}
Since you aren't attempting to call a method on timeInterval optional unwrapping is not necessary in this case.
timeInterval is an Optional, you need to unwrap it first
if let ti = timeInterval {
if ti < dayInSeconds {
...
}
}
More on Optionals