From 68868a35ed547968777968376fd839573ea50e59 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 3 Oct 2022 15:28:18 -0500 Subject: [PATCH] first cut of entry fields Signed-off-by: Matt Bruce --- VDS.xcodeproj/project.pbxproj | 40 ++++ .../TextFields/EntryField/EntryField.swift | 194 ++++++++++++++++++ .../EntryField/EntryFieldModel.swift | 69 +++++++ .../TextEntryField/TextEntryField.swift | 64 ++++++ .../TextEntryField/TextEntryFieldModel.swift | 49 +++++ 5 files changed, 416 insertions(+) create mode 100644 VDS/Components/TextFields/EntryField/EntryField.swift create mode 100644 VDS/Components/TextFields/EntryField/EntryFieldModel.swift create mode 100644 VDS/Components/TextFields/TextEntryField/TextEntryField.swift create mode 100644 VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index a34f8806..edd1fe38 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -59,6 +59,10 @@ EA89201328B568D8006B9984 /* RadioBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89201228B568D8006B9984 /* RadioBox.swift */; }; EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89201428B56CF4006B9984 /* RadioBoxGroup.swift */; }; EA89201728B56CFF006B9984 /* RadioBoxGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89201628B56CFF006B9984 /* RadioBoxGroupModel.swift */; }; + EAA5EEA928EB3ED0003B3210 /* EntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEA828EB3ED0003B3210 /* EntryField.swift */; }; + EAA5EEAB28EB3ED9003B3210 /* EntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEAA28EB3ED9003B3210 /* EntryFieldModel.swift */; }; + EAA5EEB128EB6A5A003B3210 /* TextEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB028EB6A5A003B3210 /* TextEntryFieldModel.swift */; }; + EAA5EEB328EB6A66003B3210 /* TextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB228EB6A66003B3210 /* TextEntryField.swift */; }; EAB1D29A28A5611D00DAE764 /* SelectorGroupModelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29928A5611D00DAE764 /* SelectorGroupModelable.swift */; }; EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */; }; EAB1D29E28A5619500DAE764 /* RadioButtonGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D29D28A5619500DAE764 /* RadioButtonGroupModel.swift */; }; @@ -161,6 +165,10 @@ EA89201228B568D8006B9984 /* RadioBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioBox.swift; sourceTree = ""; }; EA89201428B56CF4006B9984 /* RadioBoxGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioBoxGroup.swift; sourceTree = ""; }; EA89201628B56CFF006B9984 /* RadioBoxGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioBoxGroupModel.swift; sourceTree = ""; }; + EAA5EEA828EB3ED0003B3210 /* EntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = ""; }; + EAA5EEAA28EB3ED9003B3210 /* EntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryFieldModel.swift; sourceTree = ""; }; + EAA5EEB028EB6A5A003B3210 /* TextEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryFieldModel.swift; sourceTree = ""; }; + EAA5EEB228EB6A66003B3210 /* TextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; EAB1D29928A5611D00DAE764 /* SelectorGroupModelable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorGroupModelable.swift; sourceTree = ""; }; EAB1D29B28A5618900DAE764 /* RadioButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; EAB1D29D28A5619500DAE764 /* RadioButtonGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonGroupModel.swift; sourceTree = ""; }; @@ -309,6 +317,7 @@ EA89200B28B530F0006B9984 /* RadioBox */, EAF7F11428A1470D00B287F5 /* RadioButton */, EA1F265F28B945070033E859 /* RadioSwatch */, + EAA5EEA728EB3EBC003B3210 /* TextFields */, EA3361A0288B1E6F0071C351 /* Toggle */, ); path = Components; @@ -440,6 +449,33 @@ path = RadioBox; sourceTree = ""; }; + EAA5EEA728EB3EBC003B3210 /* TextFields */ = { + isa = PBXGroup; + children = ( + EAA5EEAF28EB6A49003B3210 /* TextEntryField */, + EAA5EEAE28EB6A3D003B3210 /* EntryField */, + ); + path = TextFields; + sourceTree = ""; + }; + EAA5EEAE28EB6A3D003B3210 /* EntryField */ = { + isa = PBXGroup; + children = ( + EAA5EEA828EB3ED0003B3210 /* EntryField.swift */, + EAA5EEAA28EB3ED9003B3210 /* EntryFieldModel.swift */, + ); + path = EntryField; + sourceTree = ""; + }; + EAA5EEAF28EB6A49003B3210 /* TextEntryField */ = { + isa = PBXGroup; + children = ( + EAA5EEB228EB6A66003B3210 /* TextEntryField.swift */, + EAA5EEB028EB6A5A003B3210 /* TextEntryFieldModel.swift */, + ); + path = TextEntryField; + sourceTree = ""; + }; EAB1D29F28A598D000DAE764 /* PropertyWrappers */ = { isa = PBXGroup; children = ( @@ -628,6 +664,7 @@ EAF7F11828A1475A00B287F5 /* RadioButtonModel.swift in Sources */, EA89200D28B530FD006B9984 /* RadioBoxModel.swift in Sources */, EA1F266728B945070033E859 /* RadioSwatchModel.swift in Sources */, + EAA5EEB328EB6A66003B3210 /* TextEntryField.swift in Sources */, EA89201328B568D8006B9984 /* RadioBox.swift in Sources */, EA84F6B128B94A2500D67ABC /* CodableColor.swift in Sources */, EA3362402892EF6C0071C351 /* Label.swift in Sources */, @@ -637,6 +674,8 @@ EAF7F0AF289B144C00B287F5 /* LabelAttributeUnderline.swift in Sources */, EA3361C5289030FC0071C351 /* Accessable.swift in Sources */, EA33622C2891E73B0071C351 /* FontProtocol.swift in Sources */, + EAA5EEAB28EB3ED9003B3210 /* EntryFieldModel.swift in Sources */, + EAA5EEA928EB3ED0003B3210 /* EntryField.swift in Sources */, EAF7F11728A1475A00B287F5 /* RadioButton.swift in Sources */, EAB1D2A128A598FE00DAE764 /* UsesAutoLayout.swift in Sources */, EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, @@ -666,6 +705,7 @@ EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */, EAB1D29A28A5611D00DAE764 /* SelectorGroupModelable.swift in Sources */, EAF7F0BB289D80ED00B287F5 /* Modelable.swift in Sources */, + EAA5EEB128EB6A5A003B3210 /* TextEntryFieldModel.swift in Sources */, EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */, EA4DB30028DCBC9900103EE3 /* BadgeModel.swift in Sources */, EAF7F09E289AAEC000B287F5 /* Constants.swift in Sources */, diff --git a/VDS/Components/TextFields/EntryField/EntryField.swift b/VDS/Components/TextFields/EntryField/EntryField.swift new file mode 100644 index 00000000..21fe9c1c --- /dev/null +++ b/VDS/Components/TextFields/EntryField/EntryField.swift @@ -0,0 +1,194 @@ +// +// EntryField.swift +// VDS +// +// Created by Matt Bruce on 10/3/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import Combine + +open class EntryField: Control { + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private var stackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .vertical + $0.distribution = .fill + } + }() + + private var containerStackView: UIStackView = { + return UIStackView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.axis = .horizontal + $0.distribution = .fill + $0.spacing = 12 + } + }() + + private var titleLabel = Label() + private var errorLabel = Label() + private var helperLabel = Label() + private var successLabel = Label() + + internal var containerView: UIView = { + return UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + }() + + //-------------------------------------------------- + // MARK: - Configuration Properties + //-------------------------------------------------- + // Sizes are from InVision design specs. + public let containerSize = CGSize(width: 45, height: 45) + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + @Proxy(\.model.labelText) + open var labelText: String? + + @Proxy(\.model.helperText) + open var helperText: String? + + @Proxy(\.model.showError) + open var showError: Bool + + @Proxy(\.model.errorText) + open var errorText: String? + + @Proxy(\.model.showSuccess) + open var showSuccess: Bool + + @Proxy(\.model.successText) + open var successText: String? + + @Proxy(\.model.helperTextPlacement) + open var helperTextPlacement: HelperTextPlacement + + @Proxy(\.model.transparentBackground) + open var transparentBackground: Bool + + @Proxy(\.model.width) + open var width: CGFloat? + + @Proxy(\.model.maxLength) + open var maxLength: Int? + + @Proxy(\.model.inputId) + open var inputId: String? + + @Proxy(\.model.value) + open var value: AnyHashable? + + @Proxy(\.model.defaultVaue) + open var defaultValue: AnyHashable? + + @Proxy(\.model.required) + open var required: Bool + + @Proxy(\.model.readOnly) + open var readOnly: Bool + + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + internal var heightConstraint: NSLayoutConstraint? + internal var widthConstraint: NSLayoutConstraint? + internal var minWidthConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func setup() { + super.setup() + + //add tapGesture to self + publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in + self?.sendActions(for: .touchUpInside) + }.store(in: &subscribers) + + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(stackView) + + //create the wrapping view + containerView.widthAnchor.constraint(equalToConstant: containerSize.width).isActive = true + containerView.heightAnchor.constraint(equalToConstant: containerSize.height).isActive = true + + heightConstraint = containerView.heightAnchor.constraint(equalToConstant: containerSize.height) + heightConstraint?.isActive = true + + widthConstraint = containerView.widthAnchor.constraint(equalToConstant: containerSize.width) + + minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) + minWidthConstraint?.isActive = true + + containerStackView.addArrangedSubview(containerView) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(containerStackView) + stackView.addArrangedSubview(errorLabel) + stackView.addArrangedSubview(successLabel) + + stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true + stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + } + + public override func reset() { + super.reset() + setAccessibilityLabel() + } + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + open override func updateView(viewModel: ModelType) { + + titleLabel.set(with: viewModel.labelModel) + + //show error or success + if viewModel.showError, let errorLabelModel = viewModel.errorLabelModel { + errorLabel.set(with: errorLabelModel) + errorLabel.isHidden = false + successLabel.isHidden = true + } else if viewModel.showSuccess, let successLabelModel = viewModel.successLabelModel { + successLabel.set(with: successLabelModel) + errorLabel.isHidden = true + successLabel.isHidden = false + } else { + errorLabel.isHidden = true + successLabel.isHidden = true + } + + //set the helper label position + if let helperLabelModel = viewModel.helperLabelModel { + helperLabel.removeFromSuperview() + if viewModel.helperTextPlacement == .right { + containerStackView.spacing = 12 + containerStackView.addArrangedSubview(helperLabel) + } else { + containerStackView.spacing = 0 + stackView.addArrangedSubview(helperLabel) + } + helperLabel.set(with: helperLabelModel) + helperLabel.isHidden = false + } else { + helperLabel.isHidden = true + } + + setAccessibilityHint(!viewModel.disabled) + backgroundColor = viewModel.surface.color + setNeedsLayout() + layoutIfNeeded() + } +} diff --git a/VDS/Components/TextFields/EntryField/EntryFieldModel.swift b/VDS/Components/TextFields/EntryField/EntryFieldModel.swift new file mode 100644 index 00000000..f2fd8a9b --- /dev/null +++ b/VDS/Components/TextFields/EntryField/EntryFieldModel.swift @@ -0,0 +1,69 @@ +// +// EntryFieldModel.swift +// VDS +// +// Created by Matt Bruce on 10/3/22. +// + +import Foundation + +public enum HelperTextPlacement: String, CaseIterable { + case bottom, right +} + +public protocol EntryFieldModel: Modelable, FormFieldable, Errorable { + var defaultVaue: AnyHashable? { get set } + var required: Bool { get set } + var readOnly: Bool { get set } + var labelText: String? { get set } + var helperText: String? { get set } + var helperTextPlacement: HelperTextPlacement { get set } + var showSuccess: Bool { get set } + var successText: String? { get set } + var transparentBackground: Bool { get set } + var width: CGFloat? { get set } + var maxLength: Int? { get set } +} + +extension EntryFieldModel { + + public var labelModel: DefaultLabelModel { + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = labelText ?? "" + model.surface = surface + model.disabled = disabled + return model + } + + public var helperLabelModel: DefaultLabelModel? { + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = helperText + model.surface = surface + model.disabled = disabled + return model + } + + public var errorLabelModel: DefaultLabelModel? { + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = errorText + model.surface = surface + model.disabled = disabled + return model + } + + public var successLabelModel: DefaultLabelModel? { + var model = DefaultLabelModel() + model.textPosition = .left + model.typograpicalStyle = .BodySmall + model.text = successText + model.surface = surface + model.disabled = disabled + return model + } +} diff --git a/VDS/Components/TextFields/TextEntryField/TextEntryField.swift b/VDS/Components/TextFields/TextEntryField/TextEntryField.swift new file mode 100644 index 00000000..fb2255cc --- /dev/null +++ b/VDS/Components/TextFields/TextEntryField/TextEntryField.swift @@ -0,0 +1,64 @@ +// +// TextEntryField.swift +// VDS +// +// Created by Matt Bruce on 10/3/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import Combine + +public class TextEntryField: TextEntryFieldBase{} + +open class TextEntryFieldBase: EntryField { + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + + @Proxy(\.model.type) + public var type: TextEntryFieldType + + + //-------------------------------------------------- + // MARK: - State + //-------------------------------------------------- + open override func updateView(viewModel: ModelType) { + super.updateView(viewModel: viewModel) + containerView.backgroundColor = .red + + //set the width constraints + if let width = viewModel.width, width > viewModel.type.width { + widthConstraint?.constant = width + widthConstraint?.isActive = true + minWidthConstraint?.isActive = false + } else { + minWidthConstraint?.constant = viewModel.type.width + widthConstraint?.isActive = false + minWidthConstraint?.isActive = true + } + } +} + +extension TextEntryFieldType { + var width: CGFloat { + switch self { + case .inlineAction: + return 102 + case .password: + return 62.0 + case .creditCard: + return 288.0 + case .tel: + return 176.0 + case .date: + return 114.0 + case .securityCode: + return 88.0 + default: + return 40.0 + } + } +} diff --git a/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift b/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift new file mode 100644 index 00000000..e50947ec --- /dev/null +++ b/VDS/Components/TextFields/TextEntryField/TextEntryFieldModel.swift @@ -0,0 +1,49 @@ +// +// TextEntryFieldModel.swift +// VDS +// +// Created by Matt Bruce on 10/3/22. +// + +import Foundation + +public enum TextEntryFieldType: String, CaseIterable { + case text, number, calendar, inlineAction, password, creditCard, tel, date, securityCode +} + +public protocol TextEntryFieldModel: EntryFieldModel { + var type: TextEntryFieldType { get set } +} + + +public struct DefaultTextEntryField: TextEntryFieldModel { + public var id = UUID() + public var inputId: String? + + public var type: TextEntryFieldType = .text + public var value: AnyHashable? + public var defaultVaue: AnyHashable? + public var required: Bool = false + public var readOnly: Bool = false + + public var labelText: String? + + public var helperText: String? + public var helperTextPlacement: HelperTextPlacement = .bottom + + public var showError: Bool = false + public var errorText: String? + + public var showSuccess: Bool = false + public var successText: String? + + public var transparentBackground: Bool = false + public var width: CGFloat? + public var maxLength: Int? + + + public var surface: Surface = .light + public var disabled: Bool = false + + public init() { } +}