// // Calendar.swift // VDS // // Created by Kanamarlapudi, Vasavi on 19/04/24. // import Foundation import UIKit import VDSTokens import Combine /// A calendar is a monthly view that lets customers select a single date. @objc(VDSCalendar) open class CalendarBase: 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 //-------------------------------------------------- /// If set to true, the calendar will not have a border. open var hideContainerBorder: Bool = false { didSet { setNeedsUpdate() } } /// If set to true, the calendar will not have current date indication. open var hideCurrentDateIndicator: Bool = false { didSet { setNeedsUpdate() } } /// Enable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. /// All other dates will be inactive open var activeDates: [Date] = [] { didSet { setNeedsUpdate() } } /// Disable specific days. Pass an array of string value in date format e.g. ['07/21/2024', '07/24/2024', 07/28/2024']. /// All other dates will be active. open var inactiveDates: [Date] = [] { didSet { setNeedsUpdate() } } /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. open var minDate: Date = Date() { didSet { setNeedsUpdate() } } /// If provided, the calendar will allow a selection to be made up to this date. open var maxDate: Date = Date() { didSet { setNeedsUpdate() } } /// 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 = Date() { didSet { setNeedsUpdate() } } /// If provided, the calendar will be rendered with transparent background. open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. open var indicators: [CalendarIndicatorModel] = [] { didSet { setNeedsUpdate() } } /// A callback when the date changes. Passes parameters (selectedDate). public var onChangeSelectedDate: ((Date) -> Void)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- 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 private let footerHeight = 40.0 private let items = 35 private var selectedIndexPath : IndexPath? private var dates: [Date] = [] private var days: [String] = [] internal var containerView = View().with { $0.clipsToBounds = true } /// Collectionview to load calendar month view private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) collectionView.isScrollEnabled = false collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.delegate = self collectionView.dataSource = self collectionView.showsHorizontalScrollIndicator = false collectionView.showsVerticalScrollIndicator = false collectionView.backgroundColor = .clear collectionView.register(CalendarDateCollectionViewCell.self, forCellWithReuseIdentifier: CalendarDateCollectionViewCell.identifier) collectionView.register(CalendarHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CalendarHeaderReusableView.identifier) collectionView.register(CalendarFooterReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: CalendarFooterReusableView.identifier) return collectionView }() //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- internal var containerBorderColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight , VDSColor.elementsPrimaryOndark) internal var backgroundColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) //-------------------------------------------------- // 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() let spacing = CGFloat(VDSFormControls.spaceInset) //CGFloat(VDSLayout.space2X) // Calendar View containerView.addSubview(collectionView) collectionView .pinTop(VDSLayout.space4X) .pinBottom(VDSLayout.space4X) .pinLeading(spacing) .pinTrailing(spacing) let width = containerSize.width - (2 * spacing) collectionView.widthAnchor.constraint(equalToConstant: width).activate() let height = containerSize.height - (2 * VDSLayout.space4X) collectionView.heightAnchor.constraint(equalToConstant: height).activate() collectionView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).activate() } open override func updateView() { super.updateView() self.fetchDates(with: selectedDate) collectionView.reloadData() layer.backgroundColor = backgroundColorConfiguration.getColor(self).cgColor if hideContainerBorder { layer.borderColor = nil layer.borderWidth = 0 layer.cornerRadius = 0 } else { layer.borderColor = containerBorderColorConfiguration.getColor(self).cgColor layer.borderWidth = VDSFormControls.borderWidth layer.cornerRadius = VDSFormControls.borderRadius } } override open func layoutSubviews() { super.layoutSubviews() } open override func reset() { super.reset() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- func fetchDates(with date:Date) { days.removeAll() self.dates = selectedDate.calendarDisplayDays for date in dates { // code to be executed if date.monthInt != selectedDate.monthInt { days.append("") } else { days.append(getDay(with: date)) } } } func getDay(with date:Date) -> String { if #available(iOS 15.0, *) { return date.formatted(.dateTime.day()) } else { // Fallback on earlier versions let dateFormatter: DateFormatter = DateFormatter() dateFormatter.dateFormat = "d" let day: String = dateFormatter.string(from: date) return day } } } extension CalendarBase: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { //-------------------------------------------------- // MARK: - UICollectionView Delegate & Datasource //-------------------------------------------------- public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 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() } var indicatorCount = 0 if self.indicators.count > 0 { for x in (0...(self.indicators.count-1)) { if (self.days[indexPath.row] == self.getDay(with: self.indicators[x].date)) { indicatorCount += 1 } } } cell.update(with: surface, indicators: indicators, text: days[indexPath.row], indicatorCount: indicatorCount, selectedDate: selectedDate, hideDate: hideCurrentDateIndicator, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) if (self.days[indexPath.row] == self.getDay(with: selectedDate)) { selectedIndexPath = indexPath } return cell } public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == UICollectionView.elementKindSectionHeader { // Header guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarHeaderReusableView.identifier, for: indexPath) as? CalendarHeaderReusableView else { return UICollectionReusableView() } header.configure(with: surface) return header } else { // Footer if kind == UICollectionView.elementKindSectionFooter { guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CalendarFooterReusableView.identifier, for: indexPath) as? CalendarFooterReusableView else { return UICollectionReusableView() } footer.configure(with: surface, indicators: indicators) return footer } } return UICollectionReusableView() } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedItem = Calendar.current.date(byAdding: .day, value: 1, to: self.dates[indexPath.row])! onChangeSelectedDate?(selectedItem) selectedDate = self.dates[indexPath.row] var reloadIndexPaths = [indexPath] // If an cell is already selected, then it needs to be deselected. // Add its index path to the array of index paths to be reloaded. if let deselectIndexPath = selectedIndexPath { reloadIndexPaths.append(deselectIndexPath) } self.collectionView.reloadItems(at: reloadIndexPaths) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: headerHeight) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { return CGSize(width: collectionView.frame.size.width, height: footerHeight) } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return VDSLayout.space1X } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return VDSLayout.space1X } public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return cellItemSize } }