diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 01cb6e65..bbb598cb 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -78,6 +78,10 @@ EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CC28ABE76000DAE764 /* Withable.swift */; }; EAB1D2CF28ABEF2B00DAE764 /* Typography.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */; }; EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */; }; + EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2375C29E8789100AABE9A /* Tooltip.swift */; }; + EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */; }; + EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376529E9952D00AABE9A /* UIApplication.swift */; }; + EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */; }; EAB5FED429267EB300998C17 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FED329267EB300998C17 /* UIView.swift */; }; EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */; }; EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */; }; @@ -196,6 +200,10 @@ EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = ""; }; EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = ""; }; EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlPublisher.swift; sourceTree = ""; }; + EAB2375C29E8789100AABE9A /* Tooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tooltip.swift; sourceTree = ""; }; + EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingTooltipLabel.swift; sourceTree = ""; }; + EAB2376529E9952D00AABE9A /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; + EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipAlertViewController.swift; sourceTree = ""; }; EAB5FED329267EB300998C17 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = ""; }; EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = ""; }; @@ -381,6 +389,7 @@ EA5E3056295105930082B959 /* Tilelet */, EA5E30512950DD8D0082B959 /* TitleLockup */, EA3361A0288B1E6F0071C351 /* Toggle */, + EAB2375B29E8786100AABE9A /* Tooltip */, ); path = Components; sourceTree = ""; @@ -398,6 +407,7 @@ children = ( EAF7F0992899B17200B287F5 /* CATransaction.swift */, EA33622D2891EA3C0071C351 /* DispatchQueue+Once.swift */, + EAB2376529E9952D00AABE9A /* UIApplication.swift */, EA3361A7288B23300071C351 /* UIColor.swift */, EA33623D2892EE950071C351 /* UIDevice.swift */, EAF7F0B4289C126F00B287F5 /* UILabel.swift */, @@ -580,6 +590,16 @@ path = Publishers; sourceTree = ""; }; + EAB2375B29E8786100AABE9A /* Tooltip */ = { + isa = PBXGroup; + children = ( + EAB2375C29E8789100AABE9A /* Tooltip.swift */, + EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */, + EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */, + ); + path = Tooltip; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -783,6 +803,8 @@ EA89201328B568D8006B9984 /* RadioBox.swift in Sources */, EAC9258C2911C9DE00091998 /* InputField.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, + EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */, + EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */, @@ -848,8 +870,10 @@ EA3361B6288B2A410071C351 /* Control.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, + EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, EA985BF9296C710100F2FF2E /* IconColor.swift in Sources */, EAB5FED429267EB300998C17 /* UIView.swift in Sources */, + EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */, diff --git a/VDS/Components/Tooltip/Tooltip.swift b/VDS/Components/Tooltip/Tooltip.swift new file mode 100644 index 00000000..bb48a059 --- /dev/null +++ b/VDS/Components/Tooltip/Tooltip.swift @@ -0,0 +1,162 @@ +// +// Tooltip.swift +// VDS +// +// Created by Matt Bruce on 4/13/23. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +@objc(VDSTooltip) +open class Tooltip: Control { + + //-------------------------------------------------- + // MARK: - Enums + //-------------------------------------------------- + public enum FillColor: String, CaseIterable { + case primary, secondary, brandHighlight + } + + public enum Size: String, CaseIterable { + case small + case medium + + public var dimensions: CGSize { + switch self { + + case .small: + return .init(width: 13.33, height: 13.33) + + case .medium: + return .init(width: 16.67, height: 16.67) + + } + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var widthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + private var infoImage = UIImage() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var imageView = UIImageView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.contentMode = .scaleAspectFill + $0.clipsToBounds = true + } + + open var closeButtonText: String = "Close" { didSet { didChange() }} + + open var fillColor: FillColor = .primary { didSet { didChange() }} + + open var size: Size = .medium { didSet { didChange() }} + + open var title: String = "" { didSet { didChange() }} + + open var content: String = "" { didSet { didChange() }} + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private var iconColorConfig: AnyColorable = { + let config = KeyedColorConfiguration(keyPath: \.fillColor) + config.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forKey: .primary) + config.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forKey: .secondary) + config.setSurfaceColors(VDSColor.elementsBrandhighlight, VDSColor.elementsBrandhighlight, forKey: .brandHighlight) + return config.eraseToAnyColorable() + }() + + //-------------------------------------------------- + // 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: - Lifecycle + //-------------------------------------------------- + + open override func setup() { + super.setup() + + if let image = BundleManager.shared.image(for: "info") { + infoImage = image + } + + addSubview(imageView) + + imageView.pinToSuperView() + heightConstraint = imageView.heightAnchor.constraint(equalToConstant: size.dimensions.height) + heightConstraint?.isActive = true + widthConstraint = imageView.widthAnchor.constraint(equalToConstant: size.dimensions.width) + widthConstraint?.isActive = true + + backgroundColor = .clear + + isAccessibilityElement = true + accessibilityTraits = .link + + onClickSubscriber = publisher(for: .touchUpInside) + .sink(receiveValue: { [weak self] tooltip in + guard let self else { return} + self.presentTooltip() + }) + } + + open override func reset() { + super.reset() + size = .medium + title = "" + content = "" + fillColor = .primary + closeButtonText = "Close" + imageView.image = nil + } + + open override func updateView() { + super.updateView() + + //set the dimensions + let dimensions = size.dimensions + heightConstraint?.constant = dimensions.height + widthConstraint?.constant = dimensions.width + + //get the color for the image + let imageColor = iconColorConfig.getColor(self) + imageView.image = infoImage.withTintColor(imageColor) + + accessibilityLabel = "Tooltip: \(title)" + } + + private func presentTooltip() { + if let presenting = UIApplication.topViewController() { + let tooltipViewController = TooltipAlertViewController(nibName: nil, bundle: nil).with { + $0.surface = surface + $0.titleText = title + $0.contentText = content + $0.closeButtonText = closeButtonText + $0.modalPresentationStyle = .overCurrentContext + $0.modalTransitionStyle = .crossDissolve + } + presenting.present(tooltipViewController, animated: true) + } + } +} diff --git a/VDS/Components/Tooltip/TooltipAlertViewController.swift b/VDS/Components/Tooltip/TooltipAlertViewController.swift new file mode 100644 index 00000000..a6cbf14a --- /dev/null +++ b/VDS/Components/Tooltip/TooltipAlertViewController.swift @@ -0,0 +1,162 @@ +// +// TooltipAlertViewController.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens + +open class TooltipAlertViewController: UIViewController, Surfaceable { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var onClickSubscriber: AnyCancellable? { + willSet { + if let onClickSubscriber { + onClickSubscriber.cancel() + } + } + } + + private var scrollView = UIScrollView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.backgroundColor = .clear + $0.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -5) + } + + private let containerView = View().with { + $0.layer.cornerRadius = 8 + $0.layer.shadowColor = UIColor.black.cgColor + $0.layer.shadowOpacity = 0.5 + $0.layer.shadowOffset = CGSize.zero + $0.layer.shadowRadius = 5 + } + + private var line = Line().with { instance in + instance.lineViewColorConfig = SurfaceColorConfiguration(VDSColor.elementsLowContrastOnLight, VDSColor.elementsLowContrastOnLight).eraseToAnyColorable() + } + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var surface: Surface = .light { didSet { updateView() }} + open var titleText: String = "" { didSet { updateView() }} + open var titleLabel = Label().with { label in + label.textStyle = .boldTitleMedium + } + + open var contentText: String = "" { didSet { updateView() }} + open var contentLabel = Label().with { label in + label.textStyle = .bodyMedium + } + + open var closeButtonText: String = "Close" { didSet { updateView() }} + + open lazy var closeButton: UIButton = { + let button = UIButton(type: .system) + button.backgroundColor = .clear + button.setTitle("Close", for: .normal) + button.titleLabel?.font = TextStyle.bodyLarge.font + button.translatesAutoresizingMaskIntoConstraints = false + onClickSubscriber = button.publisher(for: .touchUpInside).sink {[weak self] button in + guard let self else { return } + self.dismiss(animated: true, completion: nil) + } + return button + }() + + //-------------------------------------------------- + // MARK: - Configuration + //-------------------------------------------------- + private let containerViewBackgroundColorConfiguration = SurfaceColorConfiguration().with { instance in + instance.lightColor = .white + instance.darkColor = .black + } + + private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight) + + private let closeButtonTextColorConfiguration = SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func viewDidLoad() { + super.viewDidLoad() + isModalInPresentation = true + setup() + } + + open func setup() { + + scrollView.addSubview(titleLabel) + scrollView.addSubview(contentLabel) + containerView.addSubview(scrollView) + containerView.addSubview(line) + containerView.addSubview(closeButton) + view.addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: VDSLayout.Spacing.space8X.value), + containerView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -VDSLayout.Spacing.space8X.value), + containerView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor), + containerView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor), + + containerView.heightAnchor.constraint(equalToConstant: 312), + containerView.widthAnchor.constraint(equalToConstant: 296), + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + + scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: VDSLayout.Spacing.space4X.value), + scrollView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: VDSLayout.Spacing.space4X.value), + scrollView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -VDSLayout.Spacing.space4X.value), + scrollView.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -VDSLayout.Spacing.space4X.value), + + titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + titleLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: VDSLayout.Spacing.space1X.value), + contentLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + contentLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + contentLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + + line.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + line.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + + closeButton.topAnchor.constraint(equalTo: line.bottomAnchor), + closeButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + closeButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + closeButton.heightAnchor.constraint(equalToConstant: 44.0) + ]) + + } + + open func updateView() { + view.backgroundColor = backgroundColorConfiguration.getColor(self).withAlphaComponent(0.3) + containerView.backgroundColor = containerViewBackgroundColorConfiguration.getColor(self) + scrollView.indicatorStyle = surface == .light ? .black : .white + + titleLabel.surface = surface + contentLabel.surface = surface + line.surface = surface + + titleLabel.text = titleText + contentLabel.text = contentText + titleLabel.sizeToFit() + contentLabel.sizeToFit() + + let closeButtonTextColor = closeButtonTextColorConfiguration.getColor(self) + closeButton.setTitleColor(closeButtonTextColor, for: .normal) + closeButton.setTitleColor(closeButtonTextColor, for: .highlighted) + closeButton.setTitle(closeButtonText, for: .normal) + } +} + diff --git a/VDS/Components/Tooltip/TrailingTooltipLabel.swift b/VDS/Components/Tooltip/TrailingTooltipLabel.swift new file mode 100644 index 00000000..0714ad04 --- /dev/null +++ b/VDS/Components/Tooltip/TrailingTooltipLabel.swift @@ -0,0 +1,84 @@ +// +// TrailingTooltipLabel.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit +import Combine + +@objc(VDSTrailingTooltipLabel) +open class TrailingTooltipLabel: View { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private let toolTipAction = PassthroughSubject() + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + open var label = Label() + + open var labelText: String? { didSet { didChange() }} + + open var labelTextStyle: TextStyle = .defaultStyle { didSet { didChange() }} + + public lazy var textColorConfiguration: AnyColorable = { + label.textColorConfiguration + }() { didSet { didChange() }} + + open var closeButtonText: String? = "Close" { didSet { didChange() }} + + open var tooltipSize: Tooltip.Size = .medium { didSet { didChange() }} + + open var tooltipTitle: String = "" { didSet { didChange() }} + + open var tooltipContent: String = "" { didSet { didChange() }} + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func setup() { + super.setup() + + addSubview(label) + label.pinToSuperView() + + //create the tooltip click event + toolTipAction.sink { [weak self] in + self?.showToolTip() + }.store(in: &subscribers) + } + + open override func updateView() { + super.updateView() + + var attributes: [any LabelAttributeModel] = [] + var updatedLabelText = labelText + + //add the tool tip + if let oldText = updatedLabelText { + let toolTipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image + let toolTipAttribute = ToolTipLabelAttribute(action: toolTipAction, + location: toolTipUpdateText.count - 1, + length: 1, + tintColor: textColorConfiguration.getColor(self)) + updatedLabelText = toolTipUpdateText + attributes.append(toolTipAttribute) + } + + //set the titleLabel + label.text = updatedLabelText + label.attributes = attributes + label.textStyle = labelTextStyle + label.surface = surface + label.disabled = disabled + } + + private func showToolTip(){ + print("You should show the tooltip now!") + } +} diff --git a/VDS/Extensions/UIApplication.swift b/VDS/Extensions/UIApplication.swift new file mode 100644 index 00000000..52cf69d6 --- /dev/null +++ b/VDS/Extensions/UIApplication.swift @@ -0,0 +1,32 @@ +// +// UIApplication.swift +// VDS +// +// Created by Matt Bruce on 4/14/23. +// + +import Foundation +import UIKit + +extension UIApplication { + + public class func topViewController(controller: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? { + + if let nav = controller as? UINavigationController { + return topViewController(controller: nav.visibleViewController) + } + + if let tab = controller as? UITabBarController { + if let selected = tab.selectedViewController { + return topViewController(controller: selected) + } + } + + if let presented = controller?.presentedViewController { + return topViewController(controller: presented) + } + + return controller + } +} +