diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 7c9565f7..bf8fbc94 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -174,6 +174,7 @@ EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; }; EAF2F4762C231EAA007BFEDC /* AccessibilityActionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */; }; EAF2F4782C249D72007BFEDC /* AccessibilityUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */; }; + EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */; }; EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* CheckboxItem.swift */; }; EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; }; EAF7F0A0289AB7EC00B287F5 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09F289AB7EC00B287F5 /* View.swift */; }; @@ -402,6 +403,7 @@ EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = ""; }; EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityActionElement.swift; sourceTree = ""; }; EAF2F4772C249D72007BFEDC /* AccessibilityUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityUpdatable.swift; sourceTree = ""; }; + EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; EAF7F0932899861000B287F5 /* CheckboxItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckboxItem.swift; sourceTree = ""; }; EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = ""; }; EAF7F09F289AB7EC00B287F5 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; @@ -750,6 +752,7 @@ EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */, EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, EAF2F4752C231EAA007BFEDC /* AccessibilityActionElement.swift */, + EAF2F4882C2A1075007BFEDC /* AlertViewController.swift */, ); path = Classes; sourceTree = ""; @@ -1310,6 +1313,7 @@ EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EAC58C162BED0E0300BA39FA /* InlineAction.swift in Sources */, + EAF2F4892C2A1075007BFEDC /* AlertViewController.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, diff --git a/VDS/Classes/AlertViewController.swift b/VDS/Classes/AlertViewController.swift new file mode 100644 index 00000000..71a1c6d9 --- /dev/null +++ b/VDS/Classes/AlertViewController.swift @@ -0,0 +1,101 @@ +// +// AlertViewController.swift +// VDS +// +// Created by Matt Bruce on 6/24/24. +// + +import Foundation +import UIKit +import Combine +import VDSCoreTokens + +open class AlertViewController: UIViewController, Surfaceable { + + /// Set of Subscribers for any Publishers for this Control. + open var subscribers = Set() + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var onClickSubscriber: AnyCancellable? { + willSet { + if let onClickSubscriber { + onClickSubscriber.cancel() + } + } + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + /// Current Surface and this is used to pass down to child objects that implement Surfacable + open var surface: Surface = .light { didSet { updateView() }} + open var presenter: UIView? { didSet { updateView() }} + open var dialog: UIView! + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight) + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func viewDidLoad() { + super.viewDidLoad() + isModalInPresentation = true + setup() + } + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + UIAccessibility.post(notification: .screenChanged, argument: dialog) + } + + private func dismiss() { + dismiss(animated: true) { [weak self] in + guard let self, let presenter else { return } + UIAccessibility.post(notification: .layoutChanged, argument: presenter) + } + } + + open func setup() { + guard let dialog else { return } + view.accessibilityElements = [dialog] + + //left-right swipe + view.publisher(for: UISwipeGestureRecognizer().with{ $0.direction = .right }) + .sink { [weak self] swipe in + guard let self, !UIAccessibility.isVoiceOverRunning else { return } + self.dismiss() + }.store(in: &subscribers) + + //tapping in background + view.publisher(for: UITapGestureRecognizer().with{ $0.numberOfTapsRequired = 1 }) + .sink { [weak self] swipe in + guard let self, !UIAccessibility.isVoiceOverRunning else { return } + self.dismiss() + }.store(in: &subscribers) + + view.addSubview(dialog) + + // Activate constraints + NSLayoutConstraint.activate([ + // Constraints for the floating modal view + dialog.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dialog.centerYAnchor.constraint(equalTo: view.centerYAnchor), + dialog.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 10), + dialog.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -10), + dialog.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 10), + dialog.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -10) + ]) + } + + /// Used to make changes to the View based off a change events or from local properties. + open func updateView() { + view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3) + if var dialog = dialog as? Surfaceable { + dialog.surface = surface + } + } +}