I try to select a date (today) in a MultiDatePicker programmatically via a Button. The selection works as expected and the onChange modifier will be called and the day today is marked as selected in the Picker. But when I try to deselect the date directly in the MultiDatePicker, the date is not marked anymore but the onChange modifier will not get called. If you tap on another date in the MultiDatePicker now, both dates, the other date and the date today are marked as selected.
import SwiftUI
struct ContentView: View {
#Environment(\.calendar) private var calendar
#State private var selectedDates: Set<DateComponents> = []
#State private var onChangeCounter = 0
var body: some View {
VStack {
MultiDatePicker("Select dates", selection: $selectedDates)
.frame(height: UIScreen.main.bounds.width)
.onChange(of: selectedDates) { _ in
self.onChangeCounter += 1
}
Button("Select today") {
let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
selectedDates.insert(todayDatecomponents)
}
.foregroundColor(.white)
.frame(minWidth: 150)
.padding()
.background(Color.accentColor)
.cornerRadius(20)
HStack {
Text("onChangeCounter")
Spacer()
Text(String(onChangeCounter))
}
.padding()
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Am I doing something wrong or is it just not possible to select a date in the MultiDatePicker programmatically?
Thank You!
For purposes of this discussion, Today means December 17, 2022
The issue is that Date.now is not equal to Today
I'm in US East Coast Time Zone... if I add a button to print(Date.now) and tap it, I see this in the debug console:
2022-12-17 14:08:52 +0000
if I tap it again 4-seconds later, I see this:
2022-12-17 14:08:56 +0000
Those two dates are not equal.
So, let's find out what the MultiDatePicker is using for it's selection.
Change your MultiDatePicker to this:
MultiDatePicker("Select dates", selection: $selectedDates)
.frame(height: UIScreen.main.bounds.width)
.onChange(of: selectedDates) { _ in
print("onChange")
selectedDates.forEach { d in
print(d)
}
print()
self.onChangeCounter += 1
}
If I run the app and select Dec 14, 19 and 8, I see this:
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 19 isLeapMonth: false
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 19 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false
Now, I de-select the 19th, and I see this:
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false
The 19th was correctly removed from the Set.
Now, I tap your "Select today" button, and I see this:
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 24 second: 11 nanosecond: 339460015 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false
As we can see, these two lines:
let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
selectedDates.insert(todayDatecomponents)
Insert a DateComponents object with a lot more detail.
If I tap "Select today" again, I get this:
onChange
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 8 isLeapMonth: false
calendar: gregorian (current) era: 1 year: 2022 month: 12 day: 14 isLeapMonth: false
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 24 second: 11 nanosecond: 339460015 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false
calendar: gregorian (current) timeZone: America/New_York (fixed (equal to current)) era: 1 year: 2022 month: 12 day: 17 hour: 9 minute: 26 second: 39 nanosecond: 866878032 weekday: 7 weekdayOrdinal: 3 quarter: 0 weekOfMonth: 3 weekOfYear: 51 yearForWeekOfYear: 2022 isLeapMonth: false
Now selectedDates contains two "today dates" ... 2-minutes and 28-seconds apart.
When I tap the 17th on the calendar, there is no matching date in that set to remove... so when the calendar refreshes (such as when I select another date), the 17th still shows as selected.
So, let's change the programmatically inserted DateComponents to match the calendar's data:
let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: Date.now)
selectedDates.insert(todayDatecomponents)
Now when we tap 17 on the calendar it will be de-selected and the matching object selectedDates will be removed.
Here's how I modified your code to debug:
import SwiftUI
#available(iOS 16.0, *)
struct MultiDateView: View {
#Environment(\.calendar) private var calendar
#State private var selectedDates: Set<DateComponents> = []
#State private var onChangeCounter = 0
var body: some View {
VStack {
MultiDatePicker("Select dates", selection: $selectedDates)
.frame(height: UIScreen.main.bounds.width)
.onChange(of: selectedDates) { _ in
print("onChange")
selectedDates.forEach { d in
print(d)
}
print()
self.onChangeCounter += 1
}
Button("Select today") {
let todayDatecomponents = calendar.dateComponents(in: calendar.timeZone, from: Date.now)
selectedDates.insert(todayDatecomponents)
}
.foregroundColor(.white)
.frame(minWidth: 150)
.padding()
.background(Color.accentColor)
.cornerRadius(20)
Button("Select today the right way") {
let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: Date.now)
selectedDates.insert(todayDatecomponents)
}
.foregroundColor(.white)
.frame(minWidth: 150)
.padding()
.background(Color.green)
.cornerRadius(20)
HStack {
Text("onChangeCounter")
Spacer()
Text(String(onChangeCounter))
}
.padding()
Button("Print Date.now in debug console") {
print("debug")
print("debug:", Date.now)
print()
}
.foregroundColor(.white)
.frame(minWidth: 150)
.padding()
.background(Color.red)
.cornerRadius(20)
Spacer()
}
}
}
#available(iOS 16.0, *)
struct MultiDateView_Previews: PreviewProvider {
static var previews: some View {
MultiDateView()
}
}
it looks like MultiDatePicker is saving additional components.
Use this to set the today components:
let todayDatecomponents = calendar.dateComponents([.calendar, .era, .year, .month, .day], from: .now)
Related
May I know, what is reliable way, to calculate day differences without taking time into consideration?
A similar question is asked before. However, the highest voted and accepted answer isn't entirely accurate - https://stackoverflow.com/a/28163560/72437
The code is broken, when dealing with Day light saving case. You can run the following code in Playground
Use startOfDay (Broken)
import UIKit
struct LocalDate: Equatable {
let year: Int
let month: Int
let day: Int
}
struct LocalTime: Equatable, Codable {
let hour: Int
let minute: Int
}
extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
var dateComponents = DateComponents()
dateComponents.year = localDate.year
dateComponents.month = localDate.month
dateComponents.day = localDate.day
dateComponents.hour = localTime.hour
dateComponents.minute = localTime.minute
dateComponents.second = 0
return Calendar.current.date(from: dateComponents)!
}
func adding(_ component: Calendar.Component, _ value: Int) -> Date {
return Calendar.current.date(byAdding: component, value: value, to: self)!
}
}
// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
// Just a random local time. We will use 'startOfDay' to perform local time resetting.
let localTime = LocalTime(hour: 2, minute: 59)
let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)
let date1 = Date.of(localDate: localDate1, localTime: localTime).startOfDay
let date2 = Date.of(localDate: localDate2, localTime: localTime).startOfDay
let components = Calendar.current.dateComponents([.day], from: date1, to: date2)
/*
date1 Monday, March 22, 2021 at 1:00:00 AM Iran Daylight Time
date2 Tuesday, March 23, 2021 at 12:00:00 AM Iran Daylight Time
diff in day is Optional(0)
*/
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")
The different of day should be 1, without taking time into consideration. However, due to day light saving, the computed hour difference is 23 hours instead of 24 hours.
We are then getting 0 day difference.
One of the workaround, is using 12:00 (noon) as local time, with an assumption there is no place in this world, where day light saving occurs during 12:00. I am not sure how solid is this assumption. Such assumption seems to be pretty fragile. What if one day government decides to admen day light saving to be at 12:00?
Use 12:00 (Seems to work. But, how solid it is?)
import UIKit
struct LocalDate: Equatable {
let year: Int
let month: Int
let day: Int
}
struct LocalTime: Equatable, Codable {
let hour: Int
let minute: Int
}
extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
var dateComponents = DateComponents()
dateComponents.year = localDate.year
dateComponents.month = localDate.month
dateComponents.day = localDate.day
dateComponents.hour = localTime.hour
dateComponents.minute = localTime.minute
dateComponents.second = 0
return Calendar.current.date(from: dateComponents)!
}
func adding(_ component: Calendar.Component, _ value: Int) -> Date {
return Calendar.current.date(byAdding: component, value: value, to: self)!
}
}
// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
// Use noon
let localTime = LocalTime(hour: 12, minute: 00)
let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)
let date1 = Date.of(localDate: localDate1, localTime: localTime)
let date2 = Date.of(localDate: localDate2, localTime: localTime)
let components = Calendar.current.dateComponents([.day], from: date1, to: date2)
/*
date1 Monday, March 22, 2021 at 12:00:00 PM Iran Daylight Time
date2 Tuesday, March 23, 2021 at 12:00:00 PM Iran Daylight Time
diff in day is Optional(1)
*/
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")
May I know, what is reliable way, to calculate day differences without taking time into consideration?
Date is a precise point in time, hence expressible as a TimeInterval (aka Double) from an exact moment in time (that'll be reference date aka January 1st 2001 00:00 GMT+0).
Thus that same point in time is differently calculated between TimeZones through Calendar: if the TimeZone has daylight savings, then the calendar take it into account.
Therefore when you operate through a Calendar adopting DateComponents you should keep that in mind.
Depending on what you are trying to do in your application it could be useful to just adopt a private Calendar instance set to adopt TimeZone(secondsFromGMT: 0)! for calculating dates as absolutes values.
As in:
extension Calendar {
static let appCal: Self = {
// I'm used to reason with Gregorian calendar
var cal = Calendar(identifier: .gregorian)
// I just need this calendar for executing absolute time calculations
cal.timeZone = TimeZone(secondsFromGMT: 0)!
return cal
}()
}
I need to display current month/first weekday on the xAxis of the graph. Can anyone help me out with this?
How can I get current month's first day of all weeks.
You can create a DateComponents with the components:
year: someYear,
month: someMonth,
weekday: someCalendar.firstWeekday,
weekOfMonth: n
use Calendar.date(from:) to get the Date corresponding to those components. Now you just need to vary n from 1 to 5, and put each result into an array.
var firstsOfWeek = [Date]()
let year = 2020
let month = 10
let calendar = Calendar.current
for n in 1...5 {
let dc = DateComponents(
year: year,
month: month,
weekday: calendar.firstWeekday,
weekOfMonth: n
)
firstsOfWeek.append(calendar.date(from: dc)!)
}
However, date(from:) will return a date that is not in someMonth if the first week of someMonth is only partially inside someMonth. For example, (assuming the week starts on a Sunday) the first week of October 2020 starts on 27 September, and date(from:) will give you that date if you ask it what the date corresponding to weekOfMonth: 1 is.
If you don't want that date. simply add a check to only include the start of month if it is a start of week as well, and change the for loop range to 2...5:
let firstDayOfMonth = calendar.date(from: DateComponents(year: year, month: month))!
if calendar.component(.weekday, from: firstDayOfMonth) == calendar.firstWeekday {
firstsOfWeek.append(firstDayOfMonth)
}
for n in 2...5 {
...
But do note that if you do this, the resulting array will sometimes have 4 elements, and sometimes 5 elements.
You can get the .yearForWeekOfYear for the current date, get the range of weekOfYear in month for that date and return all dates in same month for the range:
extension Date {
func month(using calendar: Calendar = .current) -> Int {
calendar.component(.month, from: self)
}
func firstWeekdaysInMonth(using calendar: Calendar = .current) -> [Date] {
var components = calendar.dateComponents([.calendar, .yearForWeekOfYear], from: self)
return calendar.range(of: .weekOfYear, in: .month, for: self)?.compactMap {
components.weekOfYear = $0
return components.date?.month(using: calendar) == month(using: calendar) ? components.date : nil
} ?? []
}
}
Another option is to get the start of the month for that specific date, enumerateDates after that date that matches the first weekday of the calendar and stop if the resulting date is not at the same month of that date:
extension Date {
func firstWeekdaysInMonth(using calendar: Calendar = .current) -> [Date] {
let year = calendar.component(.year, from: self)
let month = calendar.component(.month, from: self)
let start = DateComponents(calendar: calendar, year: year, month: month).date!
let matching = DateComponents(weekday: calendar.firstWeekday)
var dates: [Date] = []
calendar.enumerateDates(startingAfter: start, matching: matching, matchingPolicy: .strict) { date, _, stop in
guard let date = date, calendar.component(.month, from: date) == month else {
stop = true
return
}
dates.append(date)
}
return dates
}
}
let dates = Date().firstWeekdaysInMonth() // "Oct 4, 2020 at 12:00 AM", "Oct 11, 2020 at 12:00 AM", "Oct 18, 2020 at 12:00 AM", "Oct 25, 2020 at 12:00 AM"]
let august = DateComponents(calendar: .current, year: 2020, month: 8, day: 10).date!
august.firstWeekdaysInMonth() // ["Aug 2, 2020 at 12:00 AM", "Aug 9, 2020 at 12:00 AM", "Aug 16, 2020 at 12:00 AM", "Aug 23, 2020 at 12:00 AM", "Aug 30, 2020 at 12:00 AM"]
I have a function below (datesCheck) that cycles through an array of dates and firstly removes any entries if there is more than one a day and then checks whether the dates are consecutive within the array, returning the number of consecutive days from the current date.
func datesCheck(_ dateArray: [Date]) -> Int {
let calendar = Calendar.current
let uniqueDates = NSSet(array: dateArray.map { calendar.startOfDay(for: $0) }).sorted {
($0 as AnyObject).timeIntervalSince1970 > ($1 as AnyObject).timeIntervalSince1970
} as! [Date]
var lastDate = calendar.startOfDay(for: Date())
var i = 0
while i < uniqueDates.count && uniqueDates[i].compare(lastDate) == .orderedSame {
lastDate = (calendar as NSCalendar).date(byAdding: .day, value: -1, to: lastDate, options: [])!
i += 1
}
numberOfConsecutiveDays = i
return i
}
This function works well but I want to only apply this to dates that are Monday – Friday, with the consecutive dates checker checking Friday – Monday, effectively ignoring saturday and sunday. I have tried to achieve this using calendar.components but cannot find a way to ignore weekends when checking if the dates are consecutive excluding weekends.
let today = Date()
let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
let components = calendar!.components([.weekday], fromDate: today)
if components.weekday == 2 {
print("Monday")
} else {
print("Monday")
}
A couple points:
Since you don't need weekends, filter them out
Your function is non-deterministic, since it uses the current time (Date()). The result is theoretically different for every run. I added a second parameter fromDate to make it deterministic.
func datesCheck(_ dates: [Date], fromDate: Date = Date()) -> Int {
let calendar = Calendar.current
let weekdays = dates.map { calendar.startOfDay(for: $0) }
.filter { 2...6 ~= calendar.component(.weekday, from: $0) }
let uniqueDates = Set(weekdays).sorted(by: >)
var i = 0
var lastDate = calendar.startOfDay(for: fromDate)
while i < uniqueDates.count && uniqueDates[i] == lastDate {
switch calendar.component(.weekday, from: uniqueDates[i]) {
case 2:
// When the lastDate is a Monday, the previous weekday is 3 days ago
lastDate = calendar.date(byAdding: .day, value: -3, to: lastDate)!
default:
// Otherwise, it's 1 day ago
lastDate = calendar.date(byAdding: .day, value: -1, to: lastDate)!
}
i += 1
}
return i
}
Test:
let dates = [
DateComponents(calendar: .current, year: 2017, month: 8, day: 29).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 28).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 25).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 24).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 22).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 21).date!
]
let today = DateComponents(calendar: .current, year: 2017, month: 8, day: 29).date!
print(datesCheck(dates, fromDate: today)) // 4
The code prints 4 since the gap between the 28th and 25th is ignored according to the weekend rule.
When I run the following (on January 7 2017):
let dateComponents = Calendar.current.dateComponents(in: TimeZone.current, from: date)
print("dateComponents = \(dateComponents)")
let trigger = UNCalendarNotificationTrigger(dateMatching:dateComponents, repeats: false)
let nextDate = trigger.nextTriggerDate()
print("nextDate = \(nextDate)")
then I get the following output:
dateComponents = calendar: gregorian (current) timeZone: Europe/Stockholm (current) era: 1 year: 2017 month: 1 day: 8 hour: 21 minute: 34 second: 0 nanosecond: 0 weekday: 1 weekdayOrdinal: 2 quarter: 0 weekOfMonth: 1 weekOfYear: 1 yearForWeekOfYear: 2017 isLeapMonth: false
nextDate = nil
Question: why is trigger.nextTriggerDate() = nil ?
UPDATE: I had the feeling that my dateComponents could be overdetermined. I therefore introduced a nextEvent that only contained the day hour and minute of the dateComponents:
var nextEvent = DateComponents()
nextEvent.day = dateComponents.day
nextEvent.hour = dateComponents.hour
nextEvent.minute = dateComponents.minute
let trigger = UNCalendarNotificationTrigger(dateMatching:nextEvent, repeats: false)
when I now invoke trigger.nextTriggerDate() it becomes
nextDate = Optional(2017-01-08 20:34:00 +0000)
as it should. But I do not understand why I cannot use the dateComponents when I create the trigger.
During my tests I found dateComponents.quarter = 0 to be the cause of the problems. Zero quarter is obviously wrong, it should be quarter = 1 It seems there is an old bug that causes the date components to have an invalid quarter.
If you manually set dateComponents.quarter = 1 or just dateComponents.quarter = nil, everything starts to work.
I have a function to create a date...
func date(withYear year: Int, month: Int, day: Int) -> Date {
let components = DateComponents(calendar: Calendar.current, timeZone: nil, era: nil, year: year, month: month, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0, weekday: nil, weekdayOrdinal: nil, quarter: nil, weekOfMonth: nil, weekOfYear: nil, yearForWeekOfYear: nil)
return components.date!
}
It just takes the day, month and year as a convenience.
So I create two dates.
let startDate = date(withYear: 2016, month: 9, day: 30) // prints to console as 2016-09-29 23:00:00 +0000
let endDate = date(withYear: 2016, month: 11, day: 1) // prints to console as 2016-11-01 00:00:00 +0000
And then calculate the number of months between them...
let components = Calendar.current.dateComponents([.month], from: startDate, to: endDate)
print(components.month) // returns 1
Shouldn't this be 2? How is the month component calculated? Is it just the number of days divided by 30 or something? If so I'll need a new way of calculating the number of months here.
I have the same issue, but NSCalendar is working fine this code example proofs this.
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
let components = calendar.components([.Day , .Month , .Year], fromDate: date)
let twoMonthdate = calendar.dateByAddingUnit(.Month, value: 2, toDate:date, options: [])
let difference = calendar.components([.Month], fromDate: date, toDate: twoMonthdate!, options: [])
//differences is 2 monthses
In you're case difference is 1 month and a few days. And that is why you receive such result.