vds_ios/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift
Matt Bruce aacc2ca760 added property to help with instrinsic size
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
2022-12-01 11:34:24 -06:00

320 lines
10 KiB
Swift

//
// TextLinkCaret.swift
// VDS
//
// Created by Matt Bruce on 11/1/22.
//
import Foundation
import UIKit
import VDSColorTokens
import VDSFormControlsTokens
import Combine
public enum TextLinkCaretPosition: String, CaseIterable {
case left, right
}
@objc(VDSTextLinkCaret)
open class TextLinkCaret: Control, Buttonable {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var heightConstraint: NSLayoutConstraint?
private var label = Label().with {
$0.typograpicalStyle = TypographicalStyle.BoldBodyLarge
}
private var caretView = CaretView().with {
$0.size = CaretView.CaretSize.small(.vertical)
$0.lineWidth = 2
}
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var availableSizes: [ButtonSize] { [.large] }
open var text: String? { didSet { didChange() } }
open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } }
private var height: CGFloat {
44
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(frame: .zero)
initialSetup()
}
public override init(frame: CGRect) {
super.init(frame: .zero)
initialSetup()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
//--------------------------------------------------
// MARK: - Public Functions
//--------------------------------------------------
open override func initialSetup() {
super.initialSetup()
}
open override func setup() {
super.setup()
//add tapGesture to self
publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in
self?.sendActions(for: .touchUpInside)
}.store(in: &subscribers)
//constraints
heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
let size = caretView.size!.dimensions()
caretView.frame = .init(x: 0, y: 0, width: size.width, height: size.height)
addSubview(label)
label.pinToSuperView()
label.numberOfLines = 1
label.preferredMaxLayoutWidth = 0
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
private var caretLeadingConstraint: NSLayoutConstraint?
private var caretTrailingConstraint: NSLayoutConstraint?
private var labelConstraint: NSLayoutConstraint?
open override func reset() {
super.reset()
label.reset()
label.typograpicalStyle = TypographicalStyle.BoldBodyLarge
text = nil
iconPosition = .right
accessibilityCustomActions = []
accessibilityTraits = .staticText
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
override open var intrinsicContentSize: CGSize {
var itemWidth = label.intrinsicContentSize.width
if let caretWidth = caretView.size?.dimensions().width {
itemWidth += caretWidth
}
return CGSize(width: itemWidth, height: height)
}
open override func updateView() {
let updatedText = text ?? ""
caretView.surface = surface
caretView.disabled = disabled
caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left
let image = caretView.getImage()
let location = iconPosition == .right ? updatedText.count + 1 : 0
let textColor = label.textColorConfiguration.getColor(self)
let imageAttribute = ImageLabelAttribute(location: location,
image: image,
tintColor: textColor)
label.surface = surface
label.disabled = disabled
label.text = iconPosition == .right ? "\(updatedText) " : " \(updatedText)"
label.attributes = [imageAttribute]
}
}
extension UIView {
public func getImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let image = renderer.image { ctx in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
internal class CaretView: View {
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
private var caretPath: UIBezierPath = UIBezierPath()
public var lineWidth: CGFloat = 1 { didSet{ didChange() } }
public var direction: Direction = .right { didSet{ didChange() } }
public var size: CaretSize? { didSet{ didChange() } }
public var colorConfiguration: AnyColorable = DisabledSurfaceColorConfiguration().with {
$0.disabled.lightColor = VDSColor.elementsSecondaryOnlight
$0.disabled.darkColor = VDSColor.elementsSecondaryOndark
$0.enabled.lightColor = VDSColor.elementsPrimaryOnlight
$0.enabled.darkColor = VDSColor.elementsPrimaryOndark
}.eraseToAnyColorable()
//------------------------------------------------------
// MARK: - Constraints
//------------------------------------------------------
/// Sizes of CaretView are derived from InVision design specs. They are provided for convenience.
public enum CaretSize {
case small(Orientation)
case medium(Orientation)
case large(Orientation)
/// Orientation based on the longest line of the view.
public enum Orientation {
case vertical
case horizontal
}
/// Dimensions of container; provided by InVision design.
func dimensions() -> CGSize {
switch self {
case .small(let o):
return o == .vertical ? CGSize(width: 6.9, height: 10.96) : CGSize(width: 10.96, height: 6.9)
case .medium(let o):
return o == .vertical ? CGSize(width: 9.9, height: 16.96) : CGSize(width: 16.96, height: 9.9)
case .large(let o):
return o == .vertical ? CGSize(width: 14.9, height: 24.96) : CGSize(width: 24.96, height: 14.9)
}
}
}
//------------------------------------------------------
// MARK: - Initialization
//------------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public convenience init(lineWidth: CGFloat) {
self.init(frame: .zero)
self.lineWidth = lineWidth
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("CaretView xib not supported.")
}
required public convenience init() {
self.init(frame: .zero)
}
public convenience init(size: CaretSize){
let dimensions = size.dimensions()
self.init(frame: .init(x: 0, y: 0, width: dimensions.width, height: dimensions.height))
self.size = size
}
//------------------------------------------------------
// MARK: - Setup
//------------------------------------------------------
override open func setup() {
super.setup()
defaultState()
}
//------------------------------------------------------
// MARK: - Drawing
//------------------------------------------------------
/// The direction the caret will be pointing to.
public enum Direction: Int {
case left
case right
case down
case up
}
override func draw(_ rect: CGRect) {
super.draw(rect)
caretPath.removeAllPoints()
caretPath.lineJoinStyle = .miter
caretPath.lineWidth = lineWidth
let inset = lineWidth / 2
let halfWidth = frame.size.width / 2
let halfHeight = frame.size.height / 2
switch direction {
case .up:
caretPath.move(to: CGPoint(x: inset, y: frame.size.height - inset))
caretPath.addLine(to: CGPoint(x: halfWidth, y: inset))
caretPath.addLine(to: CGPoint(x: frame.size.width, y: frame.size.height))
case .right:
caretPath.move(to: CGPoint(x: inset, y: inset))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: halfHeight))
caretPath.addLine(to: CGPoint(x: inset, y: frame.size.height - inset))
case .down:
caretPath.move(to: CGPoint(x: inset, y: inset))
caretPath.addLine(to: CGPoint(x: halfWidth, y: frame.size.height - inset))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: inset))
case .left:
caretPath.move(to: CGPoint(x: frame.size.width - inset, y: inset))
caretPath.addLine(to: CGPoint(x: inset, y: halfHeight))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: frame.size.height - inset))
}
let color = colorConfiguration.getColor(self)
color.setStroke()
caretPath.stroke()
}
override func updateView() {
setNeedsDisplay()
}
//------------------------------------------------------
// MARK: - Methods
//------------------------------------------------------
public func setLineColor(_ color: UIColor) {
setNeedsDisplay()
}
public func defaultState() {
isOpaque = false
isHidden = false
backgroundColor = .clear
}
/// Ensure you have defined a CaretSize with Orientation before calling.
public func setConstraints() {
guard let dimensions = size?.dimensions() else { return }
heightAnchor.constraint(equalToConstant: dimensions.height).isActive = true
widthAnchor.constraint(equalToConstant: dimensions.width).isActive = true
}
}