Swift/SwiftUI: week/date management in swift - swift

I'm working on fitness app where users selects the days he wants to exercise on.
When he opens the app I wanna shown him the current week where he can observe the days his training sessions are scheduled for.
If he is from the US i wanna show him a week starting from Sunday. For EU users it should start with Monday.
Is there any way to get the "current" week dates depending on user's location/geo? Taking into account what day does the week start with in appropriate location.

I tried to find a solution for your question. I think this should work:
// Define a function that returns the following seven dates, given a start date
func getWeekDates(of startDate: Date, with calender: Calendar) -> [Date] {
var weekDates: [Date] = []
for i in 0..<7 {
weekDates.append(calendar.date(byAdding: .day, value: i, to: startDate)!)
}
return weekDates
}
// This should automatically take the right calendar for the user's locale
// If you want to specify the day weeks start with manually, choose .gregorian or .iso8601:
// .gregorian starts on Sunday, .iso8601 starts on Monday
let calendar = Calendar.current
let startOfCurrentWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date()))
let currentWeekDates = getWeekDates(of: startOfCurrentWeek!, with: calendar)
Hope this helps.

Use
Calendar.current.firstWeekday
If it returns 1, then Sunday is the first day of week
If it returns 2, then Monday is the first day of week.
You can test this by setting locale manually
var calendar = Calendar.current
calendar.locale = Locale(identifier: "en_GB")
print("\(calendar.locale!) starts on day \(calendar.firstWeekday)")
// en_GB starts on day 2
calendar.locale = Locale(identifier: "en_US")
print("\(calendar.locale!) starts on day \(calendar.firstWeekday)")

Related

Working with Date() in Swift and having issues adjusting timezones for storing and reading back in Firestore/Firebase

