254 lines
8.1 KiB
Swift
254 lines
8.1 KiB
Swift
//
|
|
// 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<AnyCancellable>()
|
|
|
|
//--------------------------------------------------
|
|
// 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)
|
|
}
|
|
}
|
|
}
|