Merge branch 'feature/tooltip' into 'develop'

added initial files for Tooltip

See merge request BPHV_MIPS/vds_ios!55
This commit is contained in:
Bruce, Matt R 2023-04-17 18:40:19 +00:00
commit 3b219bd330
13 changed files with 533 additions and 157 deletions

View File

@ -68,7 +68,6 @@
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C682971B90B00F2FF2E /* IconSize.swift */; };
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C7C297DAED300F2FF2E /* Primitive.swift */; };
EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */; };
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; };
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; };
EAA5EEE428F5B855003B3210 /* VerizonNHGDS-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */; };
EAA5EEEF28F5C908003B3210 /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA5EEEC28F5C908003B3210 /* VDSTypographyTokens.xcframework */; };
@ -78,6 +77,11 @@
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 */; };
EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB2376929E9E59100AABE9A /* TooltipLaunchable.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 */; };
@ -185,7 +189,6 @@
EA985C682971B90B00F2FF2E /* IconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSize.swift; sourceTree = "<group>"; };
EA985C7C297DAED300F2FF2E /* Primitive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = "<group>"; };
EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLabelAttribute.swift; sourceTree = "<group>"; };
EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTipLabelAttribute.swift; sourceTree = "<group>"; };
EAA5EEB828ECD24B003B3210 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = "<group>"; };
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colorable.swift; sourceTree = "<group>"; };
EAA5EEE328F5B855003B3210 /* VerizonNHGDS-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "VerizonNHGDS-Light.otf"; sourceTree = "<group>"; };
@ -196,6 +199,11 @@
EAB1D2CC28ABE76000DAE764 /* Withable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Withable.swift; sourceTree = "<group>"; };
EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = "<group>"; };
EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlPublisher.swift; sourceTree = "<group>"; };
EAB2375C29E8789100AABE9A /* Tooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tooltip.swift; sourceTree = "<group>"; };
EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingTooltipLabel.swift; sourceTree = "<group>"; };
EAB2376529E9952D00AABE9A /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipAlertViewController.swift; sourceTree = "<group>"; };
EAB2376929E9E59100AABE9A /* TooltipLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipLaunchable.swift; sourceTree = "<group>"; };
EAB5FED329267EB300998C17 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = "<group>"; };
EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = "<group>"; };
@ -381,6 +389,7 @@
EA5E3056295105930082B959 /* Tilelet */,
EA5E30512950DD8D0082B959 /* TitleLockup */,
EA3361A0288B1E6F0071C351 /* Toggle */,
EAB2375B29E8786100AABE9A /* Tooltip */,
);
path = Components;
sourceTree = "<group>";
@ -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,17 @@
path = Publishers;
sourceTree = "<group>";
};
EAB2375B29E8786100AABE9A /* Tooltip */ = {
isa = PBXGroup;
children = (
EAB2375C29E8789100AABE9A /* Tooltip.swift */,
EAB2376729E9992800AABE9A /* TooltipAlertViewController.swift */,
EAB2376929E9E59100AABE9A /* TooltipLaunchable.swift */,
EAB2376129E9880400AABE9A /* TrailingTooltipLabel.swift */,
);
path = Tooltip;
sourceTree = "<group>";
};
EAC9257E29119B5D00091998 /* TextLink */ = {
isa = PBXGroup;
children = (
@ -642,7 +663,6 @@
EAF7F0AA289B13FD00B287F5 /* TextStyleLabelAttribute.swift */,
EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */,
EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */,
EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */,
EAF7F0AE289B144C00B287F5 /* UnderlineLabelAttribute.swift */,
);
path = Attributes;
@ -783,12 +803,14 @@
EA89201328B568D8006B9984 /* RadioBox.swift in Sources */,
EAC9258C2911C9DE00091998 /* InputField.swift in Sources */,
EA3362402892EF6C0071C351 /* Label.swift in Sources */,
EAB2376229E9880400AABE9A /* TrailingTooltipLabel.swift in Sources */,
EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */,
EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */,
EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */,
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */,
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */,
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */,
EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */,
EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */,
EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.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 */,

View File

@ -32,17 +32,18 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
public var length: Int
public var shouldUnderline: Bool
public var accessibleText: String?
public var action = PassthroughSubject<Void, Never>()
public var action: PassthroughSubject<Void, Never>
public var subscriber: AnyCancellable?
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(location: Int, length: Int, shouldUnderline: Bool = true, accessibleText: String? = nil) {
public init(location: Int, length: Int, shouldUnderline: Bool = true, accessibleText: String? = nil, action: PassthroughSubject<Void, Never> = .init() ) {
self.location = location
self.length = length
self.shouldUnderline = shouldUnderline
self.accessibleText = accessibleText
self.action = action
}
private enum CodingKeys: String, CodingKey {
@ -53,5 +54,10 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
if(shouldUnderline){
UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString)
}
attributedString.addAttribute(NSAttributedString.Key.action, value: "handler", range: range)
}
}
extension NSAttributedString.Key {
public static let action = NSAttributedString.Key(rawValue: "action")
}

