import Foundation import UIKit import VDSTokens import Combine /// A dropdown select is an expandable menu of predefined options that allows a customer to make a single selection. @objc(VDSDatePicker) open class DatePicker: EntryFieldBase, DatePickerPopoverViewControllerDelegate, UIPopoverPresentationControllerDelegate { //-------------------------------------------------- // 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 //-------------------------------------------------- /// A callback when the selected option changes. Passes parameters (option). open var onDateSelected: ((Date, DatePicker) -> Void)? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var minWidthDefault = 186.0 internal var bottomStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill $0.alignment = .top $0.spacing = VDSLayout.space2X } }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var calendarIcon = Icon().with { $0.name = .calendar $0.size = .medium } open var selectedDate: Date? { didSet { setNeedsUpdate() } } open var calendarModel: CalendarModel = .init() { didSet { setNeedsUpdate() } } open override var value: String? { get { selectedDateLabel.text } set { } } open var selectedDateLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textAlignment = .left $0.textStyle = .bodyLarge $0.lineBreakMode = .byCharWrapping } public enum DateFormat: String, CaseIterable, CustomStringConvertible { case shortNumeric case longAlphabetic case mediumNumeric case consiseNumeric public var format: String { switch self { case .shortNumeric: "MM/dd/yy" case .longAlphabetic: "MMMM d, yyyy" case .mediumNumeric: "MM/dd/yyyy" case .consiseNumeric: "M/d/yyyy" } } public var description: String { return format } } open var dateFormat: DateFormat = .shortNumeric { didSet{ setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal override var containerSize: CGSize { CGSize(width: minWidthDefault, height: 44) } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() accessibilityLabel = "Dropdown Select" // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // tap gesture fieldStackView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in guard let self else { return } if self.isEnabled && !self.isReadOnly { self.togglePicker() } } .store(in: &subscribers) } open override func getFieldContainer() -> UIView { // stackview for controls in EntryFieldBase.controlContainerView let controlStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.spacing = VDSLayout.space3X } controlStackView.addArrangedSubview(calendarIcon) controlStackView.addArrangedSubview(selectedDateLabel) return controlStackView } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() if let selectedDate { formatDate(selectedDate) } selectedDateLabel.surface = surface selectedDateLabel.isEnabled = isEnabled calendarIcon.color = iconColorConfiguration.getColor(self) } /// Resets to default settings. open override func reset() { super.reset() selectedDateLabel.textStyle = .bodyLarge } internal func formatDate(_ date: Date) { let formatter = DateFormatter() formatter.dateFormat = dateFormat.format selectedDateLabel.text = formatter.string(from: date) } internal func togglePicker() { //let calendarVC = DatePickerPopoverViewController(calendar: Calendar(identifier: .gregorian), delegate: self) let calendarVC = DatePickerPopoverViewController(calendarModel, delegate: self) calendarVC.modalPresentationStyle = .popover calendarVC.selectedDate = selectedDate ?? Date() if let popoverController = calendarVC.popoverPresentationController { popoverController.delegate = self popoverController.sourceView = containerView popoverController.sourceRect = containerView.bounds popoverController.permittedArrowDirections = .up } if let viewController = UIApplication.topViewController() { viewController.present(calendarVC, animated: true, completion: nil) } } internal func didSelectDate(_ controller: DatePickerPopoverViewController, date: Date) { selectedDate = date controller.dismiss(animated: true) sendActions(for: .valueChanged) } public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none } } extension DatePicker { public struct CalendarModel { public let surface: Surface /// If set to true, the calendar will not have a border. public let hideContainerBorder: Bool /// If set to true, the calendar will not have current date indication. public let hideCurrentDateIndicator: Bool /// 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. public let activeDates: [Date] /// 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. public let inactiveDates: [Date] /// If provided, the calendar will allow a selection to be made from this date forward. Defaults to today. public let minDate: Date /// If provided, the calendar will allow a selection to be made up to this date. public let maxDate: Date /// 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. public let selectedDate: Date /// Array of ``CalendarIndicatorModel`` you are wanting to show on legend. public let indicators: [CalendarBase.CalendarIndicatorModel] public init(surface: Surface = .light, hideContainerBorder: Bool = false, hideCurrentDateIndicator: Bool = false, selectedDate: Date = Date(), activeDates: [Date] = [], inactiveDates: [Date] = [], minDate: Date = Date().startOfMonth, maxDate: Date = Date().endOfMonth, indicators: [CalendarBase.CalendarIndicatorModel] = []) { self.surface = surface self.hideContainerBorder = hideContainerBorder self.hideCurrentDateIndicator = hideCurrentDateIndicator self.selectedDate = selectedDate self.activeDates = activeDates self.inactiveDates = inactiveDates self.minDate = minDate self.maxDate = maxDate self.indicators = indicators } } } protocol DatePickerPopoverViewControllerDelegate: NSObject { func didSelectDate(_ controller: DatePicker.DatePickerPopoverViewController, date: Date) } //class DatePickerPopoverViewController: UIViewController { // // private let picker = UIDatePicker() // weak var delegate: DatePickerPopoverViewControllerDelegate? // // init(calendar: Calendar, delegate: DatePickerPopoverViewControllerDelegate?) { // self.delegate = delegate // super.init(nibName: nil, bundle: nil) // picker.datePickerMode = .date // picker.preferredDatePickerStyle = .inline // picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) // } // // var selectedDate: Date = Date() { // didSet { // picker.date = selectedDate // } // } // // required init?(coder: NSCoder) { // fatalError("init(coder:) has not been implemented") // } // // override func viewDidLoad() { // super.viewDidLoad() // let v = UIView().with { // $0.translatesAutoresizingMaskIntoConstraints = false // $0.backgroundColor = .white // $0.width(constant: 250) // $0.height(constant: 350) // } // view.addSubview(v) // v.pinTop(25).pinLeading(15).pinTrailing(15).pinBottom(15) // // view.backgroundColor = .blue // // preferredContentSize = CGSize(width: 300, height: 400) // Adjust as needed // } // // @objc private func dateChanged(_ sender: UIDatePicker) { // delegate?.didSelectDate(self, date: sender.date) // } //} extension DatePicker { class DatePickerPopoverViewController: UIViewController { private var padding: CGFloat = 15 private var topPadding: CGFloat { 10 + padding } private var calendarModel: CalendarModel private let picker = CalendarBase() weak var delegate: DatePickerPopoverViewControllerDelegate? init(_ calendarModel: CalendarModel, delegate: DatePickerPopoverViewControllerDelegate?) { self.delegate = delegate self.calendarModel = calendarModel super.init(nibName: nil, bundle: nil) self.picker.onChangeSelectedDate = { [weak self] date in guard let self else { return } self.delegate?.didSelectDate(self, date: date) } } var selectedDate: Date = Date() { didSet { picker.selectedDate = selectedDate } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.addSubview(picker) picker.surface = calendarModel.surface picker.hideContainerBorder = calendarModel.hideContainerBorder picker.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator picker.activeDates = calendarModel.activeDates picker.inactiveDates = calendarModel.inactiveDates picker.selectedDate = calendarModel.selectedDate picker.indicators = calendarModel.indicators picker.minDate = calendarModel.minDate picker.maxDate = calendarModel.maxDate picker.pinToSuperView(.init(top: topPadding, left: padding, bottom: padding, right: padding)) view.backgroundColor = picker.backgroundColor } override var preferredContentSize: CGSize { get { var size = picker.containerSize size.height += 40 size.width += 30 return size } set { super.preferredContentSize = newValue } } } }