// // TextLinkCaret.swift // VDS // // Created by Matt Bruce on 11/1/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens 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 rightCaret: CaretLabelAttribute? private var leftCaret: Icon = Icon().with { $0.name = .leftCaretBold $0.size = .xsmall } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Determines icon position of Caret. open var iconPosition: IconPosition = .right { didSet { setNeedsUpdate() } } open override var textAttributes: [any LabelAttributeModel]? { guard let rightCaret, iconPosition == .right else { return nil } return [rightCaret] } /// 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() accessibilityTraits = .link titleLabel?.lineBreakMode = .byWordWrapping titleLabel?.numberOfLines = 0 } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { if iconPosition == .right { leftCaret.removeFromSuperview() rightCaret = CaretLabelAttribute(tintColor: textColor, position: .right) setImage(nil, for: .normal) titleEdgeInsets = .zero contentEdgeInsets = .zero } else { if let text, text.isEmpty { setImage(nil, for: .normal) titleEdgeInsets = .zero contentEdgeInsets = .zero } else { leftCaret.color = textColor if let image = leftCaret.imageView.image, let resized = resize(image: image, size: leftCaret.size.dimensions) { contentVerticalAlignment = .top setImage(resized, for: .normal) imageEdgeInsets = .init(top: 5, left: 0, bottom: 0, right: VDSLayout.Spacing.space1X.value) titleEdgeInsets = .init(top: 0, left: VDSLayout.Spacing.space1X.value, bottom: 0, right: 0) } } } super.updateView() } func resize(image: UIImage, size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) image.draw(in: CGRect(origin: .zero, size: size)) let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resizedImage } /// 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. override open var intrinsicContentSize: CGSize { //get the labels size, if not the button return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize } } 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 = VDSLayout.Spacing.space1X.value 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 } } }