// // 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 imageAttribute: CaretLabelAttribute? { iconPosition == .right ? CaretLabelAttribute(tintColor: textColor, position: iconPosition) : nil } //-------------------------------------------------- // 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() accessibilityTraits = .link titleLabel?.numberOfLines = 0 } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { setImage(iconPosition == .right ? nil : BundleManager.shared.image(for: Icon.Name.leftCaretBold.rawValue), for: .normal) imageEdgeInsets = iconPosition == .right ? .zero : .init(top: 0, left: -spacing, bottom: 0, right: 0) 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. override open var intrinsicContentSize: CGSize { //get the labels size, if not the button if iconPosition == .right { return titleLabel?.intrinsicContentSize ?? super.intrinsicContentSize } else { let width = imageSize.width + spacing + (titleLabel?.intrinsicContentSize.width ?? super.intrinsicContentSize.width) let height = titleLabel?.intrinsicContentSize.height ?? super.intrinsicContentSize.height return .init(width: width, height: height) } } private let imageSize = Icon.Size.xsmall.dimensions private let spacing = 4.0 // private var activeConstraints: [NSLayoutConstraint] = [] // // private func setupConstraints() { // guard let titleLabel else { return } // titleLabel.translatesAutoresizingMaskIntoConstraints = false // // NSLayoutConstraint.deactivate(activeConstraints) // activeConstraints.removeAll() // // if let caret = BundleManager.shared.image(for: Icon.Name.leftCaretBold.rawValue), iconPosition == .left{ // setImage(caret, for: .normal) // guard let imageView else { return } // imageView.removeConstraints(imageView.constraints) // imageView.translatesAutoresizingMaskIntoConstraints = false // // activeConstraints = [ // imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), // imageView.topAnchor.constraint(equalTo: titleLabel.topAnchor, constant: 5), // imageView.widthAnchor.constraint(equalToConstant: imageSize.width), // imageView.heightAnchor.constraint(equalToConstant: imageSize.height), // titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor), // titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), // titleLabel.topAnchor.constraint(equalTo: self.topAnchor), // titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor) // ] // // } else { // // setImage(nil, for: .normal) // activeConstraints = [ // titleLabel.topAnchor.constraint(equalTo: self.topAnchor), // titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor), // titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), // titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor) // ] // } // // NSLayoutConstraint.activate(activeConstraints) // } open override func layoutSubviews() { super.layoutSubviews() imageView?.frame.size = imageSize imageView?.frame.origin = .init(x: 0, y: spacing) } // open override func layoutSubviews() { // super.layoutSubviews() // // guard let imageView, let titleLabel else { return } // // if imageView.isHidden { // titleLabel.frame.origin.x = 0 // contentEdgeInsets = .zero // } else { // imageView.frame.origin.x = 0 // titleLabel.frame.origin.x = imageSize.width + spacing // // let totalWidth = titleLabel.frame.maxX // let leftInset = (bounds.width - totalWidth) / 2 // contentEdgeInsets = .init(top: 0, left: leftInset, bottom: 0, right: leftInset) // } // } } 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)) } } func isEqual(_ equatable: CaretLabelAttribute) -> Bool { return id == equatable.id && range == equatable.range } } }