// // PriceLockup.swift // VDS // // Created by Kanamarlapudi, Vasavi on 06/08/24. // import Foundation import UIKit import VDSCoreTokens @objcMembers @objc(VDSPriceLockup) open class PriceLockup: 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 PriceLockup. public enum Kind: String, CaseIterable { case primary, secondary, savings } /// Enum used to describe the term of PriceLockup. public enum Term: String, CaseIterable { case month, year, biennial, none /// The default term is 'month'. public static var defaultValue : Self { .month } /// Text for this term of PriceLockup. public var text: String { switch self { case .month: return "mo" case .year: return "yr" case .biennial: return "biennial" case .none: return "" } } } /// Enum type describing size of PriceLockup. public enum Size: String, CaseIterable { case xxxsmall case xxsmall case xsmall case small case medium case large case xlarge case xxlarge public var defaultValue: Self { .medium } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// If true, the component will render as bold. open var bold: Bool = false { didSet { setNeedsUpdate() } } /// Currency - If hideCurrency true, the component will render without currency. open var hideCurrency: Bool = false { didSet { setNeedsUpdate() } } /// Leading text for the component. open var leadingText: String? { didSet { setNeedsUpdate() } } /// Value rendered for the component. open var price: Float? { didSet { setNeedsUpdate() } } /// Color to the component. The default kind is primary. open var kind: Kind = .primary { didSet { setNeedsUpdate() } } /// Size of the component. It varies by size and viewport(mobile/Tablet). /// The default size is medium with viewport mobile. open var size: Size = .medium { didSet { setNeedsUpdate() } } /// If true, the component with a strikethrough. It applies only when uniformSize is true. /// Does not apply a strikethrough format to leading and trailing text. open var strikethrough: Bool = false { didSet { setNeedsUpdate() } } /// Term text for the component. The default term is 'month'. /// Superscript placement can vary when term and delimeter are "none". open var term: Term = Term.defaultValue { didSet { setNeedsUpdate() } } /// Trailing text for the component. open var trailingText: String? { didSet { setNeedsUpdate() } } /// Superscript text for the component. open var superscript: String? { didSet { setNeedsUpdate() } } /// If true, currency and value have the same font text style as delimeter, term label and superscript. /// This will render the pricing and term sections as a uniform size. open var uniformSize: Bool = false { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var containerView = View().with { $0.clipsToBounds = true } internal var label = Label().with { $0.isAccessibilityElement = true $0.lineBreakMode = .byWordWrapping } internal var index = 0 internal var strikethroughLocation = 0 internal var strikethroughlength = 0 internal var textContentType:TextContentType = .preDelimiter enum TextContentType: String, CaseIterable { case preDelimiter, postDelimiter } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal var containerSize: CGSize { CGSize(width: 45, height: 44) } private var contentFontSize: VDS.TextStyle { switch (size, textContentType) { case (.xxxsmall, .preDelimiter), (.xxxsmall, .postDelimiter): return TextStyle.micro case (.xxsmall, .preDelimiter), (.xxsmall, .postDelimiter): return TextStyle.bodySmall case (.xsmall, .preDelimiter), (.xsmall, .postDelimiter): return TextStyle.bodyMedium case (.small, .preDelimiter), (.small, .postDelimiter): return TextStyle.bodyLarge case (.medium, .preDelimiter): return UIDevice.isIPad ? TextStyle.titleSmall : TextStyle.titleMedium case (.medium, .postDelimiter): return TextStyle.bodyLarge case (.large, .preDelimiter): return UIDevice.isIPad ? TextStyle.titleMedium : TextStyle.titleLarge case (.large, .postDelimiter): return UIDevice.isIPad ? TextStyle.titleSmall : TextStyle.titleMedium case (.xlarge, .preDelimiter): return UIDevice.isIPad ? TextStyle.titleLarge : TextStyle.titleXLarge case (.xlarge, .postDelimiter): return UIDevice.isIPad ? TextStyle.titleMedium : TextStyle.titleLarge case (.xxlarge, .preDelimiter): return UIDevice.isIPad ? TextStyle.titleXLarge : TextStyle.featureSmall case (.xxlarge, .postDelimiter): return UIDevice.isIPad ? TextStyle.titleLarge : TextStyle.titleXLarge } } private var textColorConfiguration: ViewColorConfiguration { switch kind { case .primary: return ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) } case .secondary: return ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false) } case .savings: return ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.paletteGreen26, VDSColor.paletteGreen36, forDisabled: false) } } } //-------------------------------------------------- // 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() addSubview(containerView) containerView .pinTop() .pinBottom() .pinLeadingGreaterThanOrEqualTo() .pinTrailingLessThanOrEqualTo() .height(containerSize.height) containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() // Price lockup containerView.addSubview(label) label.pinToSuperView() label.centerXAnchor.constraint(equalTo: centerXAnchor).activate() } func updateLabel() { var attributes: [any LabelAttributeModel] = [] let colorAttr = ColorLabelAttribute(location: 0, length: label.text.count, color: textColorConfiguration.getColor(self)) attributes.append(colorAttr) if index > 0 { textContentType = .preDelimiter let textStyleAttr = TextStyleLabelAttribute(location: 0, length: index, textStyle: contentFontSize) textContentType = .postDelimiter let othertextStyleAttr = TextStyleLabelAttribute(location: index+1, length: label.text.count-index-1, textStyle: contentFontSize) attributes.append(textStyleAttr) attributes.append(othertextStyleAttr) } label.attributes = attributes } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() label.text = fetchText() label.surface = surface // Set the attributed text updateLabel() if strikethrough { // strike applies only when uniformSize true. Does not apply a strikethrough format to leading, trailing, and superscript text. textContentType = .postDelimiter var strikethroughAttributes: [any LabelAttributeModel]? { [TextStyleLabelAttribute(location: 0, length: label.text.count, textStyle: contentFontSize), StrikeThroughLabelAttribute(location:strikethroughLocation, length: strikethroughlength)] } label.attributes = strikethroughAttributes } if uniformSize { // currency and value have the same font text style as delimeter, term, trailing text and superscript. textContentType = .postDelimiter var uniformSizeAttributes: [any LabelAttributeModel]? { [TextStyleLabelAttribute(location: 0, length: label.text.count, textStyle: contentFontSize)] } label.attributes = uniformSizeAttributes } } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false label.reset() shouldUpdateView = true setNeedsUpdate() } open override var accessibilityElements: [Any]? { get { return nil } set {} } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- open func fetchText() -> String { var text : String = "" index = 0 let currency: String = hideCurrency ? "" : "$" if let leadingStr = leadingText { text = text + leadingStr + " " index = index + leadingStr.count + 1 } if let value = price { strikethroughLocation = index let valueStr = "\(value.clean)" text = text + currency + valueStr index = index + valueStr.count + 1 strikethroughlength = valueStr.count + 1 } if term != .none { text = text + "/" + term.text strikethroughlength = strikethroughlength + term.text.count + 1 } if let trailingStr = trailingText { text = text + " " + trailingStr } text = text + (superscript ?? "") return text } } extension Float { // remove a decimal from a float if the decimal is equal to 0 var clean: String { return self.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", self) : String(self) } }