I am storing dates from a Swift Project in Firestore without any problem. Dates are converted into UTC format and stored in Firestore as a Timestamp. All good.
Then, back on clientside, I can read them back and apply the TimeZone.current and the date/time are adjusted accordingly based on the timezone the user is currently in.
So, as en example, a time originally of:
9:00 pm Melbourne time (which is GMT+10),
shows as 7:00 am if the user is in New York.
Great.
But I have some items that I want to adjust for timezones (as per above) and others I don't.
So say I have two items the same as the above example, but one is an alarm and I want to keep at the time it was originally set for regardless of the new timezone... So still keep it at 9:00 pm.
I have a Bool flag saved in my database to say ignoreTimezone but I'm lost as to how to do this in Swift when reading back the timestamp from Firestore in UTC format and get it back to the original 9:00 pm.
All the Q&A's I've found are all about converting timezones etc. but not really on this example of ignoring one and keeping the date and time set to the timezone they were originally set for.
Thanks in advance for any help and/or suggestions.
Question updated as recommended
I have now incorporated the suggested code. So have a calendar extension:
extension Calendar {
func offsetFromMidnight(for date: Date) -> TimeInterval {
return date.timeIntervalSince(startOfDay(for: date))
}
}
Then I carry out the recommended steps.
Take an offset from midnight, in this case, the current Date():
let offsetSinceMidnight = UInt64(Calendar.current.offsetFromMidnight(for: Date()))
This value is then stored on the server.
I'm currently in Melbourne (Australia), so the date and time item used for testing is July 9 # 2:00pm.
When it is retrieved on the client end in a different timezone, I'm using the recommended code:
//Create a calendar for the target timezone
guard let chicagoTimeZone = TimeZone(identifier: "America/Chicago") else { fatalError() }
var chicagoCalendar = Calendar(identifier: .gregorian)
chicagoCalendar.timeZone = chicagoTimeZone
//Calculate midngiht in the target calendar
let chicagoMidnight = chicagoCalendar.startOfDay(for: Date())
//calculate the same time-of-day in the new timezone
let adjustedChicagoTime = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: chicagoMidnight)
The output is set to the correct time, 2:00pm in Chicago, but because of the differnent dates (Chicago is still July 8th), then the midnight timeinterval is being applied on the wrong date. So I get July 8 # 2:00pm.
I'm assuming I will also need to capture the original date components to apply the offsetSinceMidnight to a date in the newTimeZone that has matching date components??? Or is there a better approach to this?
Date objects store an instant in time, anywhere in the world. They don't capture the idea of a time-of-day regardless of time zone.
To do that I would suggest calculating an offsetFromMidnight value.
Edited to fix return value.
extension Calendar {
func offsetFromMidnight(for date: Date) -> TimeInterval {
return date.timeIntervalSince(startOfDay(for: date))
}
}
You'd call that function in the user's current calendar to get the seconds since midnight in the user's current time zone. Save that to your database. (You could round to a long integer with very little loss of precision.)
I happen to BE in the NYT time zone (EDT) so using that as the destination time zone won't work for me since it won't change anything. Instead, I'll show code to convert from my timezone to GMT:
//Run on user's local machine (in EDT in my case):
let offsetSinceMidnight = UInt64(Calendar.current.offsetFromMidnight(for: Date()))
//Save offset to FireStore
Then if you want that same time of day in a new timezone, you'd use code like this:
//Create a calendar for the target time zone (or the user's local time zone on the destination machine)
guard let gmt = TimeZone(abbreviation: "GMT") else { fatalError() }
var gmtCalendar = Calendar(identifier: .gregorian)
gmtCalendar.timeZone = gmt
//Read time offset from FireStore
let offsetFromNYC = Calendar.current.offsetFromMidnight(for: Date())
//Calculate midnight in target calendar
let gmtMidnight = gmtCalendar.startOfDay(for: Date())
//Calculate the same time-of-day in the GMT time zone
let gmtTimeToday = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: gmtMidnight)
print(gmtTimeToday)
Note that the above will give you the same hours/minutes/seconds as the offsetFromMidnight time.
Edit:
If your goal is to set an alarm to the next future time-of-day in the local time zone, you'd need to add logic to check if the computed date/time is in the past and adjust:
//Change adjustedChicagoTime to a var
var adjustedChicagoTime = Date(timeInterval: TimeInterval(offsetSinceMidnight), since: chicagoMidnight)
//If the alarm time is in the past, add a day to the date.
if adjustedChicagoTime < Date() {
adjustedChicagoTime = Calendar.current.date(byAdding: .day,
value: 1, to: adjustedChicagoTime, wrappingComponents: false)
}
Edit #2:
After a back-and-forth, it sounds like you sometimes want to save a date and time that's independent of time zone, like 9:30 AM on 10 July. If I create that date in EDT, and you view it in Melborne, it's ALWAYS 9:30 AM on 10 July.
Other times, you want to upload and download dates & times that honor time zones.
In order to easily do both, I would suggest saving 2 different string date/time fields to FireStore, one with a time zone, and one without. The one with timezone (or rather offset from GMT) would capture a moment in time around the world, and could be converted to a local time.
The one without time zone would describe a day/month/year/hours/minutes in local time.
You could generate/parse those strings in Swift using date formatters like this:
let baseFormatString = "YYYY-MM-dd'T'HH:mm"
let timeZoneFormatString = baseFormatString + "ZZZ"
let noTimeZoneFormatter = DateFormatter()
noTimeZoneFormatter.dateFormat = baseFormatString
let timeZoneFormatter = DateFormatter()
timeZoneFormatter.dateFormat = timeZoneFormatString
Note that by default a date formatter uses the system's time zone, so the "no time zone formatter" would assume the local time zone. If you use it to convert a date string to a date, it will assume the date is in the local time zone.

Get the current week number in month from Date