View File

@ -46,7 +46,9 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel {
private func imageAttachment(image: UIImage) -> NSTextAttachment {
let attachment = NSTextAttachment()
attachment.image = tintColor != nil ? image.withTintColor(tintColor!) : image
attachment.bounds = frame ?? .init(x: 0, y: 0, width: image.size.width, height: image.size.height)
if let frame {
attachment.bounds = frame
}
return attachment
}

View File

@ -1,47 +0,0 @@
//
// ToolTipLabelAttribute.swift
// VDS
//
// Created by Matt Bruce on 10/4/22.
//
import Foundation
import UIKit
import Combine
public struct ToolTipLabelAttribute: ActionLabelAttributeModel {
public var id = UUID()
public var accessibleText: String? = "Tool Tip"
public var action: PassthroughSubject<Void, Never>
public var location: Int
public var length: Int
public var tintColor: UIColor
public func setAttribute(on attributedString: NSMutableAttributedString) {
let image = ImageLabelAttribute(location: location,
length: length,
imageName: "info",
frame: .init(x: 0, y: -2, width: 13.3, height: 13.3),
tintColor: tintColor)
image.setAttribute(on: attributedString)
}
public init(action: PassthroughSubject<Void, Never> = .init(), location: Int, length: Int, tintColor: UIColor = .black, accessibleText: String? = nil){
self.action = action
self.location = location
self.length = length
self.tintColor = tintColor
self.accessibleText = accessibleText
}
public static func == (lhs: ToolTipLabelAttribute, rhs: ToolTipLabelAttribute) -> Bool {
lhs.isEqual(rhs)
}
public func isEqual(_ equatable: ToolTipLabelAttribute) -> Bool {
return id == equatable.id && range == equatable.range
}
}

View File

@ -253,7 +253,7 @@ open class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable {
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for actionable in actions {
// This determines if we tapped on the desired range of text.
if gesture.didTapAttributedTextInLabel(self, inRange: actionable.range) {
if gesture.didTapActionInLabel(self, inRange: actionable.range) {
actionable.performAction()
return
}

View File

@ -51,7 +51,7 @@ open class Line: View {
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private var lineViewColorConfig: AnyColorable = {
public var lineViewColorConfig: AnyColorable = {
let config = KeyedColorConfiguration<Line, Style>(keyPath: \.style)
config.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forKey: .primary)
config.setSurfaceColors(VDSColor.elementsLowContrastOnLight, VDSColor.elementsLowContrastOnDark, forKey: .secondary)

View File

@ -106,11 +106,12 @@ open class EntryField: Control, Changeable {
}
}
open var titleLabel = Label().with {
open var titleLabel = TrailingTooltipLabel().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.attributes = []
$0.textPosition = .left
$0.textStyle = .bodySmall
$0.labelTextPosition = .left
$0.labelTextStyle = .bodySmall
$0.tooltipSize = .small
$0.tooltipYOffset = -2
}
open var errorLabel = Label().with {
@ -228,56 +229,14 @@ open class EntryField: Control, Changeable {
return containerView
}
open func getToolTipView() -> UIView? {
guard let tooltipTitle, let tooltipContent, !tooltipTitle.isEmpty, !tooltipContent.isEmpty else {
return nil
}
let stack = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .vertical
$0.distribution = .fill
$0.spacing = 4
}
let title = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left
$0.textStyle = .boldBodySmall
$0.text = tooltipTitle
$0.surface = surface
$0.disabled = disabled
}
let content = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left
$0.textStyle = .boldBodySmall
$0.text = tooltipContent
$0.surface = surface
$0.disabled = disabled
}
stack.addArrangedSubview(title)
stack.addArrangedSubview(content)
stack.backgroundColor = backgroundColorConfiguration.getColor(self)
return stack
}
open func showToolTipView(){
print("toolTip clicked: showToolTipView() called")
}
open override func reset() {
super.reset()
titleLabel.reset()
errorLabel.reset()
helperLabel.reset()
titleLabel.textPosition = .left
titleLabel.textStyle = .bodySmall
titleLabel.labelTextPosition = .left
titleLabel.labelTextStyle = .bodySmall
errorLabel.textPosition = .left
errorLabel.textStyle = .bodySmall
helperLabel.textPosition = .left
@ -332,29 +291,12 @@ open class EntryField: Control, Changeable {
updatedLabelText = "\(oldText) Optional"
attributes.append(optionColorAttr)
}
//add the tool tip
if let view = getToolTipView(), let oldText = updatedLabelText {
tooltipView = view
let toolTipAction = PassthroughSubject<Void, Never>()
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: primaryColorConfig.getColor(self))
updatedLabelText = toolTipUpdateText
attributes.append(toolTipAttribute)
toolTipAction.sink { [weak self] in
self?.showToolTipView()
}.store(in: &subscribers)
} else {
tooltipView = nil
}
//set the titleLabel
titleLabel.text = updatedLabelText
titleLabel.attributes = attributes
titleLabel.labelText = updatedLabelText
titleLabel.labelAttributes = attributes
titleLabel.tooltipTitle = tooltipTitle ?? ""
titleLabel.tooltipContent = tooltipContent ?? ""
titleLabel.surface = surface
titleLabel.disabled = disabled

View File

@ -0,0 +1,152 @@
//
// 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, TooltipLaunchable {
//--------------------------------------------------
// 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<Tooltip, FillColor>(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(surface: tooltip.surface,
title: tooltip.title,
content: tooltip.content,
closeButtonText: tooltip.closeButtonText)
})
}
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)"
}
}

