// // 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 { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var heightConstraint: NSLayoutConstraint? private let containerView = UIView().with{ $0.translatesAutoresizingMaskIntoConstraints = false } 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 //-------------------------------------------------- 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) addSubview(containerView) containerView.addSubview(label) containerView.addSubview(caretView) //constraints heightAnchor.constraint(equalToConstant: height).isActive = true containerView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true containerView.heightAnchor.constraint(lessThanOrEqualToConstant: height).isActive = true label.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true label.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor, multiplier: 0.90).isActive = true caretView.bottomAnchor.constraint(lessThanOrEqualTo: label.bottomAnchor, constant: -3).isActive = true caretView.setConstraints() } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- private var caretLeadingConstraint: NSLayoutConstraint? private var caretTrailingConstraint: NSLayoutConstraint? private var labelConstraint: NSLayoutConstraint? private func setConstraints(){ caretLeadingConstraint?.isActive = false caretTrailingConstraint?.isActive = false labelConstraint?.isActive = false if iconPosition == .right { labelConstraint = label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) caretLeadingConstraint = caretView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 4) caretTrailingConstraint = caretView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) } else { labelConstraint = label.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor) caretLeadingConstraint = caretView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor) caretTrailingConstraint = caretView.trailingAnchor.constraint(equalTo: label.leadingAnchor, constant: -4) } caretTrailingConstraint?.isActive = true caretLeadingConstraint?.isActive = true labelConstraint?.isActive = true self.layoutIfNeeded() } open override func reset() { super.reset() accessibilityCustomActions = [] accessibilityTraits = .staticText } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- open override func updateView() { label.surface = surface label.disabled = disabled label.text = text caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left setConstraints() } } 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) } //------------------------------------------------------ // 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 } }