vds_ios/VDS/Components/DatePicker/DatePicker.swift
Matt Bruce 6f17c1b0da refactored to use new calendar
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2024-05-14 11:23:00 -05:00

344 lines
12 KiB
Swift

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
}
}
}
}