// // Footnote.swift // VDS // // Created by Kanamarlapudi, Vasavi on 21/08/24. // import Foundation import UIKit import VDSCoreTokens /// A footnote is text that provides supporting details, legal copy and links to related content. /// It exists at the bottom or "foot" of a page or section. @objcMembers @objc(VDSFootnote) open class Footnote: View { //-------------------------------------------------- // 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 kind of component. public enum Kind: String, DefaultValuing, CaseIterable { case primary, secondary /// The default kind is 'primary'. public static var defaultValue : Self { .secondary } /// Color configuation to Symbol and Text relative to kind. public var colorConfiguration: SurfaceColorConfiguration { switch self { case .primary: return SurfaceColorConfiguration(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark) case .secondary: return SurfaceColorConfiguration(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark) } } } /// Enum that represents the size availble for component. public enum Size: String, DefaultValuing, CaseIterable { case micro case small case large public static var defaultValue: Self { .micro } /// TextStyle relative to Size. public var textStyle: TextStyle.StandardStyle { switch self { case .micro: return .micro case .small: return .bodySmall case .large: return .bodyLarge } } } /// Enum used to describe the width of a fixed value or percentage of parent's width. public enum Width { case percentage(CGFloat) case value(CGFloat) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Color to the component. The default kind is Secondary. open var kind: Kind = .defaultValue { didSet { setNeedsUpdate() } } /// Size of the component. The default size is Micro. open var size: Size = .defaultValue { didSet { setNeedsUpdate() } } /// If hideSymbol true, the component will show text without symbol. open var hideSymbol: Bool = false { didSet { setNeedsUpdate() } } /// symbol type will be shown for the footnote item. The default symbolType is 'asterisk'. open var symbolType: String = "*" { didSet { setNeedsUpdate() } } /// Text of the footnote item. open var text: String? { didSet { setNeedsUpdate() } } open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } /// Any percentage or pixel value and cannot exceed container size. /// If there is a width that is larger than container size, the footnote will resize to container's width. open var width: Width? { get { _width } set { if let newValue { switch newValue { case .percentage(let percentage): if percentage <= 100.0 { _width = newValue } case .value(let value): if value > 0 { _width = newValue } } } else { _width = nil } setNeedsUpdate() } } /// To set the widest symbol width from the symbol container in the group. open var symbolWiderWidth: CGFloat = 0 { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var _width: Width? = nil private lazy var itemStackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.alignment = .top $0.distribution = .fill $0.spacing = VDSLayout.space1X $0.backgroundColor = .clear } internal var symbolLabel = Label().with { $0.isAccessibilityElement = true $0.numberOfLines = 1 $0.sizeToFit() } internal var textLabel = Label().with { $0.isAccessibilityElement = true $0.lineBreakMode = .byWordWrapping } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal var maxWidth: CGFloat { frame.size.width } internal var minWidth: CGFloat { containerSize.width } internal var containerSize: CGSize { CGSize(width: 45, height: 44) } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var symbolWidthConstraint: NSLayoutConstraint? internal var itemWidthConstraint: NSLayoutConstraint? internal var trailingEqualsConstraint: NSLayoutConstraint? internal var trailingLessThanEqualsConstraint: NSLayoutConstraint? //-------------------------------------------------- // 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() // add footnote item stackview. addSubview(itemStackView) itemStackView.pinTop().pinBottom().pinLeading() trailingEqualsConstraint = itemStackView.pinTrailing(anchor: trailingAnchor) // width constraints itemWidthConstraint = itemStackView.widthAnchor.constraint(equalToConstant: 0).deactivate() trailingLessThanEqualsConstraint = itemStackView.pinTrailingLessThanOrEqualTo(anchor: trailingAnchor)?.deactivate() // add symbol label, text label to stack. itemStackView.addArrangedSubview(symbolLabel) itemStackView.addArrangedSubview(textLabel) itemStackView.setCustomSpacing(VDSLayout.space1X, after: symbolLabel) symbolWidthConstraint = symbolLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) symbolWidthConstraint?.isActive = true } open override func setDefaults() { super.setDefaults() hideSymbol = false text = nil tooltipModel = nil width = nil } /// Resets to default settings. open override func reset() { symbolLabel.reset() textLabel.reset() super.reset() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() // Update symbolLabel symbolWidthConstraint?.isActive = false symbolLabel.text = symbolType symbolLabel.isHidden = hideSymbol symbolLabel.textColor = kind.colorConfiguration.getColor(self) symbolLabel.textStyle = size.textStyle.regular symbolLabel.surface = surface //Set width to the symbol label if symbolWiderWidth > 0 { // Set the widest symbol width from the symbol container in the group. symbolWidthConstraint = symbolLabel.widthAnchor.constraint(equalToConstant: symbolWiderWidth) } else { symbolWidthConstraint = symbolLabel.widthAnchor.constraint(equalToConstant: symbolLabel.intrinsicContentSize.width) } symbolWidthConstraint?.isActive = true // Update textLabel textLabel.text = text textLabel.textColor = kind.colorConfiguration.getColor(self) textLabel.textStyle = size.textStyle.regular textLabel.surface = surface // Set the textLabel attributes if let tooltipModel { var attributes: [any LabelAttributeModel] = [] attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self)) textLabel.attributes = attributes } updateContainerWidth() } /// Update container width after updating content. internal func updateContainerWidth() { var newWidth = 0.0 switch width { case .percentage(let percentage): newWidth = max(maxWidth * ((percentage) / 100), minWidth) case .value(let value): newWidth = value > maxWidth ? maxWidth : value case nil: newWidth = maxWidth } itemWidthConstraint?.deactivate() trailingLessThanEqualsConstraint?.deactivate() trailingEqualsConstraint?.deactivate() if newWidth > minWidth && newWidth < maxWidth { itemWidthConstraint?.constant = newWidth itemWidthConstraint?.activate() trailingLessThanEqualsConstraint?.activate() } else { trailingEqualsConstraint?.activate() } } }