I am currently making a scheduling feature for my application. I have a view where a user is able to select what days of the week they will be available for. Now, I am looking for a way to generate an array of dates for the given weekdays for the upcoming month.
In other words, if the user selects that they will be available on Monday, the function needs to return all dates that satisfy the given predicament for the month ahead (4 date objects).
Here's what I have tried but it doesn't seem to work:
var masterSchedule = [WorkDay(weekDay: 1, startTime: 8, endTime: 16, busyHours: []), WorkDay(weekDay: 4, startTime: 8, endTime: 16, busyHours: [])]
func getDates() {
for workDay in masterSchedule {
var components = DateComponents()
components.weekday = workDay.weekDay
components.year = 2021
components.month = 8
let date = Calendar.current.date(from: components) ?? Date()
availableDays.append(date)
}
}
For some reason, the output returns 2 same dates of 2021-07-31 23:00:00 +0000.
Can someone help me?
Here's an example that prints all the Mondays in October
import UIKit
import SwiftUI
import PlaygroundSupport
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateStyle = .short
let firstOfOctober = formatter.date(from: "10/1/2021")!
if let interval = calendar.dateInterval(of: .month, for: firstOfOctober) {
let mondays = DateComponents(weekday: 2)
calendar.enumerateDates(startingAfter: firstOfOctober,
matching: mondays, matchingPolicy: .previousTimePreservingSmallerComponents) {
date, exactMatch, stopLooking in
if let date = date {
if date < interval.end {
print(formatter.string(from: date))
} else {
stopLooking = true
}
}
}
}
I’m trying to create a Date extension static function that accepts two parameters: (month: Int, year: Int) and returns -> Date.
The month that is returned would be its .startOfMonth:
var startOfMonth: Date {
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.year, .month], from: self)
return calendar.date(from: components)!
}
I know how to retrieve a month in the future by adding n-months to the current:
func addMonths(_ numMonths: Int) -> Date {
let cal = NSCalendar.current
return cal.date(byAdding: .month, value: numMonths, to: self)!
}
And I suppose this could be used in a roundabout way to determine what I’m looking for, by first determining how many months from the current is the month I’m interested in getting a Date returned for. But I would much prefer if I could retrieve it without needing to go through this step, especially if there’s a possibility that I’m not actually sure if the month is in fact in the future.
You can use DateComponents initializer to compose a new date just passing the month and year components. Just make sure to pass a calendar as well otherwise the result would be nil. Maybe something like:
extension Date {
static func startOfMonth(for month: Int, of year: Int, using calendar: Calendar = .current) -> Date? {
DateComponents(calendar: calendar, year: year, month: month).date
}
}
Date.startOfMonth(for: 2, of: 2021) // "Feb 1, 2021 at 12:00 AM"
I'm building a calendar view that I want to be agnostic of the year, just list all the possible dates that can occur in a month. I.e. to show the maximum number of days in a calendar, like February 29th. From this answer, I know there are other calendar systems that also have leap days, so I'm curious how I might be able to tell if a calendar has a leap day, regardless of the calendar system or year. Any help would be greatly appreciated! Here is what I have currently to get the number of days in a month:
func days(in month: Int) -> Int {
let components = DateComponents(month: month+1, day: -1)
let lastDay = Calendar.current.date(from: components)!
return Calendar.current.dateComponents([.day], from: lastDay).day!+1
}
This works great, but is based on the current year, which may not be a leap year.
I've seen isLeapMonth but this doesn't seem to work to query in the same way as .day.
edit/update:
You can get the next leap year for the current calendar and check the maximum number of days in month, maybe something like this:
extension Date {
var year: Int { Calendar.current.component(.year, from: self) }
var isLeapYear: Bool { Calendar.current.range(of: .day, in: .year, for: self)!.count == 366 }
// find the leap year
static var leapYear: Int {
var year = Date().year
while DateComponents(calendar: .current, year: year).date?.isLeapYear == false {
year += 1
}
return year
}
}
Date.leapYear // 2020
func maximumNumberOfDays(in month: Int) -> Int? {
Calendar.current.range(of: .day,
in: .month,
for: DateComponents(calendar: .current,
year: Date.leapYear,
month: month).date!)?.count
}
maximumNumberOfDays(in: 2) // 29
I currently use the following code to return an array of dates for every single day between two dates, including today's date and the last date itself. This works great.
However, how would I go about modifying what I'm already doing in
order to do the same exact thing otherwise, but instead return an
array of the date of every Friday between the dates? For example, if the function was called on Wed Oct 23rd 2019 to return every Friday until November 10th, the first date would be Fri the 25th, Nov 1st, and then Nov 8th.
How would I do the same thing as above but for the 1st of every month? If I called the function on Wed Oct 23rd 2019 to return the first of every month until December 16th. The array should have Nov 1st and Dec 1st in it.
func dates(for date: String) -> [String] {
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
// add the formatted date to the array
dates.append( Formatter.date.string(from: date))
// increment the date by one day
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
You just need to add a weekday parameter to your method and check if the weekday of the date inside the loop before adding it to your array:
extension Formatter {
static let date = DateFormatter()
}
func dates(for date: String, weekday: Int? = nil) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
// first get the endDate
guard var endDate = Formatter.date.date(from: date) else { return [] }
// for calendrical calculations you should use noon time
endDate = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)!
// lets get todays noon time to start
var date = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
var dates: [String] = []
// while date less than or equal to end date
while date <= endDate {
if weekday == nil {
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
} else if let weekday = weekday, Calendar.current.component(.weekday, from: date) == weekday {
// add the formatted date to the array
dates.append(Formatter.date.string(from: date))
date = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: date)!
} else {
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
}
return dates
}
dates(for: "2019-12-25") // ["2019-10-23", "2019-10-24", "2019-10-25", "2019-10-26", "2019-10-27", "2019-10-28", "2019-10-29", "2019-10-30", "2019-10-31", "2019-11-01", "2019-11-02", "2019-11-03", "2019-11-04", "2019-11-05", "2019-11-06", "2019-11-07", "2019-11-08", "2019-11-09", "2019-11-10", "2019-11-11", "2019-11-12", "2019-11-13", "2019-11-14", "2019-11-15", "2019-11-16", "2019-11-17", "2019-11-18", "2019-11-19", "2019-11-20", "2019-11-21", "2019-11-22", "2019-11-23", "2019-11-24", "2019-11-25", "2019-11-26", "2019-11-27", "2019-11-28", "2019-11-29", "2019-11-30", "2019-12-01", "2019-12-02", "2019-12-03", "2019-12-04", "2019-12-05", "2019-12-06", "2019-12-07", "2019-12-08", "2019-12-09", "2019-12-10", "2019-12-11", "2019-12-12", "2019-12-13", "2019-12-14", "2019-12-15", "2019-12-16", "2019-12-17", "2019-12-18", "2019-12-19", "2019-12-20", "2019-12-21", "2019-12-22", "2019-12-23", "2019-12-24", "2019-12-25"]
dates(for: "2019-12-25", weekday: 6) // ["2019-10-25", "2019-11-01", "2019-11-08", "2019-11-15", "2019-11-22", "2019-11-29", "2019-12-06", "2019-12-13", "2019-12-20"]
func firstDayOfTheMonth(until date: String) -> [String] {
Formatter.date.locale = Locale(identifier: "en_US_POSIX")
Formatter.date.dateFormat = "yyyy-MM-dd"
guard let endDate = Formatter.date.date(from: date) else { return [] }
var date = Date()
var dates: [String] = []
// while date less than or equal to end date
while let firstDayOfTheMonth = Calendar.current.nextDate(after: date, matching: .init(day: 1), matchingPolicy: .nextTime), firstDayOfTheMonth <= endDate {
dates.append(Formatter.date.string(from: firstDayOfTheMonth))
date = firstDayOfTheMonth
}
return dates
}
firstDayOfTheMonth(until: "2019-12-25") // ["2019-11-01", "2019-12-01"]
You can get the weekday for any day of the week with:
let weekDayIndex = Calendar.current.component(.weekday, from: Date())
Friday happens to be day 5. You can get the name of any day with:
print(Calendar.current.weekdaySymbols[weekDayIndex])
So just loop over all of your dates and filter out anything where the weekday is not 5 and you have your answer:
func fridays(in dates: [Date]) {
dates.filter { Calendar.current.component(.weekday, from: $0) == 5 }
}
I have a UIDatePicker which is available for days from today to the next week ( from Date() to Date().day + 7 ).
Suppose that today is 30th day of the month, so the available days to choose are 30th day of this month and first to 6th day of next month.
In this case, I need to change the datePicker month to next month as the user change day from 30 to 1.
But the problem is:
"When I change the day to an invalid one, the .change method give me the least valid day."
Example:
Assumptions:
Today = 28 July.
Choosable dates = 28 July to 3 August.
What I need:
User can not choose the days before 28 July and not after 3 August.
When user wants to change day to 1, 2 or 3 August, the datePicker itself change the month to August and make 1, 2 and 3 available to
be chosen by user.
Main Problem
When user change the day to 1, 2 or 3, I can not get the 1, 2 or 3! and it returns me "28"!
Just set maximumDate property of your UIDatePicker to desired date.
func setupDatePicker(){
var currDateComponents = getDateComponents(fromDate: Date())
var maxDateComponents = currDateComponents
if let date = currDateComponents?.day{
maxDateComponents?.day = date + 7
}
self.datePicker?.maximumDate = getDate(fromDateComponents: maxDateComponents)
}
func getDateComponents(fromDate date: Date?)-> DateComponents?{
let calendar = Calendar(identifier: .gregorian)
var dateComponents: DateComponents? = nil
if(date != nil){
dateComponents = calendar.dateComponents([.day, .month, .year], from: date!)
}
return dateComponents
}
func getDate(fromDateComponents dateComponents:DateComponents?)-> Date?{
let calendar = Calendar(identifier: .gregorian)
if let componentsBasedDate = calendar.date(from: dateComponents!) {
return componentsBasedDate
}
return nil
}
Call this function in viewDidLoad to setup calendar for setting maximum and minimum date:
fileprivate func setupCalendar() {
datePicker.minimumDate = Calendar.current.date(byAdding: .day, value: 0, to: ServerTime.sharedInstance.nowTime)
datePicker.maximumDate = Calendar.current.date(byAdding: .day, value: +7, to: ServerTime.sharedInstance.nowTime)
}
for calculate days between shown date and current date I've used this method:
extension Date {
//Calculate days between two day objects
func daysBetween(_ date: Date) -> Int {
let calendar = NSCalendar.current
// Replace the hour (time) of both dates with 00:00
let date1 = calendar.startOfDay(for: date)
let date2 = calendar.startOfDay(for: self)
let components = calendar.dateComponents([.day], from: date1, to: date2)
return components.day ?? 0 // This will return the number of day(s) between dates
}
}}
In fact, it is impossible to get date before they chose. And according to unavailability of days in next month before changing the month, I can not use UIDatePicker to handle it.
So Finally I implemented my custom UIPickerView.
as #holex said.
Thank you all for answering.