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, DatePickerViewControllerDelegate, 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() fieldStackView.isAccessibilityElement = true fieldStackView.accessibilityLabel = "Date Picker" fieldStackView.accessibilityHint = "Double Tap to open" // 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) } open override func updateAccessibility() { super.updateAccessibility() let label = "Date Picker, \(isReadOnly ? ", read only" : "")" if let errorText, showError { fieldStackView.accessibilityLabel = "\(label) ,error, \(errorText)" } else { fieldStackView.accessibilityLabel = label } fieldStackView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to open." fieldStackView.accessibilityValue = value } /// 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 = DatePickerViewController(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: DatePickerViewController, date: Date) { selectedDate = date controller.dismiss(animated: true) sendActions(for: .valueChanged) } public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none } }