View File

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

View File

@ -0,0 +1,27 @@
//
// ToolTipLaunchable.swift
// VDS
//
// Created by Matt Bruce on 4/14/23.
//
import Foundation
import UIKit
public protocol TooltipLaunchable { }
extension TooltipLaunchable {
public func presentTooltip(surface: Surface, title: String, content: String, closeButtonText: String = "Close") {
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)
}
}
}

View File

@ -0,0 +1,94 @@
//
// TrailingTooltipLabel.swift
// VDS
//
// Created by Matt Bruce on 4/14/23.
//
import Foundation
import UIKit
import Combine
@objc(VDSTrailingTooltipLabel)
open class TrailingTooltipLabel: View, TooltipLaunchable {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private let tooltipAction = PassthroughSubject<Void, Never>()
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
open var label = Label()
open var labelText: String? { didSet { didChange() }}
open var labelAttributes: [any LabelAttributeModel]? { didSet { didChange() } }
open var labelTextStyle: TextStyle = .defaultStyle { didSet { didChange() } }
open var labelTextPosition: TextPosition = .left { didSet { didChange() } }
public lazy var textColorConfiguration: AnyColorable = {
label.textColorConfiguration
}() { didSet { didChange() }}
open var tooltipCloseButtonText: String = "Close" { didSet { didChange() } }
open var tooltipSize: Tooltip.Size = .medium { didSet { didChange() } }
open var tooltipTitle: String = "" { didSet { didChange() } }
open var tooltipContent: String = "" { didSet { didChange() } }
open var tooltipYOffset: CGFloat = 0 { didSet { didChange() } }
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func setup() {
super.setup()
addSubview(label)
label.pinToSuperView()
//create the tooltip click event
tooltipAction.sink { [weak self] in
guard let self else { return }
self.presentTooltip(surface: self.surface,
title: self.tooltipTitle,
content: self.tooltipContent,
closeButtonText: self.tooltipCloseButtonText)
}.store(in: &subscribers)
}
open override func updateView() {
super.updateView()
var attributes: [any LabelAttributeModel] = []
if let labelAttributes {
attributes.append(contentsOf: labelAttributes)
}
var updatedLabelText = labelText
//add the tool tip
if let oldText = updatedLabelText, !tooltipTitle.isEmpty, !tooltipContent.isEmpty {
let tooltipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image
let frame = CGRect(x: 0, y: tooltipYOffset, width: tooltipSize.dimensions.width, height: tooltipSize.dimensions.width)
let color = textColorConfiguration.getColor(self)
let tooltipAttribute = ImageLabelAttribute(location: tooltipUpdateText.count - 2, imageName: "info", frame: frame, tintColor: color)
let tooltipAction = ActionLabelAttribute(location: tooltipUpdateText.count - 3, length: 3, shouldUnderline: false, action: tooltipAction)
updatedLabelText = tooltipUpdateText
attributes.append(tooltipAttribute)
attributes.append(tooltipAction)
}
//set the titleLabel
label.text = updatedLabelText
label.attributes = attributes
label.textStyle = labelTextStyle
label.textPosition = labelTextPosition
label.surface = surface
label.disabled = disabled
}
}

View File

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

View File

@ -10,38 +10,20 @@ import UIKit
extension UITapGestureRecognizer {
public func didTapAttributedTextInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let abstractContainer = label.abstractTextContainer() else { return false }
let textContainer = abstractContainer.0
let layoutManager = abstractContainer.1
public func didTapActionInLabel(_ label: UILabel, inRange targetRange: NSRange) -> Bool {
let tapLocation = location(in: label)
let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer)
let intrinsicWidth = label.intrinsicContentSize.width
guard let attributedText = label.attributedText else { return false }
// Assert that tapped occured within acceptable bounds based on alignment.
switch label.textAlignment {
case .right:
if tapLocation.x < label.bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = label.bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if tapLocation.x > halfBounds + halfIntrinsicWidth {
return false
} else if tapLocation.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if tapLocation.x > intrinsicWidth {
return false
}
}
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: label.bounds.size)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let location = location(in: label)
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard let _ = attributedText.attribute(NSAttributedString.Key.action, at: characterIndex, effectiveRange: nil) as? String, characterIndex < attributedText.length else { return false }
return true
}
}