import Foundation import UIKit import VDSCoreTokens 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 { //-------------------------------------------------- // 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() // setting color config selectedDateLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() // tap gesture containerView .publisher(for: UITapGestureRecognizer()) .sink { [weak self] _ in guard let self else { return } if self.isEnabled && !self.isReadOnly { self.togglePicker() } } .store(in: &subscribers) NotificationCenter.default .publisher(for: UIDevice.orientationDidChangeNotification).sink { [weak self] _ in guard let self, let popoverController else { return } popoverController.dismiss(animated: true){ [weak self] in guard let self else { return } } } .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 var popoverController: UIViewController? func didSelect(_ date: Date) { selectedDate = date sendActions(for: .valueChanged) UIAccessibility.post(notification: .layoutChanged, argument: self.containerView) popoverController?.dismiss(animated: true){ [weak self] in guard let self else { return } popoverController = nil } } internal func togglePicker() { let calendar = CalendarBase() calendar.activeDates = calendarModel.activeDates calendar.hideContainerBorder = calendarModel.hideContainerBorder calendar.hideCurrentDateIndicator = calendarModel.hideCurrentDateIndicator calendar.inactiveDates = calendarModel.inactiveDates calendar.indicators = calendarModel.indicators calendar.maxDate = calendarModel.maxDate calendar.minDate = calendarModel.minDate calendar.surface = calendarModel.surface calendar.setNeedsLayout() calendar.layoutIfNeeded() calendar.onChange = { [weak self] control in guard let self else { return } didSelect(control.selectedDate) } popoverController = ClearPopoverViewController(contentView: calendar, arrow: .up, sourceView: containerView, sourceRect: containerView.bounds, spacing: VDSLayout.space1X) if let viewController = UIApplication.topViewController(), let popoverController { viewController.present(popoverController, animated: true, completion: nil) } } }