From 408effa9f0c9ea7fd8e9c27e035f3be32e50246d Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 2 May 2024 17:51:56 +0530 Subject: [PATCH] Digital ACT-191 ONEAPP-7016 story: displaying days for calendar month view --- VDS.xcodeproj/project.pbxproj | 10 +- VDS/Components/Calendar/Calendar.swift | 49 ++++- .../CalendarDateCollectionViewCell.swift | 193 +++++++++++++++--- ...endView.swift => CalendarFooterView.swift} | 8 +- .../Calendar/CalendarHeaderView.swift | 12 +- .../Calendar/CalendarReusableView.swift | 2 +- VDS/Components/Calendar/Date+Extension.swift | 45 ++++ 7 files changed, 271 insertions(+), 48 deletions(-) rename VDS/Components/Calendar/{CalendarLegendView.swift => CalendarFooterView.swift} (97%) diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index e75dc4c1..e34e7772 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */; }; 1808BEC02BA456B700129230 /* CarouselScrollbarChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */; }; 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */; }; + 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */; }; 18450CF12BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */; }; 1855EC662BAABF2A002ACAC2 /* BreadcrumbItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */; }; 186B2A8A2B88DA7F001AB71F /* TextAreaChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */; }; @@ -23,7 +24,6 @@ 18B463A42BBD3C46005C4528 /* DropdownOptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */; }; 18BDEE822B75316E00452358 /* ButtonIconChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */; }; 18FEA1AD2BDD137500A56439 /* CalendarIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */; }; - 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */; }; 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */; }; 18FEA1B52BE0E63600A56439 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */; }; 18FEA1B72BE0EBFE00A56439 /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */; }; @@ -207,6 +207,7 @@ 1808BEBB2BA41C3200129230 /* CarouselScrollbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselScrollbar.swift; sourceTree = ""; }; 1808BEBF2BA456B700129230 /* CarouselScrollbarChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = CarouselScrollbarChangeLog.txt; sourceTree = ""; }; 1832AC562BA0791D008AE476 /* BreadcrumbCellItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbCellItem.swift; sourceTree = ""; }; + 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFooterView.swift; sourceTree = ""; }; 18450CF02BA1B19C009FDF2A /* BreadcrumbsChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BreadcrumbsChangeLog.txt; sourceTree = ""; }; 1855EC652BAABF2A002ACAC2 /* BreadcrumbItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadcrumbItemModel.swift; sourceTree = ""; }; 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextAreaChangeLog.txt; sourceTree = ""; }; @@ -220,7 +221,6 @@ 18B463A32BBD3C46005C4528 /* DropdownOptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownOptionModel.swift; sourceTree = ""; }; 18BDEE812B75316E00452358 /* ButtonIconChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonIconChangeLog.txt; sourceTree = ""; }; 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarIndicatorModel.swift; sourceTree = ""; }; - 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarLegendView.swift; sourceTree = ""; }; 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarReusableView.swift; sourceTree = ""; }; 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; @@ -436,11 +436,11 @@ children = ( 18A3F1292BD9298900498E4A /* Calendar.swift */, 18A3F1312BD944E800498E4A /* CalendarDateCollectionViewCell.swift */, + 18408EDD2BE32C9900E8646B /* CalendarFooterView.swift */, + 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, 18FEA1AC2BDD137500A56439 /* CalendarIndicatorModel.swift */, - 18FEA1B02BE0B69300A56439 /* CalendarLegendView.swift */, 18FEA1B22BE0BC8700A56439 /* CalendarReusableView.swift */, 18FEA1B42BE0E63600A56439 /* Date+Extension.swift */, - 18FEA1B62BE0EBFE00A56439 /* CalendarHeaderView.swift */, 18FEA1B82BE1301700A56439 /* CalendarChangeLog.txt */, ); path = Calendar; @@ -1216,6 +1216,7 @@ EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */, EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */, EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */, + 18408EDE2BE32C9900E8646B /* CalendarFooterView.swift in Sources */, EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */, EAA7456C2AB23E2000C1841F /* TooltipModel.swift in Sources */, EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, @@ -1229,7 +1230,6 @@ EA0B180A2AA78F9000F2D0CD /* UIEdgeInsets.swift in Sources */, EA985C1D296CD13600F2FF2E /* BundleManager.swift in Sources */, EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */, - 18FEA1B12BE0B69300A56439 /* CalendarLegendView.swift in Sources */, EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */, 18FEA1B32BE0BC8700A56439 /* CalendarReusableView.swift in Sources */, EAF7F0AB289B13FD00B287F5 /* TextStyleLabelAttribute.swift in Sources */, diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index 3fa9de4a..faa1bed4 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -54,7 +54,18 @@ open class CalendarBase: View { /// If provided, this is the date that will show as selected by the Calendar. /// If no value is provided, the current date will be used. If null is provided, no date will be selected. - open var selectedDate: Date? + open var selectedDate: Date? { + get { return _selectedDate } + set { + if let newValue { + _selectedDate = newValue + } else { + _selectedDate = Date() + } + //update views what needed + setNeedsUpdate() + } + } /// If provided, the calendar will be rendered with transparent background. open var transparentBackground: Bool = false @@ -71,6 +82,9 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + internal var _selectedDate: Date = Date() + private var dates: [Date] = [] + private var days: [String] = [] internal var containerSize: CGSize { CGSize(width: 328, height: 376) } //width:320/328 private let cellItemSize = CGSize(width: 40, height: 40) private let headerHeight = 88.0 @@ -150,6 +164,7 @@ open class CalendarBase: View { open override func updateView() { super.updateView() + self.fetchDates(with: selectedDate ?? Date()) collectionView.reloadData() containerView.layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor } @@ -161,6 +176,30 @@ open class CalendarBase: View { open override func reset() { super.reset() } + + func fetchDates(with date:Date) { + days.removeAll() + if let dates = selectedDate?.calendarDisplayDays { + self.dates = dates + for day in dates { + // code to be executed + if day.monthInt != selectedDate?.monthInt { + days.append("") + } else { + if #available(iOS 15.0, *) { + days.append(day.formatted(.dateTime.day())) + } else { + // Fallback on earlier versions + let dateFormatter: DateFormatter = DateFormatter() + dateFormatter.dateFormat = "d" + let dayStr: String = dateFormatter.string(from: day) + days.append(dayStr) + } + } + } +// print("days: \(days)") + } + } } extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { @@ -168,11 +207,12 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI // MARK: - UICollectionView Delegate & Datasource //-------------------------------------------------- public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - items + days.count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CalendarDateCollectionViewCell.identifier, for: indexPath) as? CalendarDateCollectionViewCell else { return UICollectionViewCell() } + cell.configure(with: indicators, text: days[indexPath.row]) return cell } @@ -197,6 +237,11 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI return UICollectionReusableView() } + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let date = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! + print("selected day: \(days[indexPath.row]), date: \(date)") + } + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: headerHeight) } diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index 156cce04..f1c50f15 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -9,46 +9,179 @@ import Foundation import UIKit import VDSTokens -///This is customised view for Calendar cell item +/// Calendar collection view cell final class CalendarDateCollectionViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell static let identifier: String = String(describing: CalendarDateCollectionViewCell.self) - //-------------------------------------------------- - // MARK: - Private Properties - //-------------------------------------------------- -// internal var stackView: UIStackView = { -// return UIStackView().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.axis = .horizontal -// $0.distribution = .fill -// $0.alignment = .center -// $0.spacing = VDSLayout.space2X -// $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) -// $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) -// } -// }() -// -// private lazy var selectionBackgroundView = View().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.clipsToBounds = true -// $0.backgroundColor = .systemRed -// } -// -// private lazy var numberLabel = Label().with { -// $0.translatesAutoresizingMaskIntoConstraints = false -// $0.textAlignment = .center -// // $0.font -// // $0.textColor -// } - override init(frame:CGRect) { + private lazy var dateView = DateView() + + override init(frame: CGRect) { super.init(frame: frame) - contentView.backgroundColor = .link } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + func configure(with indicators: [CalendarBase.CalendarIndicatorModel], text: String) { + addSubview(dateView) + dateView.dateIndicators = indicators + dateView.numberLabel.text = text + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + +/// Date view to show Date number and indicator if applies +private class DateView : View { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var dateIndicators: [CalendarBase.CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } + + open var numberLabel = Label().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.textAlignment = .center + $0.textStyle = .bodySmall //isCurrentDate: .boldBodySmall + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + internal var containerSize: CGSize { CGSize(width: 40, height: 40) } + + internal var containerView = View().with { + $0.clipsToBounds = true + } + + private lazy var stackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.alignment = .center + $0.spacing = VDSLayout.space2X + $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } + }() + +// private lazy var indicatorsView = View().with { +// $0.translatesAutoresizingMaskIntoConstraints = false +// $0.clipsToBounds = true +// $0.backgroundColor = .systemRed +// } + + private var legendIndicator: View = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.layer.borderWidth = 1.0 + } + + private lazy var shapeLayer = CAShapeLayer() + + private let indicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) + + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func initialSetup() { + super.initialSetup() + } + + open override func setup() { + super.setup() + isAccessibilityElement = false + + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(containerSize.width) + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + + // Number label + containerView.addSubview(numberLabel) + numberLabel.pinToSuperView() + + // Indicators + if dateIndicators.count > 0 { + containerView.addSubview(stackView) + let topPos = containerSize.height * 0.6 + stackView.pinTop(topPos).pinBottom().pinLeading().pinTrailing().pinCenterY() + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() + let width = (dateIndicators.count * 8) + (8 * (dateIndicators.count - 1)) + stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() + } + } + + open override func updateView() { + super.updateView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + } + + open override func reset() { + super.reset() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + func configure(with indicators: [CalendarBase.CalendarIndicatorModel]) { + //TO DO: handling indicators - in progress + //show indicator + legendIndicator.pinLeading().pinTrailing().width(8).height(8).pinCenterY() + stackView.addArrangedSubview(legendIndicator) + updateIndicator(with: VDSColor.elementsSecondaryOnlight, surface: surface, clearFullCircle: false, drawSemiCircle: false) + } + + func updateIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool){ + legendIndicator.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) + legendIndicator.layer.borderColor = indicatorColorConfiguration.getColor(surface).cgColor + + self.layoutIfNeeded() + + legendIndicator.layer.cornerRadius = legendIndicator.frame.size.height / 2.0 + + guard drawSemiCircle else { return } + + let center = CGPoint(x: legendIndicator.frame.size.width/2, y: legendIndicator.frame.size.height/2) + let path = UIBezierPath() + path.move(to: center) + path.addArc(withCenter: center, radius: center.x, startAngle: 2 * .pi, endAngle: .pi, clockwise: true) + path.close() + shapeLayer.path = path.cgPath + shapeLayer.fillColor = color.cgColor + + guard legendIndicator.layer.sublayers?.contains(shapeLayer) ?? true else { return } + legendIndicator.layer.addSublayer(shapeLayer) + } } diff --git a/VDS/Components/Calendar/CalendarLegendView.swift b/VDS/Components/Calendar/CalendarFooterView.swift similarity index 97% rename from VDS/Components/Calendar/CalendarLegendView.swift rename to VDS/Components/Calendar/CalendarFooterView.swift index 9345483d..84d76cab 100644 --- a/VDS/Components/Calendar/CalendarLegendView.swift +++ b/VDS/Components/Calendar/CalendarFooterView.swift @@ -1,5 +1,5 @@ // -// CalendarLegendView.swift +// CalendarFooterView.swift // VDS // // Created by Kanamarlapudi, Vasavi on 29/04/24. @@ -10,8 +10,8 @@ import UIKit import VDSTokens import Combine -/// Legend view to show array of indicators as calendar footer view. -open class CalendarLegendView: View { +/// Footer view to show indicators data. +open class CalendarFooterView: View { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -103,7 +103,7 @@ open class CalendarLegendView: View { } } -extension CalendarLegendView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +extension CalendarFooterView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return items.count diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index bba9805c..44f5462f 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -43,7 +43,7 @@ open class CalendarHeaderView: View { $0.backgroundColor = .clear } - private lazy var monthHeaderStackView = UIStackView().with { + private lazy var monthYearHeaderStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.distribution = .fill $0.spacing = VDSLayout.space1X @@ -137,19 +137,19 @@ open class CalendarHeaderView: View { monthView.heightAnchor.constraint(equalToConstant: 40).isActive = true // month Header stack view - monthView.addSubview(monthHeaderStackView) - monthHeaderStackView.pinToSuperView() + monthView.addSubview(monthYearHeaderStackView) + monthYearHeaderStackView.pinToSuperView() // previous button - monthHeaderStackView.addArrangedSubview(previousMonthView) + monthYearHeaderStackView.addArrangedSubview(previousMonthView) previousMonthView.widthAnchor.constraint(equalToConstant: 40).activate() previousMonthView.addSubview(previousButton) // month year label - monthHeaderStackView.addArrangedSubview(monthYearLabel) + monthYearHeaderStackView.addArrangedSubview(monthYearLabel) // next button - monthHeaderStackView.addArrangedSubview(nextMonthView) + monthYearHeaderStackView.addArrangedSubview(nextMonthView) nextMonthView.widthAnchor.constraint(equalToConstant: 40).activate() nextMonthView.addSubview(nextButton) diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index b348cc30..33bedfe2 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -39,7 +39,7 @@ class CalendarFooterReusableView: UICollectionReusableView { ///Identifier for the Calendar Footer Reusable View static let identifier: String = String(describing: CalendarFooterReusableView.self) - private lazy var footerView = CalendarLegendView() + private lazy var footerView = CalendarFooterView() override init(frame: CGRect) { super.init(frame: frame) diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 60036ee2..07284507 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -8,6 +8,7 @@ import Foundation extension Date { + static var firstDayOfWeek = Calendar.current.firstWeekday static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current let weekdays = calendar.shortWeekdaySymbols @@ -17,4 +18,48 @@ extension Date { return String(firstLetter).capitalized } } + + var startOfMonth: Date { + Calendar.current.dateInterval(of: .month, for: self)!.start + } + + var endOfMonth: Date { + let lastDay = Calendar.current.dateInterval(of: .month, for: self)!.end + return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! + } + + var numberOfDaysInMonth: Int { + Calendar.current.component(.day, from: endOfMonth) + } + + var firstWeekDayBeforeStart: Date { + let startOfMonthWeekday = Calendar.current.component(.weekday, from: startOfMonth) + var numberFromPreviousMonth = startOfMonthWeekday - Self.firstDayOfWeek + if numberFromPreviousMonth < 0 { + numberFromPreviousMonth += 7 // Adjust to a 0-6 range if negative + } + return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! + } + + var calendarDisplayDays: [Date] { + var days: [Date] = [] + // Start with days from the previous month to fill the grid + let firstDisplayDay = firstWeekDayBeforeStart + var day = firstDisplayDay + while day < startOfMonth { + days.append(day) + day = Calendar.current.date(byAdding: .day, value: 1, to: day)! + } + // Add days of the current month + for dayOffset in 0..