From 450dd1aae8f6847e108c2da1174952e6e4def50d Mon Sep 17 00:00:00 2001 From: vasavk Date: Thu, 9 May 2024 09:43:13 +0530 Subject: [PATCH] Digital ACT-191 ONEAPP-7958 story: added action for next and previous on changing min / max dates --- VDS/Components/Calendar/Calendar.swift | 52 +++++++++-- .../CalendarDateCollectionViewCell.swift | 22 ++--- .../Calendar/CalendarHeaderView.swift | 87 ++++++++++--------- .../Calendar/CalendarReusableView.swift | 22 ----- VDS/Components/Calendar/Date+Extension.swift | 36 ++++++++ 5 files changed, 139 insertions(+), 80 deletions(-) diff --git a/VDS/Components/Calendar/Calendar.swift b/VDS/Components/Calendar/Calendar.swift index f57b45be..ce183fac 100644 --- a/VDS/Components/Calendar/Calendar.swift +++ b/VDS/Components/Calendar/Calendar.swift @@ -79,7 +79,8 @@ open class CalendarBase: View { private var selectedIndexPath : IndexPath? private var dates: [Date] = [] private var days: [String] = [] - + private var displayDate: Date = Date() + internal var containerView = View().with { $0.clipsToBounds = true } @@ -146,8 +147,14 @@ open class CalendarBase: View { open override func updateView() { super.updateView() - self.fetchDates(with: selectedDate) - collectionView.reloadData() + // range check between min & max dates + if (minDate <= maxDate) { + // Check if current date falls between min & max dates. + let fallsBetween = displayDate.isBetweeen(date: minDate, andDate: maxDate) + displayDate = fallsBetween ? displayDate : minDate + self.fetchDates(with: displayDate) + collectionView.reloadData() + } layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { layer.borderColor = nil @@ -171,12 +178,12 @@ open class CalendarBase: View { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - func fetchDates(with date:Date) { + func fetchDates(with aDate:Date) { days.removeAll() - self.dates = selectedDate.calendarDisplayDays + self.dates = aDate.calendarDisplayDays for date in dates { // code to be executed - if date.monthInt != selectedDate.monthInt { + if date.monthInt != aDate.monthInt { days.append("") } else { days.append(getDay(with: date)) @@ -216,7 +223,7 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI } } } - cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) + cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, displayDate: displayDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } @@ -227,7 +234,36 @@ extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UI guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { return UICollectionReusableView() } - header.configure(with: surface) + var nextEnabled = false + var prevEnabled = false + + // check the interval between min date, max date.. set enable/disable flag for next / previous buttons. + if ((displayDate.monthInt < maxDate.monthInt) && (displayDate.yearInt == maxDate.yearInt)) || (displayDate.yearInt < maxDate.yearInt) { + nextEnabled = true + } + if ((minDate.monthInt < displayDate.monthInt) && (minDate.yearInt == displayDate.yearInt)) || (minDate.yearInt < displayDate.yearInt) { + prevEnabled = true + } + + header.nextClicked = { [weak self] in + guard let self = self else { return } + let aDate = Calendar.current.date(byAdding: .month, value: 1, to:self.displayDate)! + if ((aDate.monthInt <= maxDate.monthInt) && (aDate.yearInt == maxDate.yearInt)) || (aDate.yearInt < maxDate.yearInt) { + displayDate = aDate + self.fetchDates(with: displayDate) + self.collectionView.reloadData() + } + } + header.previousClicked = { [weak self] in + guard let self = self else { return } + let aDate = Calendar.current.date(byAdding: .month, value: -1, to:self.displayDate)! + if ((minDate.monthInt <= aDate.monthInt) && (minDate.yearInt == aDate.yearInt)) || (minDate.yearInt < aDate.yearInt) { + displayDate = aDate + self.fetchDates(with: displayDate) + self.collectionView.reloadData() + } + } + header.update(with: surface, aDate: displayDate, nextEnabled: nextEnabled, previousEnabled: prevEnabled) return header } else { // Footer diff --git a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift index f73a1ae1..efc506d8 100644 --- a/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift +++ b/VDS/Components/Calendar/CalendarDateCollectionViewCell.swift @@ -94,12 +94,12 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { /// Updating UI based on selected date, modified indicators data along with surface /// Enable/disable cell based on min date, max date, active dates, inactive dates - func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { + func update(with surface: Surface, indicators: [CalendarBase.CalendarIndicatorModel], text: String, indicatorCount: Int, selectedDate: Date, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { numberLabel.surface = surface numberLabel.text = text stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - - // disabled cells based on min date, max date. + + // enable/disable cells based on min date, max date. if let day:Int = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { numberLabel.isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) @@ -121,16 +121,18 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // handling inactive dates if inactiveDates.count > 0 { for x in (0...(inactiveDates.count-1)) { - if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { - numberLabel.isEnabled = false - numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) - layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + if (activeDates[x].monthInt == displayDate.monthInt) && (activeDates[x].yearInt == displayDate.yearInt) { + if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { + numberLabel.isEnabled = false + numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) + layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor + } } } } // update text color, bg color, corner radius - if (numberLabel.text == self.getDay(with: selectedDate)) && numberLabel.isEnabled { + if (numberLabel.text == self.getDay(with: selectedDate)) && (selectedDate.monthInt == displayDate.monthInt) && (selectedDate.yearInt == displayDate.yearInt) && numberLabel.isEnabled { numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius @@ -142,8 +144,6 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { // add indicators if indicatorCount > 0 { -// let width = (indicatorCount * 8) + (Int(VDSLayout.space1X) * (indicatorCount - 1)) -// stackView.widthAnchor.constraint(equalToConstant: CGFloat(width)).activate() for x in (0...(indicators.count-1)) { if (self.numberLabel.text == self.getDay(with: indicators[x].date)) { let color = (numberLabel.text == self.getDay(with: selectedDate)) ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) @@ -153,7 +153,7 @@ final class CalendarDateCollectionViewCell: UICollectionViewCell { } // update text style for current date - if (numberLabel.text == self.getDay(with: currentDate)) { + if (numberLabel.text == self.getDay(with: currentDate)) && (currentDate.monthInt == displayDate.monthInt) { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall diff --git a/VDS/Components/Calendar/CalendarHeaderView.swift b/VDS/Components/Calendar/CalendarHeaderView.swift index 53facda5..cdb08a18 100644 --- a/VDS/Components/Calendar/CalendarHeaderView.swift +++ b/VDS/Components/Calendar/CalendarHeaderView.swift @@ -10,25 +10,19 @@ import UIKit import VDSTokens /// Header view to display month and year along with days of week -open class CalendarHeaderView: View { - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - required public init() { - super.init(frame: .zero) - } +class CalendarHeaderReusableView: UICollectionReusableView { - public override init(frame: CGRect) { - super.init(frame: .zero) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - } + ///Identifier for the Calendar Header Reusable View + static let identifier: String = String(describing: CalendarHeaderReusableView.self) //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- + /// A callback when the next button clicked + public var nextClicked: (() -> (Void))? + + /// A callback when the previous button clicked + public var previousClicked: (() -> (Void))? //-------------------------------------------------- // MARK: - Private Properties @@ -64,6 +58,9 @@ open class CalendarHeaderView: View { return collectionView }() + private var surface: Surface = .light + private var displayDate: Date = Date() + internal var previousMonthView = View() internal var nextMonthView = View() let viewSize = 40.0 @@ -84,7 +81,7 @@ open class CalendarHeaderView: View { $0.size = .small } - internal var monthYearLabel = Label().with { + internal var headerTitle = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center $0.numberOfLines = 1 @@ -94,17 +91,23 @@ open class CalendarHeaderView: View { } internal let daysOfWeek = Date.capitalizedFirstLettersOfWeekdays - internal let monthYearLabelTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) - + internal let headerTitleTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Initializers //-------------------------------------------------- - open override func initialSetup() { - super.initialSetup() + override init(frame: CGRect) { + super.init(frame: frame) + setUp() } - open override func setup() { - super.setup() + required init?(coder: NSCoder) { + super.init(coder: coder) + setUp() + } + + /// Configuring the cell with default setup + private func setUp() { isAccessibilityElement = false // stackview @@ -114,7 +117,7 @@ open class CalendarHeaderView: View { .pinBottom(VDSLayout.space1X) .pinLeading() .pinTrailing() - .height(containerSize.height) + .height(containerSize.height - VDSLayout.space1X) .width(containerSize.width) stackView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() @@ -128,43 +131,49 @@ open class CalendarHeaderView: View { previousMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() previousMonthView.addSubview(previousButton) previousButton.pinCenterY().pinCenterX() - + previousButton.onClick = { _ in self.previousButtonClick() } + // month year label - topHeaderView.addArrangedSubview(monthYearLabel) + topHeaderView.addArrangedSubview(headerTitle) // next button topHeaderView.addArrangedSubview(nextMonthView) nextMonthView.widthAnchor.constraint(equalToConstant: viewSize).activate() nextMonthView.addSubview(nextButton) nextButton.pinCenterY().pinCenterX() - + nextButton.onClick = { _ in self.nextButtonClick() } + // days Collection View stackView.addArrangedSubview(daysCollectionView) topHeaderView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true daysCollectionView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } - open override func updateView() { - super.updateView() - monthYearLabel.surface = surface + /// Updating UI based on next/previous clicks along with surface. + /// Updating UI to enable/disable the next & previous buttons, updating header title + func update(with surface: Surface, aDate: Date, nextEnabled: Bool, previousEnabled: Bool) { + self.surface = surface + headerTitle.surface = surface previousButton.surface = surface nextButton.surface = surface + nextButton.isEnabled = nextEnabled + previousButton.isEnabled = previousEnabled daysCollectionView.reloadData() - monthYearLabel.text = "May 2024" - monthYearLabel.textColor = monthYearLabelTextColorConfiguration.getColor(surface) + let labelText = aDate.getMonthName(date: aDate) + " \(aDate.yearInt)" + headerTitle.text = labelText + headerTitle.textColor = headerTitleTextColorConfiguration.getColor(surface) } - override open func layoutSubviews() { - super.layoutSubviews() + func nextButtonClick() { + nextClicked?() } - - open override func reset() { - super.reset() - monthYearLabel.textStyle = .boldBodySmall + + func previousButtonClick() { + previousClicked?() } } -extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +extension CalendarHeaderReusableView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return daysOfWeek.count @@ -172,7 +181,7 @@ extension CalendarHeaderView: UICollectionViewDelegate, UICollectionViewDataSour public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: collectionViewCell.identifier, for: indexPath) as? collectionViewCell else { return UICollectionViewCell() } - cell.updateTitle(text: daysOfWeek[indexPath.row], surface: surface) + cell.updateTitle(text: daysOfWeek[indexPath.row], surface: self.surface) return cell } diff --git a/VDS/Components/Calendar/CalendarReusableView.swift b/VDS/Components/Calendar/CalendarReusableView.swift index ebb2d269..4a695ab0 100644 --- a/VDS/Components/Calendar/CalendarReusableView.swift +++ b/VDS/Components/Calendar/CalendarReusableView.swift @@ -8,28 +8,6 @@ import UIKit import VDSTokens -/// Custom header view -class CalendarHeaderReusableView: UICollectionReusableView { - - ///Identifier for the Calendar Header Reusable View - static let identifier: String = String(describing: CalendarHeaderReusableView.self) - - private lazy var headerView = CalendarHeaderView() - - override init(frame: CGRect) { - super.init(frame: frame) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(with surface: Surface) { - headerView.surface = surface - addSubview(headerView) - } -} - /// Custom footer view class CalendarFooterReusableView: UICollectionReusableView { diff --git a/VDS/Components/Calendar/Date+Extension.swift b/VDS/Components/Calendar/Date+Extension.swift index 13e80479..2a016ecb 100644 --- a/VDS/Components/Calendar/Date+Extension.swift +++ b/VDS/Components/Calendar/Date+Extension.swift @@ -8,7 +8,10 @@ import Foundation extension Date { + static var firstDayOfWeek = Calendar.current.firstWeekday + + /// Capitalizes the first letter of the day of the week static var capitalizedFirstLettersOfWeekdays: [String] { let calendar = Calendar.current let weekdays = calendar.shortWeekdaySymbols @@ -19,6 +22,18 @@ extension Date { } } + /// Returns all month names + static var fullMonthNames: [String] { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + + return (1...12).compactMap { month in + dateFormatter.setLocalizedDateFormatFromTemplate("MMMM") + let date = Calendar.current.date(from: DateComponents(year: 2000, month: month, day: 1)) + return date.map { dateFormatter.string(from: $0) } + } + } + var startOfMonth: Date { Calendar.current.dateInterval(of: .month, for: self)!.start } @@ -28,6 +43,7 @@ extension Date { return Calendar.current.date(byAdding: .day, value: -1, to: lastDay)! } + /// Get the number of days of the month var numberOfDaysInMonth: Int { Calendar.current.component(.day, from: endOfMonth) } @@ -41,6 +57,7 @@ extension Date { return Calendar.current.date(byAdding: .day, value: -numberFromPreviousMonth, to: startOfMonth)! } + /// Get the days of the month to display var calendarDisplayDays: [Date] { var days: [Date] = [] // Start with days from the previous month to fill the grid @@ -58,11 +75,30 @@ extension Date { return days } + /// Returns the year value of the given date + var yearInt: Int { + Calendar.current.component(.year, from: self) + } + + /// Returns the month value of the given date var monthInt: Int { Calendar.current.component(.month, from: self) } + /// Returns the day value of the given date var dayInt: Int { Calendar.current.component(.day, from: self) } + + /// Check if the date falls between the given dates + func isBetweeen(date date1: Date, andDate date2: Date) -> Bool { + return date1.compare(self) == self.compare(date2) + } + + /// Returns the month name of the given date + func getMonthName(date: Date) -> String { + let names = Date.fullMonthNames + return names[date.monthInt - 1] + } + }