// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSTokens import Combine /// Base Class used to build out a Input controls. @objc(VDSEntryField) open class EntryFieldBase: Control, Changeable, FormFieldInternalValidatable { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- /// Enum used to describe the position of the helper text. public enum HelperTextPlacement: String, CaseIterable { case bottom, right } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var containerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.alignment = .top } }() internal var controlContainerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var bottomContainerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var bottomContainerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal var containerSize: CGSize { CGSize(width: 45, height: 44) } internal let primaryColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) } internal let secondaryColorConfiguration = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsSecondaryOnlight, VDSColor.elementsSecondaryOndark, forDisabled: false) } internal var backgroundColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .normal) $0.setSurfaceColors(VDSFormControlsColor.backgroundOnlight, VDSFormControlsColor.backgroundOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: .error) } internal var borderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.disabled,.error]) } internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var onChangeSubscriber: AnyCancellable? { willSet { if let onChangeSubscriber { onChangeSubscriber.cancel() } } } open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall } open var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall } open var helperLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall } open var icon: Icon = Icon().with { $0.size = .small } open var labelText: String? { didSet { setNeedsUpdate() } } open var helperText: String? { didSet { setNeedsUpdate() } } /// Whether not to show the error. open var showError: Bool = false { didSet { setNeedsUpdate() } } /// FormFieldValidator internal var validator: (any FormFieldValidatorable)? /// Whether or not to show the internal error open var hasInternalError: Bool { !(validator?.isValid ?? true) } /// Override UIControl state to add the .error state if showError is true. open override var state: UIControl.State { get { var state = super.state if showError || hasInternalError { state.insert(.error) } return state } } open var errorText: String? { didSet { updateContainerView() updateErrorLabel() setNeedsUpdate() } } open var internalErrorText: String? { didSet { updateContainerView() updateErrorLabel() setNeedsUpdate() } } open var tooltipModel: Tooltip.TooltipModel? { didSet { setNeedsUpdate() } } open var transparentBackground: Bool = false { didSet { setNeedsUpdate() } } open var width: CGFloat? { didSet { setNeedsUpdate() } } open var inputId: String? { didSet { setNeedsUpdate() } } /// The text of this textField. internal var _value: String? open var value: String? { get { _value } set { if let newValue, newValue != _value { _value = newValue sendActions(for: .valueChanged) } } } open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } } open var required: Bool = false { didSet { setNeedsUpdate() } } open var readOnly: Bool = false { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() isAccessibilityElement = true addSubview(stackView) //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) heightConstraint?.priority = .defaultHigh heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) widthConstraint?.priority = .defaultHigh //get the container this is what is color //border, internal, etc... let container = getContainer() //add ContainerStackView //this is the horizontal stack that contains //the left, InputContainer, Icons, Buttons container.addSubview(containerStackView) containerStackView.pinToSuperView(.uniform(12)) //add the view to add input fields containerStackView.addArrangedSubview(controlContainerView) containerStackView.addArrangedSubview(icon) containerStackView.setCustomSpacing(VDSLayout.space3X, after: controlContainerView) //get the container this is what show helper text, error text //can include other for character count, max length let bottomContainer = getBottomContainer() //add bottomContainerStackView //this is the vertical stack that contains error text, helper text bottomContainer.addSubview(bottomContainerStackView) bottomContainerStackView.pinToSuperView() bottomContainerStackView.addArrangedSubview(errorLabel) bottomContainerStackView.addArrangedSubview(helperLabel) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) stackView.addArrangedSubview(bottomContainer) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: bottomContainer) stackView .pinTop() .pinLeading() .pinTrailing(0, .defaultHigh) .pinBottom(0, .defaultHigh) titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable() } /// Resets to default settings. open override func reset() { super.reset() titleLabel.reset() errorLabel.reset() helperLabel.reset() titleLabel.textStyle = .bodySmall errorLabel.textStyle = .bodySmall helperLabel.textStyle = .bodySmall labelText = nil helperText = nil showError = false errorText = nil tooltipModel = nil transparentBackground = false width = nil inputId = nil value = nil defaultValue = nil required = false readOnly = false } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() updateContainerView() updateTitleLabel() updateErrorLabel() updateHelperLabel() backgroundColor = surface.color validator?.validate() internalErrorText = validator?.errorMessage } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func updateContainerView() { containerView.backgroundColor = backgroundColorConfiguration.getColor(self) containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor containerView.layer.borderWidth = VDSFormControls.borderWidth containerView.layer.cornerRadius = VDSFormControls.borderRadius } //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- /// Container for the area in which the user interacts. open func getContainer() -> UIView { return containerView } /// Container for the area in which helper or error text presents. open func getBottomContainer() -> UIView { return bottomContainerView } open func updateTitleLabel() { //update the local vars for the label since we no //long have a model var attributes: [any LabelAttributeModel] = [] var updatedLabelText = labelText //dealing with the "Optional" addition to the text if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { if isEnabled { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, color: VDSColor.elementsSecondaryOnlight) attributes.append(optionColorAttr) } updatedLabelText = "\(oldText) Optional" } if let tooltipModel { attributes.append(TooltipLabelAttribute(surface: surface, model: tooltipModel, presenter: self)) } //set the titleLabel titleLabel.text = updatedLabelText titleLabel.attributes = attributes titleLabel.surface = surface titleLabel.isEnabled = isEnabled } open func updateErrorLabel(){ if showError, hasInternalError, let errorText, let internalErrorText { errorLabel.text = [internalErrorText, errorText].joined(separator: "\n") errorLabel.surface = surface errorLabel.isEnabled = isEnabled errorLabel.isHidden = false icon.name = .error icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = !isEnabled } else if showError, let errorText { errorLabel.text = errorText errorLabel.surface = surface errorLabel.isEnabled = isEnabled errorLabel.isHidden = false icon.name = .error icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = !isEnabled } else if hasInternalError, let internalErrorText { errorLabel.text = internalErrorText errorLabel.surface = surface errorLabel.isEnabled = isEnabled errorLabel.isHidden = false icon.name = .error icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = !isEnabled } else { icon.isHidden = true errorLabel.isHidden = true } } open func updateHelperLabel(){ //set the helper label position if let helperText { helperLabel.text = helperText helperLabel.surface = surface helperLabel.isEnabled = isEnabled helperLabel.isHidden = false } else { helperLabel.isHidden = true } } }