// // 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 var image: UIImage { UIImage.image(for: self == .left ? .leftCaretBold : .rightCaretBold)! } } //-------------------------------------------------- // 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) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Determines icon position of Caret. open var iconPosition: IconPosition = .right { didSet { setNeedsUpdate() } } /// 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 } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { setImage(iconPosition.image.withTintColor(textColor), for: .normal) 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. // Property to specify the icon size private var imageSize: CGSize = Icon.Size.xsmall.dimensions open override func layoutSubviews() { super.layoutSubviews() guard let titleLabel = titleLabel, let imageView = imageView else { return } // Adjust imageView size based on the imageSize property imageView.frame.size = imageSize let space: CGFloat = 5 // Space between the icon and the text // Adjust icon and text positions based on the iconPosition switch iconPosition { case .left: imageView.frame.origin.x = bounds.minX + contentEdgeInsets.left imageView.frame.origin.y = titleLabel.frame.minY + (textStyle.lineHeight - imageSize.height) / 2.0 titleLabel.frame.origin.x = imageView.frame.maxX + space case .right: guard let attribtedText = titleLabel.attributedText else { return } let textContainer = NSTextContainer(size: CGSize(width: titleLabel.bounds.width, height: CGFloat.greatestFiniteMagnitude)) textContainer.lineFragmentPadding = 0 let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) let textStorage = NSTextStorage(attributedString: attribtedText) textStorage.addLayoutManager(layoutManager) let lastGlyphIndex = layoutManager.glyphIndexForCharacter(at: attribtedText.string.utf16.count - 1) var lastGlyphRect = layoutManager.boundingRect(forGlyphRange: NSRange(location: lastGlyphIndex, length: 1), in: textContainer) lastGlyphRect.origin.x += titleLabel.frame.origin.x lastGlyphRect.origin.y += titleLabel.frame.origin.y imageView.frame.origin.x = lastGlyphRect.maxX + space imageView.frame.origin.y = lastGlyphRect.midY - imageSize.height / 2 } imageView.contentMode = .scaleAspectFit } private var space: CGFloat { return 5 // Space between the icon and text, used in multiple places } }