// // 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, 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, CaseIterable { case xxxsmall case xxsmall case xsmall case small case medium case large case xlarge case xxlarge public static var defaultValue: Self { .medium } } /// Enum used to describe the kind of PriceLockup. public enum Kind: String, CaseIterable { case primary, secondary, savings /// The default kind is 'primary'. public static var defaultValue : Self { .primary } /// Color configuation relative to kind. public var colorConfiguration: ViewColorConfiguration { switch self { 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: - 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 = 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 = 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 = 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 = fetchText() priceLockupLabel.surface = surface // Set the attributed text updateLabelAttributes() } /// Resets to default settings. open override func reset() { super.reset() shouldUpdateView = false priceLockupLabel.reset() shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // 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. open func fetchText() -> String { var text : String = "" let space = " " let delimiter = "/" delimiterIndex = 0 strikethroughLength = 0 let currency: String = hideCurrency ? "" : "$" if let leadingStr = leadingText { text = text + leadingStr + space delimiterIndex = delimiterIndex + leadingStr.count + space.count } strikethroughLocation = delimiterIndex if let value = price { let valueStr = "\(value.clean)" text = text + currency + valueStr delimiterIndex = delimiterIndex + valueStr.count + currency.count strikethroughLength = valueStr.count + currency.count } if term != .none { text = text + delimiter + term.type strikethroughLength = strikethroughLength + delimiter.count + term.type.count } if let trailingStr = trailingText { text = text + space + 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) } }