// // TextLinkCaret.swift // VDS // // Created by Matt Bruce on 11/1/22. // import Foundation import UIKit import VDSCoreTokens import Combine /// A text link caret is an interactive element that always brings a customer to another page. It's used for navigation, /// like "Back", or a call-to-action for a product or feature, like "Shop smartphones". This class can be used within a ``ButtonGroup``. /// /// If you are using AutoLayoutConstraints you have a combination of Leading/Left and Trailing/Right NSLayoutConstraints, /// you need to ensure that one of these Horizontal Contraints is not constraint of "equatTo". If you are to pin the left/right edges /// to its parent this object will stretch to the parent's width. @objc(VDSTextLinkCaret) open class TextLinkCaret: ButtonBase { //-------------------------------------------------- // 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: - Enums //-------------------------------------------------- /// Enum used to describe the position of the icon in relation to the title label. public enum IconPosition: String, CaseIterable { case left, right } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var textColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.interactiveActiveOnlight, VDSColor.interactiveActiveOndark, forState: .highlighted) } private var imageAttribute: CaretLabelAttribute? //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Determines icon position of Caret. open var iconPosition: IconPosition = .right { didSet { setNeedsUpdate() } } open override var textAttributes: [any LabelAttributeModel]? { guard let imageAttribute else { return nil } return [imageAttribute] } /// UIColor used on the titleLabel text. open override var textColor: UIColor { textColorConfiguration.getColor(self) } open override var textStyle: TextStyle { TextStyle.boldBodyLarge } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() //left align titleLabel in case this is pinned leading/trailing //default is always set to center contentHorizontalAlignment = .left accessibilityTraits = .link titleLabel?.numberOfLines = 0 titleLabel?.lineBreakMode = .byWordWrapping bridge_accessibilityHintBlock = { [weak self] in guard let self else { return "" } return !isEnabled ? "" : "Double tap to open." } } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { imageAttribute = CaretLabelAttribute(tintColor: textColor, position: iconPosition) super.updateView() } /// Resets to default settings. open override func reset() { super.reset() iconPosition = .right text = nil } /// The natural size for the receiving view, considering only properties of the view itself. open override var intrinsicContentSize: CGSize { guard let titleLabel else { return super.intrinsicContentSize } // Calculate the titleLabel's intrinsic content size let labelSize = titleLabel.sizeThatFits(CGSize(width: self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right), height: CGFloat.greatestFiniteMagnitude)) // Adjust the size if needed (add any additional padding if your design requires) let adjustedSize = CGSize(width: labelSize.width + contentEdgeInsets.left + contentEdgeInsets.right, height: labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom) return adjustedSize } open override func layoutSubviews() { super.layoutSubviews() // This ensures the titleLabel is correctly positioned within the button titleLabel?.preferredMaxLayoutWidth = self.frame.width - (contentEdgeInsets.left + contentEdgeInsets.right) super.layoutSubviews() // Calling super again to ensure layout is updated with preferredMaxLayoutWidth } } extension TextLinkCaret { struct CaretLabelAttribute: LabelAttributeModel { var id: UUID = .init() var location: Int = 0 var length: Int = 1 var tintColor: UIColor var position: IconPosition var spacerWidth: CGFloat = 4.0 var width: CGFloat { caretSize.width + spacerWidth } var caretSize: CGSize { Icon.Size.xsmall.dimensions } init(tintColor: UIColor, position: IconPosition) { self.tintColor = tintColor self.position = position } func setAttribute(on attributedString: NSMutableAttributedString) { let imageAttr = ImageLabelAttribute(location: location, imageName: "\(position.rawValue)-caret-bold", frame: .init(x: 0, y: 0, width: caretSize.width, height: caretSize.height), tintColor: tintColor) let spacer = NSAttributedString.spacer(for: spacerWidth) guard let image = try? imageAttr.getAttachment() else { return } if position == .right { attributedString.append(spacer) attributedString.append(NSAttributedString(attachment: image)) } else { attributedString.insert(NSAttributedString(attachment: image), at: 0) attributedString.insert(spacer, at: 1) } } func isEqual(_ equatable: CaretLabelAttribute) -> Bool { return id == equatable.id && range == equatable.range } } }