Merge branch 'feature/tooltip' into 'develop'
added initial files for Tooltip See merge request BPHV_MIPS/vds_ios!55
This commit is contained in:
commit
3b219bd330
@ -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 */,
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
152
VDS/Components/Tooltip/Tooltip.swift
Normal file
152
VDS/Components/Tooltip/Tooltip.swift
Normal 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)"
|
||||
}
|
||||
|
||||
}
|
||||
162
VDS/Components/Tooltip/TooltipAlertViewController.swift
Normal file
162
VDS/Components/Tooltip/TooltipAlertViewController.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
27
VDS/Components/Tooltip/TooltipLaunchable.swift
Normal file
27
VDS/Components/Tooltip/TooltipLaunchable.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
VDS/Components/Tooltip/TrailingTooltipLabel.swift
Normal file
94
VDS/Components/Tooltip/TrailingTooltipLabel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
32
VDS/Extensions/UIApplication.swift
Normal file
32
VDS/Extensions/UIApplication.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user