diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 125d88a8..10b469ed 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ EA5F86C82A1BD99100BC83E4 /* TabModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86C72A1BD99100BC83E4 /* TabModel.swift */; }; EA5F86CC2A1D28B500BC83E4 /* ReleaseNotes.txt in Resources */ = {isa = PBXBuildFile; fileRef = EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */; }; EA5F86D02A1F936100BC83E4 /* TabsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */; }; + EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6F330D2B911E9000BACAB9 /* TextView.swift */; }; EA81410B2A0E8E3C004F60D2 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */; }; EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */; }; EA89200428AECF4B006B9984 /* UITextField+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */; }; @@ -236,6 +237,7 @@ EA5F86C72A1BD99100BC83E4 /* TabModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabModel.swift; sourceTree = ""; }; EA5F86CB2A1D28B500BC83E4 /* ReleaseNotes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ReleaseNotes.txt; sourceTree = ""; }; EA5F86CF2A1F936100BC83E4 /* TabsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsContainer.swift; sourceTree = ""; }; + EA6F330D2B911E9000BACAB9 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; EA81410A2A0E8E3C004F60D2 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+VDSColor.swift"; sourceTree = ""; }; EA89200328AECF4B006B9984 /* UITextField+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextField+Publisher.swift"; sourceTree = ""; }; @@ -717,6 +719,7 @@ isa = PBXGroup; children = ( EA985C22296E033A00F2FF2E /* TextArea.swift */, + EA6F330D2B911E9000BACAB9 /* TextView.swift */, 186B2A892B88DA7F001AB71F /* TextAreaChangeLog.txt */, ); path = TextArea; @@ -1059,6 +1062,7 @@ EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, + EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */, EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */, EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */, EAD0688E2A55F819002E3A2D /* Loader.swift in Sources */, diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 5fe4e68b..317748e4 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -35,7 +35,6 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- internal var minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? - internal var allowCharCount: Int = 0 internal var inputFieldStackView: UIStackView = { return UIStackView().with { @@ -204,11 +203,9 @@ open class TextArea: EntryFieldBase { minWidthConstraint?.isActive = true } + let characterError = getCharacterCounterText() if let maxLength, maxLength > 0 { - // allow - 20% of character limit - let overflowLimit = Double(maxLength) * 0.20 - allowCharCount = Int(overflowLimit) + maxLength - characterCounterLabel.text = getCharacterCounterText() + characterCounterLabel.text = characterError } else { characterCounterLabel.text = "" } @@ -245,22 +242,23 @@ open class TextArea: EntryFieldBase { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- - private func getCharacterCounterText() -> String { + private func getCharacterCounterText() -> String? { let count = textView.text.count let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)" - if ((maxLength ?? 0) > 0) { - if (count > (maxLength ?? 0)) { + if let maxLength, maxLength > 0 { + if count > maxLength { showInternalError = true internalErrorText = "You have exceeded the character limit." return countStr } else { - showInternalError = false internalErrorText = nil - return ("\(countStr)" + "/" + "\(maxLength ?? 0)") + return ("\(countStr)" + "/" + "\(maxLength)") } } else { - return "" + showInternalError = false + internalErrorText = nil + return nil } } @@ -303,7 +301,11 @@ extension TextArea: UITextViewDelegate { } //The exceeding characters will be highlighted to help users correct their entry. - if ((maxLength ?? 0) > 0) { + if let maxLength, maxLength > 0 { + // allow - 20% of character limit + let overflowLimit = Double(maxLength) * 0.20 + let allowCharCount = Int(overflowLimit) + maxLength + if textView.text.count <= allowCharCount { highlightCharacterOverflow() @@ -319,146 +321,5 @@ extension TextArea: UITextViewDelegate { text = textView.text sendActions(for: .valueChanged) } - } - -} - -/// Will move this into a new file, need to talk with Scott/Kyle -open class TextView: UITextView, ViewProtocol { - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - required public init() { - super.init(frame: .zero, textContainer: nil) - initialSetup() - } - - public override init(frame: CGRect, textContainer: NSTextContainer?) { - super.init(frame: frame, textContainer: textContainer) - 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 var isEnabled: Bool = true { didSet { setNeedsUpdate() } } - - 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 { } - } - - override public var text: String! { - get { super.text } - set { - super.text = newValue - updateLabel() - } - } - - override public 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 - updateLabel() - } - } - } - - //-------------------------------------------------- - // MARK: - Lifecycle - //-------------------------------------------------- - open func initialSetup() { - if !initialSetupPerformed { - backgroundColor = .clear - translatesAutoresizingMaskIntoConstraints = false - accessibilityCustomActions = [] - setup() - setNeedsUpdate() - } - } - - - open func setup() { - translatesAutoresizingMaskIntoConstraints = false - } - - open func updateView() { - updateLabel() - } - - open func updateAccessibility() {} - - open func reset() { - shouldUpdateView = false - surface = .light - text = nil - accessibilityCustomActions = [] - shouldUpdateView = true - setNeedsUpdate() - } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - private func updateLabel() { - - //clear the arrays holding actions - accessibilityCustomActions = [] - if let text, !text.isEmpty { - //create the primary string - let mutableText = NSMutableAttributedString.mutableText(for: text, - textStyle: textStyle, - useScaledFont: useScaledFont, - textColor: textColor!, - alignment: textAlignment, - lineBreakMode: .byWordWrapping) - //apply any attributes - if let attributes = textAttributes { - mutableText.apply(attributes: attributes) - } - attributedText = mutableText - } else { - attributedText = nil - } - } - - + } } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift new file mode 100644 index 00000000..cc1567bf --- /dev/null +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -0,0 +1,150 @@ +// +// TextView.swift +// VDS +// +// Created by Matt Bruce on 2/29/24. +// + +import Foundation +import UIKit +import Combine +import VDSColorTokens + +/// Will move this into a new file, need to talk with Scott/Kyle +open class TextView: UITextView, ViewProtocol { + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero, textContainer: nil) + initialSetup() + } + + public override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + 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 var isEnabled: Bool = true { didSet { setNeedsUpdate() } } + + 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 { } + } + + override public var text: String! { + get { super.text } + set { + super.text = newValue + updateLabel() + } + } + + override public 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 + updateLabel() + } + } + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open func initialSetup() { + if !initialSetupPerformed { + backgroundColor = .clear + translatesAutoresizingMaskIntoConstraints = false + accessibilityCustomActions = [] + setup() + setNeedsUpdate() + } + } + + + open func setup() { + translatesAutoresizingMaskIntoConstraints = false + } + + open func updateView() { + updateLabel() + } + + open func updateAccessibility() {} + + open func reset() { + shouldUpdateView = false + surface = .light + text = nil + accessibilityCustomActions = [] + shouldUpdateView = true + setNeedsUpdate() + } + + //-------------------------------------------------- + // MARK: - Private Methods + //-------------------------------------------------- + private func updateLabel() { + + //clear the arrays holding actions + accessibilityCustomActions = [] + if let text, !text.isEmpty { + //create the primary string + let mutableText = NSMutableAttributedString.mutableText(for: text, + textStyle: textStyle, + useScaledFont: useScaledFont, + textColor: textColor!, + alignment: textAlignment, + lineBreakMode: .byWordWrapping) + //apply any attributes + if let attributes = textAttributes { + mutableText.apply(attributes: attributes) + } + attributedText = mutableText + } else { + attributedText = nil + } + } +} +