diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index d04570b2..a8befef3 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -150,7 +150,7 @@ EAC58C142BED0DEC00BA39FA /* Number.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C132BED0DEC00BA39FA /* Number.swift */; }; EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C152BED0E0300BA39FA /* InlineAction.swift */; }; EAC58C182BED0E2300BA39FA /* SecurityCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C172BED0E2300BA39FA /* SecurityCode.swift */; }; - EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C202BF127FE00BA39FA /* DatePicker.swift */; }; + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C222BF2824200BA39FA /* DatePicker.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -356,7 +356,7 @@ EAC58C132BED0DEC00BA39FA /* Number.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Number.swift; sourceTree = ""; }; EAC58C152BED0E0300BA39FA /* InlineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAction.swift; sourceTree = ""; }; EAC58C172BED0E2300BA39FA /* SecurityCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityCode.swift; sourceTree = ""; }; - EAC58C202BF127FE00BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DatePicker.swift; path = ../../../../../../../../../Downloads/DatePicker.swift; sourceTree = ""; }; + EAC58C222BF2824200BA39FA /* DatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePicker.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -915,7 +915,7 @@ EAC58C1F2BF127F000BA39FA /* DatePicker */ = { isa = PBXGroup; children = ( - EAC58C202BF127FE00BA39FA /* DatePicker.swift */, + EAC58C222BF2824200BA39FA /* DatePicker.swift */, ); path = DatePicker; sourceTree = ""; @@ -1168,7 +1168,7 @@ 1832AC572BA0791D008AE476 /* BreadcrumbCellItem.swift in Sources */, EA3361C328902D960071C351 /* Toggle.swift in Sources */, EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */, - EAC58C212BF127FE00BA39FA /* DatePicker.swift in Sources */, + EAC58C232BF2824200BA39FA /* DatePicker.swift in Sources */, EA89201328B568D8006B9984 /* RadioBoxItem.swift in Sources */, 71FC86E42B9841AC00700965 /* PaginationFlowLayout.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift new file mode 100644 index 00000000..9d049613 --- /dev/null +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -0,0 +1,183 @@ +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 { + //-------------------------------------------------- + // 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 let picker = UIDatePicker() + + 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 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 { + 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" + } + } + } + + 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() + + // setup the calendar + picker.datePickerMode = .date + picker.preferredDatePickerStyle = .inline + picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) + picker.isHidden = true + + // tap gesture + fieldStackView + .publisher(for: UITapGestureRecognizer()) + .sink { [weak self] _ in + 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 + } + + open override func getBottomContainer() -> UIView { + bottomStackView.addArrangedSubview(bottomContainerStackView) + bottomStackView.addArrangedSubview(picker) + return bottomStackView + } + + /// 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 { + picker.date = selectedDate + formatDate(selectedDate) + } + + selectedDateLabel.surface = surface + selectedDateLabel.isEnabled = isEnabled + calendarIcon.color = iconColorConfiguration.getColor(self) + + //set the width constraints + let minWidth = containerSize.width + let maxwidth = frame.size.width + if let width, width > minWidth && width < maxwidth { + widthConstraint?.constant = width + } else { + widthConstraint?.constant = maxwidth >= minWidth ? maxwidth : minWidth + } + } + + /// 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() { + picker.isHidden = !picker.isHidden + bottomContainerStackView.isHidden = !bottomContainerStackView.isHidden + } + + @objc private func dateChanged(_ sender: UIDatePicker) { + selectedDate = sender.date + sendActions(for: .valueChanged) + togglePicker() + } +}