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
}
How do I list yesterday's date in SwiftUI? It probably is a simple answer but I'm just learning to code and for some reason I can't seem to find the solution anywhere. Is it because it is too easy?
struct DateShown: View {
let datechoice: Datechoice
var body: some View {
Text(currentDate(date: Date()))
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.blue)
}
func currentDate(date: Date!) -> String {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "MMMM d, yyyy"
return date == nil ? "" : formatter.string(from: date)
}
}
I would rather use View extensions, though you also need Date formatting so I went the easier way and extended your solution. If the number at line "dayComponent.day" is positive, you go futher in time. I tested under:
swift 5
xcode 11.3.1
iOS 13.3.1 non beta
func yesterDay() -> String {
var dayComponent = DateComponents()
dayComponent.day = -1
let calendar = Calendar.current
let nextDay = calendar.date(byAdding: dayComponent, to: Date())!
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateFormat = "MMMM d, yyyy"
return formatter.string(from: nextDay). //Output is "March 6, 2020
}
Usage is the same as yours:
Text(yesterDay())
The following code, in Swift 1, throws the error: Cannot call value of non-function type 'Calendar'
class CyclicDay {
lazy var baseline: NSDate? = {
var components = NSDateComponents()
components.day = 02
components.month = 05
components.year = 2018
return NSCalendar.currentCalendar.datefromComponents(components)
}()
}
What are the Swift 3 equivalents of the NSDate and NSCalendar functions?
The equivalents of NSDate, NSDateComponents & NSCalendar for Swift 3 are:
Date
DateComponents
Calendar
Actually every (NS)Type has equivalent Type in Swift 3.
Now in Swift 3, your code will look like this:
class CyclicDay {
lazy var baseline: Date? = {
var components = DateComponents()
components.day = 02
components.month = 05
components.year = 2018
return Calendar.current.date(from: components)
}()
}
I want to add local notification in my app. I am filling information in textfields and set date, time and reminder before particular date selected for the exam. Anyone implement such a demo then please suggest me what to do.
Answer is based on what ever i understood, Please change the time and reminder string as per your requirement.
func scheduleNotification(InputUser:String) {
let now: NSDateComponents = NSCalendar.currentCalendar().components([.Hour, .Minute], fromDate: NSDate())
let cal = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let date = cal.dateBySettingHour(now.hour, minute: now.minute + 1, second: 0, ofDate: NSDate(), options: NSCalendarOptions())
let reminder = UILocalNotification()
reminder.fireDate = date
reminder.alertBody = InputUser
reminder.alertAction = "Cool"
reminder.soundName = "sound.aif"
reminder.repeatInterval = NSCalendarUnit.Minute
UIApplication.sharedApplication().scheduleLocalNotification(reminder)
print("Firing at \(now.hour):\(now.minute+1)")
}
Set up Daily basic Local Notification 1 Day before or you can be modified it with the help of specific date in Swift 3.1
import UIKit
import UserNotifications
fileprivate struct AlarmKey{
static let startWorkIdentifier = "com.Reminder.Notification" //Add your own Identifier for Local Notification
static let startWork = "Ready for work? Toggle To \"Available\"."
}
class AlarmManager: NSObject{
static let sharedInstance = AlarmManager()
override init() {
super.init()
}
//MARK: - Clear All Previous Notifications
func clearAllNotifications(){
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
} else {
UIApplication.shared.cancelAllLocalNotifications()
}
}
func addReminder(with _ hour: Int, minutes: Int){
clearAllNotifications()
var dateComponent = DateComponents()
dateComponent.hour = hour // example - 7 Change you time here Progrmatically
dateComponent.minute = minutes // example - 00 Change you time here Progrmatically
if #available(iOS 10.0, *) {
dateComponent.timeZone = TimeZone.autoupdatingCurrent
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponent, repeats: false) //Set here **Repeat** condition
let content = UNMutableNotificationContent()
content.body = AlarmKey.startWork //Message Body
content.sound = UNNotificationSound.default()
let notification = UNNotificationRequest(identifier: AlarmKey.startWorkIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().add(notification) {(error) in
if let error = error {
print("Uh oh! We had an error: \(error)")
}
}
} else {
//iOS *9.4 below if fails the Above Condition....
dateComponent.timeZone = NSTimeZone.system
let calender = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)!
let date = calender.date(from: dateComponent)!
let localNotification = UILocalNotification()
localNotification.fireDate = date
localNotification.alertBody = AlarmKey.startWork
localNotification.repeatInterval = NSCalendar.Unit.day
localNotification.soundName = UILocalNotificationDefaultSoundName
UIApplication.shared.scheduleLocalNotification(localNotification)
}
}
}
//This is optional method if you want to show your Notification foreground condition
extension AlarmManager: UNUserNotificationCenterDelegate{
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Swift.Void){
completionHandler([.alert,.sound])
}
}
I have a timetable app, and after converting everything to Swift 3, one particular line threw an EXC_BAD_INSTRUCTION error, stating "Unexpectedly found nil while unwrapping an Optional value"
Here is the code, the final line returns the error:
class CyclicDay {
enum CyclicDayError: Error {
case invalidStartDate }
lazy var baseline: Date! = {
var components = DateComponents()
components.day = 27
components.month = 3
components.year = 2017
return Calendar.current.date(from: components)!
}()
func dayOfCycle(_ testDate: Date) throws -> Int {
if let start = baseline {
let interval = testDate.timeIntervalSince(start as Date)
let days = interval / (60 * 60 * 24)
return Int(days.truncatingRemainder(dividingBy: 14)) + 1 }
throw CyclicDayError.invalidStartDate }}
override func viewDidLoad() {
// Do any additional setup after loading the view, typically from a nib.
let cd = CyclicDay()
let day = try! cd.dayOfCycle(Date())
let date = Date()
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute], from: date)
let hour = components.hour
let minutes = components.minute
_ = "\(String(describing: hour)):\(String(describing: minutes))"
let lengthTestHour = "\(String(describing: hour))"
let lengthTestMinute = "\(String(describing: minutes))"
let formatter = DateFormatter()
formatter.dateFormat = "a"
formatter.amSymbol = "AM"
formatter.pmSymbol = "PM"
let dateString = formatter.string(from: Date())
var finalHour = String()
if lengthTestHour.characters.count == 1 {
finalHour = String("0\(String(describing: hour))")
} else {
finalHour = "\(String(describing: hour))"
}
if lengthTestMinute.characters.count == 1 {
_ = "0\(String(describing: minutes))"
} else {_ = minutes }
let convert = finalHour
let mTime = Int(convert)
// mTime * 100 + minutes
let compTime = mTime! * 100 + minutes!
In Swift 3 all date components are optional, you need to unwrap the optionals
let hour = components.hour!
let minutes = components.minute!
otherwise you get in trouble with the string interpolations.
Btw: You don't need String(describing just write for example
_ = "\(hour):\(minutes)"
I'm wondering anyway why you do all the formatting stuff manually instead of using the date formatter you created.
The problem lies in these two lines:
let lengthTestHour = "\(String(describing: hour))"
let lengthTestMinute = "\(String(describing: minutes))"
You thought lengthTestHour will store a value like "7" and lengthTestMinute will have a value like "33". But no, lengthTestHours actually holds "Optional(7)" and lengthTestMinutes actually holds "Optional(33)".
You then assign lengthTestHour to convert and try to convert that Optional(7) thing into an Int, which obviously can't be done. Now mTime is nil and you try to force unwrap in the last line. BOOM!
This is because String(describing:) returns an optional. The two lines can be shortened and fixed by doing:
let lengthTestHour = "\(hour!)"
let lengthTestMinute = "\(minute!)"