// // 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 term of PriceLockup. public enum Term: String, DefaultValuing, 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 type: String { switch self { case .month: return "mo" case .year: return "yr" case .biennial: return "biennial" case .none: return "" } } } /// Enum that represents the size availble for PriceLockup. public enum Size: String, DefaultValuing, CaseIterable { case xxxsmall = "3XSmall" case xxsmall = "2XSmall" case xsmall = "XSmall" case small case medium case large case xlarge = "XLarge" case xxlarge = "2XLarge" public static var defaultValue: Self { .medium } } /// Enum used to describe the kind of PriceLockup. public enum Kind: String, DefaultValuing, CaseIterable { case primary, secondary, savings /// The default kind is 'primary'. public static var defaultValue : Self { .primary } /// Color configuation 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) case .savings: return SurfaceColorConfiguration(VDSColor.paletteGreen26, VDSColor.paletteGreen36) } } } //-------------------------------------------------- // 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 = .defaultValue { 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 = .defaultValue { 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 = .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 priceLockupLabel = Label().with { $0.isAccessibilityElement = true $0.lineBreakMode = .byWordWrapping } internal var delimiterIndex = 0 internal var strikethroughLocation = 0 internal var strikethroughLength = 0 internal var textPosition:TextPosition = .preDelimiter enum TextPosition: String, CaseIterable { case preDelimiter, postDelimiter } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- internal var containerSize: CGSize { CGSize(width: 45, height: 44) } // TextStyle for the size. private var textStyle: TextStyle.StandardStyle { switch (size, textPosition) { case (.xxxsmall, .preDelimiter), (.xxxsmall, .postDelimiter): return .micro case (.xxsmall, .preDelimiter), (.xxsmall, .postDelimiter): return .bodySmall case (.xsmall, .preDelimiter), (.xsmall, .postDelimiter): return .bodyMedium case (.small, .preDelimiter), (.small, .postDelimiter): return .bodyLarge case (.medium, .preDelimiter): return UIDevice.isIPad ? .titleSmall : .titleMedium case (.medium, .postDelimiter): return .bodyLarge case (.large, .preDelimiter): return UIDevice.isIPad ? .titleMedium : .titleLarge case (.large, .postDelimiter): return UIDevice.isIPad ? .titleSmall : .titleMedium case (.xlarge, .preDelimiter): return UIDevice.isIPad ? .titleLarge : .titleXLarge case (.xlarge, .postDelimiter): return UIDevice.isIPad ? .titleMedium : .titleLarge case (.xxlarge, .preDelimiter): return UIDevice.isIPad ? .titleXLarge : .featureSmall case (.xxlarge, .postDelimiter): return UIDevice.isIPad ? .titleLarge : .titleXLarge } } //-------------------------------------------------- // 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() // Price lockup label addSubview(priceLockupLabel) priceLockupLabel.pinToSuperView() } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() priceLockupLabel.text = formatText() priceLockupLabel.surface = surface // Set the attributed text updateLabelAttributes() } open override func setDefaults() { super.setDefaults() bold = false hideCurrency = false leadingText = nil price = nil kind = .defaultValue size = .defaultValue strikethrough = false term = .defaultValue trailingText = nil superscript = nil uniformSize = false } /// Resets to default settings. open override func reset() { priceLockupLabel.reset() super.reset() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- // Update PriceLockup text attributes func updateLabelAttributes() { var attributes: [any LabelAttributeModel] = [] attributes.append(ColorLabelAttribute(location: 0, length: priceLockupLabel.text.count, color: kind.colorConfiguration.getColor(self))) textPosition = .postDelimiter if strikethrough { // strike applies only when uniformSize true. Does not apply a strikethrough format to leading, trailing, and superscript text. attributes.append(TextStyleLabelAttribute(location: 0, length: priceLockupLabel.text.count, textStyle: bold ? textStyle.bold : textStyle.regular, textPosition: .left)) attributes.append(StrikeThroughLabelAttribute(location:strikethroughLocation, length: strikethroughLength)) } else if uniformSize { // currency and value have the same font text style as delimeter, term, trailing text and superscript. attributes.append(TextStyleLabelAttribute(location: 0, length: priceLockupLabel.text.count, textStyle: bold ? textStyle.bold : textStyle.regular, textPosition: .left)) } else { // size updates relative to predelimiter, postdelimiter if delimiterIndex > 0 { textPosition = .preDelimiter attributes.append(TextStyleLabelAttribute(location: 0, length: delimiterIndex, textStyle: bold ? textStyle.bold : textStyle.regular, textPosition: .left)) textPosition = .postDelimiter attributes.append(TextStyleLabelAttribute(location: delimiterIndex, length: priceLockupLabel.text.count-delimiterIndex, textStyle: bold ? textStyle.bold : textStyle.regular, textPosition: .left)) } } priceLockupLabel.attributes = attributes } // Get text for PriceLockup. private func formatText() -> String { var text : String = "" let space = " " let delimiter = "/" delimiterIndex = 0 strikethroughLength = 0 let currency: String = hideCurrency ? "" : "$" if let leadingText { text.append(leadingText) text.append(space) delimiterIndex = delimiterIndex + leadingText.count + space.count } strikethroughLocation = delimiterIndex if let price = price?.clean { text.append(currency) text.append(price) delimiterIndex = delimiterIndex + price.count + currency.count strikethroughLength = price.count + currency.count } if term != .none { text.append(delimiter) text.append(term.type) strikethroughLength = strikethroughLength + delimiter.count + term.type.count } if let trailingText { text.append(space) text.append(trailingText) } if let superscript { text.append(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(describing: self) } }