diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index b6a3d192..6e21eacf 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068912A560B65002E3A2D /* LoaderViewController.swift */; }; EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */; }; EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */; }; + EAE785352BA22825009428EA /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE785342BA22825009428EA /* TextField.swift */; }; EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */; }; EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */; }; EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */; }; @@ -303,6 +304,7 @@ EAD068912A560B65002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = ""; }; EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderLaunchable.swift; sourceTree = ""; }; EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = ""; }; + EAE785342BA22825009428EA /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeChangeLog.txt; sourceTree = ""; }; EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonChangeLog.txt; sourceTree = ""; }; EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LineChangeLog.txt; sourceTree = ""; }; @@ -800,6 +802,7 @@ isa = PBXGroup; children = ( EAC925872911C9DE00091998 /* InputField.swift */, + EAE785342BA22825009428EA /* TextField.swift */, ); path = InputField; sourceTree = ""; @@ -1053,6 +1056,7 @@ EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */, EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */, EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */, + EAE785352BA22825009428EA /* TextField.swift in Sources */, EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */, EA89200628B526D6006B9984 /* CheckboxGroup.swift in Sources */, EA8E40932A82889500934ED3 /* TooltipDialog.swift in Sources */, diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index ac350bc1..0f3e1449 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -64,10 +64,7 @@ open class InputField: EntryFieldBase, UITextFieldDelegate { } /// UITextField shown in the InputField. - open var textField = UITextField().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.font = TextStyle.bodyLarge.font - } + open var textField = TextField() /// Color configuration for the textField. open var textFieldTextColorConfiguration: AnyColorable = ViewColorConfiguration().with { diff --git a/VDS/Components/TextFields/InputField/TextField.swift b/VDS/Components/TextFields/InputField/TextField.swift new file mode 100644 index 00000000..e1b33440 --- /dev/null +++ b/VDS/Components/TextFields/InputField/TextField.swift @@ -0,0 +1,253 @@ +// +// 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) + } + } +}