// // TextField.swift // VDS // // Created by Matt Bruce on 3/13/24. // import Foundation import UIKit import Combine import VDSColorTokens @objc(VDSTextField) open class TextField: UITextField, ViewProtocol { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) initialSetup() } public override init(frame: CGRect) { super.init(frame: frame) initialSetup() } public required init?(coder: NSCoder) { super.init(coder: coder) initialSetup() } //-------------------------------------------------- // MARK: - Combine Properties //-------------------------------------------------- /// Set of Subscribers for any Publishers for this Control. open var subscribers = Set() //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var initialSetupPerformed = false //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true open var surface: Surface = .light { didSet { setNeedsUpdate() } } /// Array of LabelAttributeModel objects used in rendering the text. open var textAttributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() } } /// TextStyle used on the titleLabel. open var textStyle: TextStyle { .defaultStyle } /// Will determine if a scaled font should be used for the titleLabel font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() } } open override var isEnabled: Bool { didSet { setNeedsUpdate() } } open override var isSelected: Bool { didSet { setNeedsUpdate() } } /// State of animating isHighlight. public var isHighlighting = false /// Whether the Control should handle the isHighlighted state. open var shouldHighlight: Bool { isHighlighting == false } /// Whether the Control is highlighted or not. open override var isHighlighted: Bool { didSet { if shouldHighlight { isHighlighting = true setNeedsUpdate() isHighlighting = false } } } open var textColorConfiguration: AnyColorable = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable(){ didSet { setNeedsUpdate() }} open override var textColor: UIColor? { get { textColorConfiguration.getColor(self) } set { } } private enum TextSetMode { case text case attributedText } private var textSetMode: TextSetMode = .text /// :nodoc: open override var text: String! { get { super.text } set { // When text is set, we may need to re-style it as attributedText // with the correct paragraph style to achieve the desired line height. textSetMode = .text styleText(newValue) } } /// :nodoc: open override var attributedText: NSAttributedString? { get { super.attributedText } set { // When text is set, we may need to re-style it as attributedText // with the correct paragraph style to achieve the desired line height. textSetMode = .attributedText styleAttributedText(newValue) } } /// :nodoc: open override var textAlignment: NSTextAlignment { didSet { if textAlignment != oldValue { // Text alignment can be part of our paragraph style, so we may need to // re-style when changed restyleText() } } } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] setup() setNeedsUpdate() } } open func setup() { translatesAutoresizingMaskIntoConstraints = false } open func updateView() { restyleText() } open func updateAccessibility() {} open func reset() { shouldUpdateView = false surface = .light text = nil accessibilityCustomActions = [] shouldUpdateView = true setNeedsUpdate() } //-------------------------------------------------- // MARK: - Overrides Methods //-------------------------------------------------- /// :nodoc open override func textRect(forBounds bounds: CGRect) -> CGRect { super.textRect(forBounds: bounds).inset(by: textStyle.edgeInsets) } /// :nodoc open override func editingRect(forBounds bounds: CGRect) -> CGRect { super.editingRect(forBounds: bounds).inset(by: textStyle.edgeInsets) } /// :nodoc open override func clearButtonRect(forBounds bounds: CGRect) -> CGRect { super.clearButtonRect(forBounds: bounds).offsetBy(dx: -textStyle.edgeInsets.right, dy: 0) } /// :nodoc open override func leftViewRect(forBounds bounds: CGRect) -> CGRect { super.leftViewRect(forBounds: bounds).offsetBy(dx: textStyle.edgeInsets.left, dy: 0) } /// :nodoc open override func rightViewRect(forBounds bounds: CGRect) -> CGRect { super.rightViewRect(forBounds: bounds).offsetBy(dx: -textStyle.edgeInsets.right, dy: 0) } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func restyleText() { if textSetMode == .text { styleText(text) } else { styleAttributedText(attributedText) } } private func styleText(_ newValue: String!) { defer { invalidateIntrinsicContentSize() } guard let newValue else { // We don't need to use attributed text super.attributedText = nil super.text = newValue return } accessibilityCustomActions = [] //create the primary string let mutableText = NSMutableAttributedString.mutableText(for: newValue, textStyle: textStyle, useScaledFont: useScaledFont, textColor: textColorConfiguration.getColor(self), alignment: textAlignment, lineBreakMode: .byWordWrapping) applyAttributes(mutableText) // Set attributed text to match typography super.attributedText = mutableText } private func styleAttributedText(_ newValue: NSAttributedString?) { defer { invalidateIntrinsicContentSize() } guard let newValue = newValue else { // We don't need any additional styling super.attributedText = newValue return } let mutableText = NSMutableAttributedString(attributedString: newValue) applyAttributes(mutableText) // Modify attributed text to match typography super.attributedText = mutableText } private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) { if let textAttributes { mutableAttributedString.apply(attributes: textAttributes) } } }