Hi to all of you. I need some help in creating a list of the days of a month in SwiftUI. I tried to have a look online but no success. Is there anything I can try to do to make this thing as simple as possible?
Thanks in advance!
Try this:
func getDaysSimple(for month: Date) -> [Date] {
//get the current Calendar for our calculations
let cal = Calendar.current
//get the days in the month as a range, e.g. 1..<32 for March
let monthRange = cal.range(of: .day, in: .month, for: month)!
//get first day of the month
let comps = cal.dateComponents([.year, .month], from: month)
//start with the first day
//building a date from just a year and a month gets us day 1
var date = cal.date(from: comps)!
//somewhere to store our output
var dates: [Date] = []
//loop thru the days of the month
for _ in monthRange {
//add to our output array...
dates.append(date)
//and increment the day
date = cal.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
Usage:
print(getDaysSimple(for: .now))
//running on 2022-03-02 returns the following result:
//[2022-03-01 08:00:00 +0000, 2022-03-02 08:00:00 +0000, 2022-03-03 08:00:00 +0000,
//2022-03-04 08:00:00 +0000, 2022-03-05 08:00:00 +0000, 2022-03-06 08:00:00 +0000,
//2022-03-07 08:00:00 +0000, 2022-03-08 08:00:00 +0000, 2022-03-09 08:00:00 +0000,
//2022-03-10 08:00:00 +0000, 2022-03-11 08:00:00 +0000, 2022-03-12 08:00:00 +0000,
//2022-03-13 08:00:00 +0000, 2022-03-14 07:00:00 +0000, 2022-03-15 07:00:00 +0000,
//2022-03-16 07:00:00 +0000, 2022-03-17 07:00:00 +0000, 2022-03-18 07:00:00 +0000,
//2022-03-19 07:00:00 +0000, 2022-03-20 07:00:00 +0000, 2022-03-21 07:00:00 +0000,
//2022-03-22 07:00:00 +0000, 2022-03-23 07:00:00 +0000, 2022-03-24 07:00:00 +0000,
//2022-03-25 07:00:00 +0000, 2022-03-26 07:00:00 +0000, 2022-03-27 07:00:00 +0000,
//2022-03-28 07:00:00 +0000, 2022-03-29 07:00:00 +0000, 2022-03-30 07:00:00 +0000,
//2022-03-31 07:00:00 +0000]
Note that the dates are listed for UTC+00:00
adjusted for your locale. I am currently in UTC-08:00
, so the start of the day for me is 08:00:00
at the beginning of March, but since Daylight Saving Time starts the second Sunday in March, it switches to 07:00:00
on the 14th since that time zone is UTC-07:00
.
With this array of Date
objects, you should be able to create a list in SwiftUI or whatever you want to do with it.
Thanks for your reply and help.
What I need to do is like the image below, I understand your func but how to implement it in such a view?
It’s not necessary to have that custom Row, I just need a navigation view with all the day cells for every month of the year…
This is what I made until now.
DateView File:
struct DateView: View {
static let dateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.setLocalizedDateFormatFromTemplate("MMMM yyyy")
return formatter
}()
@Binding var date : Date
var body: some View {
HStack {
Image(systemName: "chevron.left")
.padding()
.onTapGesture {
print("Month -1")
self.changeDateBy(-1)
}
Spacer()
Text("\(date, formatter: Self.dateFormat)")
Spacer()
Image(systemName: "chevron.right")
.padding()
.onTapGesture {
print("Month +1")
self.changeDateBy(1)
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.background(Color.white)
}
func changeDateBy(_ months: Int) {
if let date = Calendar.current.date(byAdding: .month, value: months, to: date) {
self.date = date
}
}
}
ContentView:
struct ContentView: View {
@State var currentDate = Date()
let calendar = Calendar.current
private var startDateOfMonth: String {
let components = Calendar.current.dateComponents([.year, .month], from: currentDate)
let startOfMonth = Calendar.current.date(from: components)!
return format(date: startOfMonth)
}
private var endDateOfMonth: String {
var components = Calendar.current.dateComponents([.year, .month], from: currentDate)
components.month = (components.month ?? 0) + 1
components.hour = (components.hour ?? 0) - 1
let endOfMonth = Calendar.current.date(from: components)!
return format(date: endOfMonth)
}
var body: some View {
NavigationView {
VStack {
DateView(date: $currentDate)
.padding()
Divider()
Spacer()
List {
}
Text(format(date: currentDate))
Divider()
Text(startDateOfMonth)
Text(endDateOfMonth)
Divider()
}
.navigationBarTitle("DAYS")
}
}
private func format(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
return dateFormatter.string(from: date)
}
}
Result in simulator is in the image below.
In the middle space, I need to have the list of days.
First, some convenience methods on Date
:
extension Date {
func startOfMonth() -> Date {
let cal = Calendar.current
let comps = cal.dateComponents([.year, .month], from: self)
return cal.date(from: comps)!
}
func endOfMonth() -> Date {
let cal = Calendar.current
let comps = cal.dateComponents([.year, .month], from: self)
let date = cal.date(from: comps)!
let lastDayOfMonth = cal.date(byAdding: DateComponents(month: 1, day: -1), to: date)!
return lastDayOfMonth
}
func getDaysOfMonth() -> [Date] {
//get the current Calendar for our calculations
let cal = Calendar.current
//get the days in the month as a range, e.g. 1..<32 for March
let monthRange = cal.range(of: .day, in: .month, for: self)!
//get first day of the month
let comps = cal.dateComponents([.year, .month], from: self)
//start with the first day
//building a date from just a year and a month gets us day 1
var date = cal.date(from: comps)!
//somewhere to store our output
var dates: [Date] = []
//loop thru the days of the month
for _ in monthRange {
//add to our output array...
dates.append(date)
//and increment the day
date = cal.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
}
Now for the main View
:
struct CalendricalView: View {
class ViewModel: ObservableObject {
@Published var currentDate: Date = .now
let calendar = Calendar.current
func previousMonth() {
currentDate = calendar.date(byAdding: .month, value: -1, to: currentDate) ?? currentDate
}
func nextMonth() {
currentDate = calendar.date(byAdding: .month, value: 1, to: currentDate) ?? currentDate
}
var startDateOfMonth: Date {
currentDate.startOfMonth()
}
var endDateOfMonth: Date {
currentDate.endOfMonth()
}
var monthDays: [Date] {
currentDate.getDaysOfMonth()
}
}
@StateObject var viewModel = ViewModel()
var abbreviatedDate: Date.FormatStyle {
Date.FormatStyle(date: .abbreviated, time: .omitted)
}
var header: some View {
VStack {
HStack {
Button {
viewModel.previousMonth()
} label: {
Image(systemName: "chevron.left")
}
.buttonStyle(.plain)
.padding()
Spacer()
Text(viewModel.currentDate, format: .dateTime.month(.wide).year())
Spacer()
Button {
viewModel.nextMonth()
} label: {
Image(systemName: "chevron.right")
}
.buttonStyle(.plain)
.padding()
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.background(Color.white)
Divider()
}
}
var footer: some View {
VStack {
Divider()
Text(viewModel.currentDate, format: abbreviatedDate)
Divider()
Text(viewModel.startDateOfMonth, format: abbreviatedDate)
Text(viewModel.endDateOfMonth, format: abbreviatedDate)
Divider()
}
}
var body: some View {
NavigationView {
VStack {
header
DateListView(dates: viewModel.monthDays)
footer
}
.navigationBarTitle("DAYS")
}
}
}
And the list View
:
struct DateListView: View {
let dates: [Date]
var body: some View {
List {
ForEach(dates, id: \.self) { day in
NavigationLink {
Text(day, format: .dateTime.day().month(.abbreviated))
} label: {
Text(day, format: .dateTime.day().month(.abbreviated))
}
}
}
.listStyle(.plain)
}
}
- There are numerous places here you could customize to your liking.
- Obviously the date format will be a little different since our
Locale
s are different. -
CalendricalView
could have been done with@State
forcurrentDate
and some methods on thatView
but I used a view model instead.
Ciao Alessandro,
Grazie per il tuo post! È stato divertente provare a completare questa sfida. Ho fatto questo:
Unfortunately, I could not beat the excellent @roosterboy to the answer, but it seems like my code gets closest to your design. I used your code (thanks for providing all of it!) as well as Patrick’s in order to get here.
Here’s my new DateRowView
:
//
// DateRowView.swift
// Dates App
//
// Created by Leone on 3/2/22.
//
import SwiftUI
struct DateRowView: View {
// Pass in a date
var sideDate: Date
// Return the three-letter date abbreviation, e.g. Monday -> Mon
var abbreviatedDay: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
// Returns like "Wed"
return formatter.string(from: sideDate)
}
var dayNumber: String {
let formatter = DateFormatter()
// Formats the Date as a string e.g. 20
formatter.dateFormat = "d"
// Returns like 5
return formatter.string(from: sideDate)
}
var body: some View {
VStack(alignment: .leading) {
// MARK: - Date Elements
// Holds the left date elements
HStack {
// Inner VStack holds the date's three-letter day abbreviation as well as number
VStack {
Text(abbreviatedDay)
.font(.caption)
.foregroundColor(.gray)
Text(dayNumber)
.foregroundColor(.orange)
}
// Create a small gray divider 50 pixels high
Divider()
.frame(maxHeight: 40)
}
.frame(width: 40)
// MARK: - Bottom Divider
// Create a Divider under each View
Divider()
}
.padding(.leading)
}
}
struct DateRowView_Previews: PreviewProvider {
static var previews: some View {
DateRowView(sideDate: Date())
}
}
Meanwhile, I updated the ContentView
code to read like this:
//
// ContentView.swift
// Dates App
//
// Created by Leone on 3/2/22.
//
import SwiftUI
struct ContentView: View {
@State var currentDate = Date()
let calendar = Calendar.current
private var startDateOfMonth: String {
let components = Calendar.current.dateComponents([.year, .month], from: currentDate)
let startOfMonth = Calendar.current.date(from: components)!
return format(date: startOfMonth)
}
private var endDateOfMonth: String {
var components = Calendar.current.dateComponents([.year, .month], from: currentDate)
components.month = (components.month ?? 0) + 1
components.hour = (components.hour ?? 0) - 1
let endOfMonth = Calendar.current.date(from: components)!
return format(date: endOfMonth)
}
// Get the list of dates for the current month
private var listOfDates: [Date] {
let month = Date()
//get the current Calendar for our calculations
let cal = Calendar.current
//get the days in the month as a range, e.g. 1..<32 for March
let monthRange = cal.range(of: .day, in: .month, for: month)!
//get first day of the month
let comps = cal.dateComponents([.year, .month], from: month)
//start with the first day
//building a date from just a year and a month gets us day 1
var date = cal.date(from: comps)!
//somewhere to store our output
var dates: [Date] = []
//loop thru the days of the month
for _ in monthRange {
//add to our output array...
dates.append(date)
//and increment the day
date = cal.date(byAdding: .day, value: 1, to: date)!
}
return dates
}
var body: some View {
NavigationView {
VStack {
DateView(date: $currentDate)
.padding()
Divider()
Spacer()
// MARK: - Date Cells
// Create a nice scroll view for the user
ScrollView {
// Use a LazyVStack in order to not unnecessarilly create elements for things off of screen
// This uses less memory
LazyVStack {
// Loop through each of the dates in the month
ForEach(listOfDates, id: \.self) { date in
NavigationLink {
// Navigate to a new page here
Text("Navigated to \(date)")
} label: {
// Create a DateRow for each date
DateRowView(sideDate: date)
}
}
}
}
Text(format(date: currentDate))
Divider()
Text(startDateOfMonth)
Text(endDateOfMonth)
Divider()
}
.navigationBarTitle("DAYS")
}
}
private func format(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
return dateFormatter.string(from: date)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Cari saluti,
Andrea
P.S. apologies if you don’t speak Italian, but I’m 99% sure you are, because of one screenshot marzo, and that beautiful Italian name
Hi guys,
first of all, thank you very much for all your suggestions, I really appreciate.
I try to “mix” both solutions, in Andrea’s one (Andrea are u Italian? ) when I change the month in my header, the list of days do not update itself but I will look at this later.
I think “calendar” in Swift needs a lot of teaching, since it’s something really important and hard to manage.
Because it always uses the same date for calculating listOfDates
:
let month = Date()
Yes, I made it update correctly using a viewModel.
I am trying now to calculate how many “working” days we have per single month.
Is there any way to “filter” festive days in calendar?
AFAIK there is no built-in way to know which days are holidays and which ones aren’t. Maybe you could do something with EventKit?
Or find a public API you can query to get the list of holidays? Something like Nager.Date, perhaps? For instance, this URL gets you a list of public holidays in Italy in 2022. You would obviously have to set up the data fetching and parsing yourself, but that shouldn’t be too hard. I have no idea regarding the accuracy of this API, though.
Just for giggles, here’s a quick thing I worked up to try out the Nager.Date API. It creates a simple List
of holidays for the user’s current Locale
and the current year. Hopefully you can extrapolate what you are looking for from this. Again note that I am relying on the accuracy of Nager.Date so no guarantees.
struct Holiday: Identifiable, Codable {
let date: Date
let localName: String
let name: String
let global: Bool
var id: String {
date.formatted(.iso8601)
}
}
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.calendar = Calendar(identifier: .iso8601)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()
}
@MainActor
class HolidayAPI: ObservableObject {
@Published var holidays: [Holiday] = []
func fetchHolidays(for locale: Locale, in year: Date) async {
guard let region = locale.regionCode else { return }
var apiURL = URL(string: "https://date.nager.at/api/v3/PublicHolidays")!
apiURL.appendPathComponent(year.formatted(.dateTime.year()))
apiURL.appendPathComponent(region)
do {
let (data, _) = try await URLSession.shared.data(from: apiURL)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.yyyyMMdd)
let result = try decoder.decode([Holiday].self, from: data)
//filter out non-global holidays
//global holidays are those observed by the entire country
holidays = result.filter(\.global)
} catch {
print(error)
}
}
}
struct Holidays: View {
@StateObject var api = HolidayAPI()
let locale = Locale.current
let year = Date.now
var body: some View {
VStack {
VStack {
Text("Holidays").font(.largeTitle).bold()
Text("for \(locale.identifier) in \(year.formatted(.dateTime.year()))")
}
List {
ForEach(api.holidays) { holiday in
HStack {
VStack(alignment: .leading) {
Text(holiday.name)
.font(.headline)
.bold()
Text(holiday.date, format: .dateTime.year().month(.abbreviated).day())
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
if holiday.date < .now {
Image(systemName: "checkmark.circle")
.foregroundColor(.green)
}
}
}
}
.task {
await api.fetchHolidays(for: locale, in: year)
}
}
}
}
Guys, thanks to all of you for your fantastic support.
I really appreciate it.
I think that what I try to do is way too much for my Swift understanding, I was “simply” trying to record my working hours day by day, but simple operations like understanding how many official working hours I have month by month, is not at my level.
Anyway, really thank you very very much!
Haha no I’m not! Pero parlo l’italiano
Hey you did a great job! Between your code and @roosterboy’s I already learned a lot. Keep practicing, watching the videos at CWC+, and come back to it, or the forum with questions!
With Patrick’s answer/ sample code to use the holiday’s API it should not be so hard to figure out either. You could see that there are X number of holidays per month, then you could simply subtract that number of days from the weekdays in the month to get your total number of working days in a month, then do the multiplication to determine how many hours you would have to work.
Cheers,
Andrew