I am currently facing a weird issue. I am trying to find out in which week of a month a given Date instance lies.
My code is the following:
var calendar : Calendar {
var calendar = Calendar(identifier: .iso8601)
calendar.timeZone = .UTC
return calendar
}
func generateDate(year: Int, month: Int, day: Int) -> Date {
let dateComponents = DateComponents(year: year, month: month, day: day)
return calendar.date(from: dateComponents)!
}
print(calendar.component(.weekOfMonth, from: Date.generateDate(year: 2019, month: 12, day: 1))) // prints "0"
print(calendar.component(.weekOfMonth, from: Date.generateDate(year: 2020, month: 1, day: 1))) // prints "1"
generateDate simply generates a Date with the help of calendar. When I print the both statements I get 0 and 1 as the result. In my opinion this is wrong. I would assume I get the same value for both since both dates should be in the first week of their respective month value.
Another example would be the 2. Dec 2019, this should give the second week as well as the 6. January 2020 should also give me the second week.
Does anyone know what could be wrong here or where my mistake could be ?
This is due to the 1st day of the month falling on a sunday, try march 01, 2020 if you want confirmation.
The first week of the month is chosen by Swift according to their own standards as ISO standards do not suggest any specific implementations so the Swift team went with this. You can ask them in their forums what their reasoning behind this is Wikipedia link
The logic for .weekOfMonth seems to be that if the "first" week is less than half a week, that is 3 days, then it is considered to be week 0 and otherwise week 1. But note that this is dependent on what locale is being used, for a country like Canada that has Sunday as first day of week there is never a week 0 when running the below code. So when the first day of week is Monday .weekOfMonth will return a value between 0 and 5 but when it is Sunday the range is 1 to 6.
This can be seen running the following code in a playground
let calendar = Calendar.current
let year = 2019
print("First day of week: \(calendar.weekdaySymbols[calendar.firstWeekday - 1])")
for month in 1...12 {
print(calendar.monthSymbols[month - 1])
let first = calendar.date(from: DateComponents(year: year, month: month, day: 1))!
if let range = calendar.range(of: .day, in: .month, for: first) {
var currentWeek = -1
for day in range {
let date = calendar.date(from: DateComponents(year: year, month: month, day: day))!
let week = calendar.component(.weekOfMonth, from: date)
if week > currentWeek {
currentWeek = week
let dayOfWeek = calendar.component(.weekday, from: date)
print("Week# \(week), weekday \(calendar.weekdaySymbols[dayOfWeek - 1])")
}
}
}
}
It seems to me if you want whatever day it is on the 1st to be the first day of weekOfMonth = 1 then you need to write your own code for this

How to ask a user for an accurate date in a date format

So I have a program which I need to ask a user for a date and time. But that date has to either be in the future or the the very far past. And I have tried a UIDatePicker but have found out that a date picker isn't the best way to tend to this sort of problem because it requires a lot of scrolling, and the it doesn't load past a certain date for some reason. I was thinking of a UIPickerView that asks for Year, Month, Day, and Time. But I don't know how to do it because lets say that the month is September there is only 30 days so then how can I stop the user from selecting 31. So if anyone can help me that would be amazing. And if you have any questions or need for code just ask. Oh and by the way I would like it in a date format where I can compare it to other date.
Given the year and month, you can calculate the array of possible day values to be supplied to your picker like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: 1)
guard let date = calendar.date(from: components),
let range = calendar.range(of: .day, in: .month, for: date) else { return }
let days = Array(range)
I would like it in a date format where I can compare it to other date.
Once you have the year, month, and day, hour (converted to 24 hour value, from 0 to 23), you can build the Date object like so:
let calendar = Calendar.current
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = calendar.date(from: components) else { return }
That date can now be compared to any other Date object.

How to get date range with a month of calendar with DateToolsSwift?

So the purpose is to create a month view calendar with collection view. Today is the starting point, so I will get the first and last day of month in calendar from today.
func setDate(date: Date) {
let firstDate = date.start(of: .month)
let lastDate = date.end(of: .month)
}
Say today is 2017-07-19, instead of first date return 25th of June it return 30th of June and last date return 31st of July instead of 5th of August.
I could solve this by using the pure Swift Date Library, but I believe that this task could be solved by DateTools. I just don't know how.
By the way I'm using Swift 3.
DateTools Swift

Swift NSDate get current date but strip Year, Month, and Day

I am trying to get the current date using swift. I have that taken care of but it gets the year month and day and I really only want the current hour and minute. The reason being is that I am trying to later on compare a created date that only has an hour and minute. I am currently just using this
let currentDate = NSDate()
Thank you
let calendar = Calendar.autoupdatingCurrent
let components = calendar.dateComponents([.hour, .minute], from: Date())
let hour = components.hour
let minute = components.minute