// // CalendarDateViewCell.swift // VDS // // Created by Kanamarlapudi, Vasavi on 24/04/24. // import UIKit import VDSCoreTokens final class CalendarDateViewCell: UICollectionViewCell { ///Identifier for the Calendar Date Cell. static let identifier: String = String(describing: CalendarDateViewCell.self) //-------------------------------------------------- // 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.space1X $0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) $0.setContentHuggingPriority(.defaultHigh, for: .horizontal) $0.backgroundColor = .clear } }() private var numberLabel = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textAlignment = .center $0.textStyle = .bodySmall } override var isHighlighted: Bool { didSet{ if self.isHighlighted && hasText() && isDateEnabled() { self.contentView.layer.borderColor = activeBorderColorConfiguration.getColor(surface).cgColor self.contentView.layer.borderWidth = VDSFormControls.borderWidth self.contentView.layer.cornerRadius = VDSFormControls.borderRadius } else { self.contentView.layer.borderColor = nil self.contentView.layer.borderWidth = 0 self.contentView.layer.cornerRadius = 0 } } } private var isEnabled = false private lazy var shapeLayer = CAShapeLayer() private var surface: Surface = .light private let selectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryInverseOnlight, VDSColor.elementsPrimaryInverseOndark) private let selectedBackgroundColor = SurfaceColorConfiguration(VDSColor.backgroundPrimaryInverseLight, VDSColor.backgroundPrimaryInverseDark) private let selectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteGray65, VDSColor.paletteGray44) private let unselectedTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) private let unselectedCellIndicatorColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) private let disabledTextColorConfiguration = SurfaceColorConfiguration(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark) private let disabledBackgroundColor = SurfaceColorConfiguration(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark) private var activeBorderColorConfiguration = SurfaceColorConfiguration(VDSFormControlsColor.borderHoverOnlight , VDSFormControlsColor.borderHoverOndark) private let currentDate = Date() //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- override init(frame: CGRect) { super.init(frame: frame) setUp() } required init?(coder: NSCoder) { super.init(coder: coder) setUp() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- /// Configuring the cell with default setup. private func setUp() { isAccessibilityElement = false contentView.addSubview(containerView) containerView .pinTop() .pinBottom() .pinLeadingGreaterThanOrEqualTo() .pinTrailingLessThanOrEqualTo() .height(containerSize.height) .width(containerSize.width) .pinCenterX() // Number label containerView.addSubview(numberLabel) numberLabel.pinToSuperView() // Indicators containerView.addSubview(stackView) let topPos = containerSize.height * 0.7 stackView .pinTop(topPos) .pinBottom() .pinTopGreaterThanOrEqualTo() .pinTrailingLessThanOrEqualTo() .pinCenterX() } /// 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, displayDate: Date, hideDate: Bool, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } numberLabel.surface = surface numberLabel.text = text self.surface = surface // enable/disable cells based on min date, max date and active/inactive dates. updateLabel(with:surface, displayDate: displayDate, minDate: minDate, maxDate: maxDate, activeDates: activeDates, inactiveDates: inactiveDates) // handling inactive dates. if inactiveDates.count > 0 { for x in 0...inactiveDates.count-1 { if inactiveDates[x].monthInt == displayDate.monthInt && inactiveDates[x].yearInt == displayDate.yearInt { if let day:Int = Int(numberLabel.text), day == inactiveDates[x].dayInt { disableLabel(with: surface) } } } } // Set selected/unselected state text color, bg color, corner radius if cell is in enabled state. if isEnabled { if numberLabel.text == selectedDate.getDay() && selectedDate.monthInt == displayDate.monthInt && selectedDate.yearInt == displayDate.yearInt { numberLabel.textColor = selectedTextColorConfiguration.getColor(surface) layer.backgroundColor = selectedBackgroundColor.getColor(surface).cgColor layer.cornerRadius = VDSFormControls.borderRadius } else { numberLabel.textColor = unselectedTextColorConfiguration.getColor(surface) layer.backgroundColor = nil layer.cornerRadius = 0 } } // add indicators. if indicatorCount > 0 { for x in 0...indicators.count-1 { if numberLabel.text == indicators[x].date.getDay() { let color = numberLabel.text == selectedDate.getDay() ? selectedCellIndicatorColorConfiguration.getColor(surface) : unselectedCellIndicatorColorConfiguration.getColor(surface) addIndicator(with: color, surface: surface, clearFullCircle: x == 1, drawSemiCircle: x == 2) } } } // update text style for current date. if numberLabel.text == currentDate.getDay() && currentDate.monthInt == displayDate.monthInt && currentDate.yearInt == displayDate.yearInt { numberLabel.textStyle = hideDate ? .bodySmall : .boldBodySmall } else { numberLabel.textStyle = .bodySmall } } func hasText() -> Bool { return !numberLabel.text.isEmpty } // returns cell enabled state. func isDateEnabled() -> Bool { return isEnabled } func disableLabel(with surface: Surface) { isEnabled = false numberLabel.textColor = disabledTextColorConfiguration.getColor(surface) layer.backgroundColor = disabledBackgroundColor.getColor(surface).cgColor } func showActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { for x in 0...activeDates.count-1 { if activeDates[x].monthInt == displayDate.monthInt && activeDates[x].yearInt == displayDate.yearInt { if let day:Int = Int(numberLabel.text), day == activeDates[x].dayInt { isEnabled = true } } } } // handing active dates if exist, else enable numberLabel to display day. func handleActiveDates(with displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { if activeDates.count > 0 && inactiveDates.count == 0 { showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { isEnabled = true } } // enable all days if no active dates, handing active dates if exist. func enableAllDaysAndCheckActiveDates(with surface:Surface, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { if activeDates.count > 0 && inactiveDates.count == 0 { disableLabel(with: surface) showActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { isEnabled = true } } func minDateValidation(with surface:Surface, minDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with min date only. if let day = Int(numberLabel.text), day < minDate.dayInt { disableLabel(with: surface) } else { isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } func maxDateValidation(with surface:Surface, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with max date only. if let day = Int(numberLabel.text), day > maxDate.dayInt { disableLabel(with: surface) } else { isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } func minAndMaxDateValidation(with surface:Surface, minDate: Date, maxDate: Date, displayDate: Date, activeDates: [Date], inactiveDates: [Date]) { // validate days to enable/disable with min and max date. if let day = Int(numberLabel.text), day < minDate.dayInt || day > maxDate.dayInt { disableLabel(with: surface) } else { isEnabled = false handleActiveDates(with: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } // enable/disable cells based on min date, max date and active/inactive dates. func updateLabel(with surface: Surface, displayDate: Date, minDate: Date, maxDate: Date, activeDates: [Date], inactiveDates: [Date]) { if minDate.yearInt == displayDate.yearInt && !(maxDate.yearInt == displayDate.yearInt) { // min year and max year are different, and matched to min year. if minDate.monthInt == displayDate.monthInt { // min year and max year are different, and matched to min year and min month. minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } else if maxDate.yearInt == displayDate.yearInt && !(minDate.yearInt == displayDate.yearInt) { // min year and max year are different, and matched to max year. if maxDate.monthInt == displayDate.monthInt { // min year and max year are different, and matched to max year and max month. maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else { // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } else if minDate.yearInt == displayDate.yearInt && maxDate.yearInt == displayDate.yearInt { // min year and max year same if minDate.monthInt == displayDate.monthInt && maxDate.monthInt == displayDate.monthInt { // min year and max year same, when choose dates in same month. minAndMaxDateValidation(with: surface, minDate: minDate, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else if minDate.monthInt == displayDate.monthInt || maxDate.monthInt == displayDate.monthInt { // min year and max year same, and choose dates in different months. if minDate.monthInt == displayDate.monthInt { // min year and max year same, and matched to min month. minDateValidation(with: surface, minDate: minDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } else if (maxDate.monthInt == displayDate.monthInt) { // min year and max year same, and matched to max month. maxDateValidation(with: surface, maxDate: maxDate, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } else { // min year and max year same, and not matched to min or max month. // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } else { // min year and max year are different, and not matched to min or max year. // handing active dates - enable all days if no active dates. enableAllDaysAndCheckActiveDates(with: surface, displayDate: displayDate, activeDates: activeDates, inactiveDates: inactiveDates) } } func addIndicator(with color: UIColor, surface: Surface, clearFullCircle: Bool, drawSemiCircle: Bool) { // add indicator let indicatorView: View = View().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.backgroundColor = .clear $0.layer.borderWidth = 1.0 } indicatorView .pinLeading() .pinTrailing() .width(8) .height(8) .pinCenterY() stackView.addArrangedSubview(indicatorView) // update indicator indicatorView.backgroundColor = drawSemiCircle ? .clear : (clearFullCircle ? .clear : color) indicatorView.layer.borderColor = color.cgColor layoutIfNeeded() indicatorView.layer.cornerRadius = indicatorView.frame.size.height / 2.0 guard drawSemiCircle else { return } let center = CGPoint(x: indicatorView.frame.size.width/2, y: indicatorView.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 indicatorView.layer.sublayers?.contains(shapeLayer) ?? true else { return } indicatorView.layer.addSublayer(shapeLayer) } }