diff --git a/VDS/Classes/ClearPopoverViewController.swift b/VDS/Classes/ClearPopoverViewController.swift index 33531a75..6f9bcb67 100644 --- a/VDS/Classes/ClearPopoverViewController.swift +++ b/VDS/Classes/ClearPopoverViewController.swift @@ -19,6 +19,10 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo /// Popover presentation controller of the popover private var popOver: UIPopoverPresentationController! + open var maxWidth: CGFloat? + + open var sourceRect: CGRect? + open var spacing: CGFloat = 0 /** A controller that manages the popover. @@ -35,6 +39,7 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo self.contentView = contentView self.spacing = spacing self.arrow = arrow + self.sourceRect = sourceRect super.init(nibName: nil, bundle: nil) setupPopover(sourceView, sourceRect, barButtonItem) } @@ -65,7 +70,6 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo popOver.popoverBackgroundViewClass = ClearPopoverBackgroundView.self popOver.sourceView = sourceView popOver.popoverLayoutMargins = .zero - if let sourceRect = sourceRect { popOver.sourceRect = sourceRect } @@ -85,11 +89,8 @@ open class ClearPopoverViewController: UIViewController, UIPopoverPresentationCo private func updatePopoverPosition() { guard let popoverPresentationController = popoverPresentationController else { return } - if let sourceView = popoverPresentationController.sourceView { - popoverPresentationController.sourceRect = .init(x: sourceView.bounds.origin.x, - y: sourceView.bounds.origin.y, - width: sourceView.bounds.width, - height: sourceView.bounds.height + spacing) + if let sourceView = popoverPresentationController.sourceView, let sourceRect { + popoverPresentationController.sourceRect = sourceRect } } diff --git a/VDS/Components/DatePicker/DatePicker.swift b/VDS/Components/DatePicker/DatePicker.swift index 0e985176..3ab83be1 100644 --- a/VDS/Components/DatePicker/DatePicker.swift +++ b/VDS/Components/DatePicker/DatePicker.swift @@ -117,10 +117,11 @@ open class DatePicker: EntryFieldBase { 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 } + popoverController?.dismiss(animated: true){ [weak self] in guard let self else { return } } + hidePopoverView() } .store(in: &subscribers) @@ -163,7 +164,7 @@ open class DatePicker: EntryFieldBase { selectedDateLabel.text = formatter.string(from: date) } - internal var popoverController: UIViewController? + internal var popoverController: ClearPopoverViewController? func didSelect(_ date: Date) { selectedDate = date @@ -175,34 +176,179 @@ open class DatePicker: EntryFieldBase { } } - 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) - } + private var overlayView = UIView().with { + $0.backgroundColor = .clear; + $0.isHidden = true + } + private var popoverView: UIView! + private var popoverVisible = false + private var outsideTapGesture: UITapGestureRecognizer? + private var outsidePanGesture: UIPanGestureRecognizer? + +// internal func togglePicker() { +// 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: .any, +// sourceView: containerView, +// sourceRect: .init(x: 0, y: 0, width: 320, height: 45), +// spacing: VDSLayout.space1X) +// popoverController?.maxWidth = 320 +// if let viewController = UIApplication.topViewController(), let popoverController { +// viewController.present(popoverController, +// animated: true, +// completion: nil) +// } +// } +} + +extension DatePicker { + + private func togglePicker() { + guard let viewController = UIApplication.topViewController(), let parentView = viewController.view else { return } + + if popoverVisible { + hidePopoverView() + } else { + 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 } + selectedDate = control.selectedDate + sendActions(for: .valueChanged) + UIAccessibility.post(notification: .layoutChanged, argument: containerView) + hidePopoverView() + } + + outsideTapGesture = UITapGestureRecognizer() + outsidePanGesture = UIPanGestureRecognizer() + + overlayView.publisher(for: outsideTapGesture!).sink { [weak self] _ in + guard let self else { return } + hidePopoverView() + }.store(in: &subscribers) + parentView.publisher(for: outsidePanGesture!).sink { [weak self] _ in + guard let self else { return } + hidePopoverView() + }.store(in: &subscribers) + + overlayView.frame = parentView.bounds + overlayView.isHidden = false + + parentView.addSubview(overlayView) + + popoverView = UIView() + popoverView.backgroundColor = .white + popoverView.layer.cornerRadius = 10 + popoverView.layer.shadowColor = UIColor.black.cgColor + popoverView.layer.shadowOpacity = 0.2 + popoverView.layer.shadowOffset = CGSize(width: 0, height: 5) + popoverView.layer.shadowRadius = 10 + popoverView.isHidden = true + popoverView.addSubview(calendar) + calendar.pinToSuperView() + popoverView.translatesAutoresizingMaskIntoConstraints = false + parentView.addSubview(popoverView) + + popoverView.width(calendar.frame.width) + popoverView.height(calendar.frame.height) + + let spacing: CGFloat = 4 + let (popoverX, popoverY) = calculatePopoverPosition(relativeTo: containerView, in: parentView, size: calendar.frame.size, with: spacing) + popoverView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: popoverX).isActive = true + popoverView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: popoverY).isActive = true + + parentView.layoutIfNeeded() + popoverView.alpha = 0 + popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + popoverView.isHidden = false + popoverVisible = true - 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) + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: .curveEaseOut, animations: { [weak self] in + guard let self else { return } + popoverView.alpha = 1 + popoverView.transform = CGAffineTransform.identity + parentView.layoutIfNeeded() + }) } } + + private func hidePopoverView() { + overlayView.isHidden = true + overlayView.removeFromSuperview() + + outsideTapGesture = nil + outsidePanGesture = nil + UIView.animate(withDuration: 0.2, animations: { + self.popoverView.alpha = 0 + self.popoverView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9) + }) { _ in + self.popoverView.isHidden = true + self.popoverView.removeFromSuperview() + self.popoverVisible = false + } + } + + private func calculatePopoverPosition(relativeTo sourceView: UIView, in parentView: UIView, size: CGSize, with spacing: CGFloat) -> (CGFloat, CGFloat) { + let sourceFrameInParent = sourceView.convert(sourceView.bounds, to: parentView) + let parentBounds = parentView.bounds + let popoverWidth: CGFloat = size.width + let popoverHeight: CGFloat = size.height + + var popoverX: CGFloat = 0 + var popoverY: CGFloat = 0 + + // Calculate horizontal position + if sourceFrameInParent.width < popoverWidth { + if sourceFrameInParent.midX - popoverWidth / 2 < 0 { + // Align to left + popoverX = sourceFrameInParent.minX + } else if sourceFrameInParent.midX + popoverWidth / 2 > parentBounds.width { + // Align to right + popoverX = sourceFrameInParent.maxX - popoverWidth + } else { + // Center on source view + popoverX = sourceFrameInParent.midX - popoverWidth / 2 + } + } else { + popoverX = sourceFrameInParent.midX - popoverWidth / 2 + } + + // Calculate vertical position + if sourceFrameInParent.origin.y > parentBounds.height / 2 { + // Show above + popoverY = sourceFrameInParent.minY - popoverHeight - spacing + } else { + // Show below + popoverY = sourceFrameInParent.maxY + spacing + } + + // Ensure the popover is within the parent's bounds + popoverX = max(0, min(popoverX, parentBounds.width - popoverWidth)) + + return (popoverX, popoverY) + } }