// // Footnote.swift // VDS // // Created by Kanamarlapudi, Vasavi on 21/08/24. // import Foundation import UIKit import VDSCoreTokens import Combine /// 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 symboldType of component. public enum SymbolType: String, DefaultValuing, CaseIterable { case asterisk case doubleAsterisk case character public static var defaultValue: Self { .asterisk } /// TextStyle relative to Size. public var text: String { switch self { case .asterisk: return "*" case .doubleAsterisk: return "**" case .character: return "1." } } } /// 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() } } /// Size of the component. The default size is Micro. open var symbolType: SymbolType = .defaultValue { 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() } } //-------------------------------------------------- // 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: - Constraints //-------------------------------------------------- internal var symbolWidthConstraint: 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.pinToSuperView() // 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 = hideSymbol ? "" : symbolType.text symbolLabel.textColor = kind.colorConfiguration.getColor(self) symbolLabel.textStyle = size.textStyle.regular symbolLabel.surface = surface // Update symbolLabel width as per updated text size 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 } } }