// // EntryFieldContainer.swift // VDS // // Created by Matt Bruce on 6/28/24. // import Foundation import UIKit import VDSCoreTokens import Combine /// Base Class used to build out a Input controls. @objc(VDSEntryFieldContainer) open class EntryFieldContainer: 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: - Private Properties //-------------------------------------------------- internal var responder: UIResponder? internal var fieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.alignment = .top } }() internal var containerBackgroundColor: UIColor { if showError || hasInternalError { return backgroundColorConfiguration.getColor(self) } else { return transparentBackground ? .clear : backgroundColorConfiguration.getColor(self) } } //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- 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) $0.setSurfaceColors(VDSColor.feedbackErrorBackgroundOnlight, VDSColor.feedbackErrorBackgroundOndark, forState: [.error, .focused]) } internal var borderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderOnlight, VDSFormControlsColor.borderOndark, forState: .normal) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: .focused) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOnlight, forState: [.focused, .error]) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .readonly) } internal let iconColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .error) } internal var readOnlyBorderColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSFormControlsColor.borderReadonlyOnlight, VDSFormControlsColor.borderReadonlyOndark, forState: .normal) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var onChangeSubscriber: AnyCancellable? open var fieldView: UIView? { didSet { if let fieldView { fieldStackView.removeArrangedSubviews() fieldView.translatesAutoresizingMaskIntoConstraints = false //add the view to add input fields fieldStackView.addArrangedSubview(fieldView) fieldStackView.addArrangedSubview(statusIcon) fieldStackView.setCustomSpacing(VDSLayout.space3X, after: fieldView) } } } open var statusIcon: Icon = Icon().with { $0.size = .medium $0.isAccessibilityElement = true } /// Whether not to show the error. open var showError: Bool = false { didSet { setNeedsUpdate() } } /// FormFieldValidator open var validator: (any FormFieldValidatorable)? /// Override UIControl state to add the .error state if showError is true. open override var state: UIControl.State { get { var state = super.state if isEnabled { if !isReadOnly && (showError || hasInternalError){ state.insert(.error) } if isReadOnly { state.insert(.readonly) } if let responder, responder.isFirstResponder { state.insert(.focused) } } return state } } open var titleText: String? { didSet { setNeedsUpdate() } } open var errorText: String? { 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. open var value: String? { get { fatalError("must be read from subclass")} } open var defaultValue: AnyHashable? { didSet { setNeedsUpdate() } } open var isRequired: Bool = false { didSet { setNeedsUpdate() } } open var isReadOnly: Bool = false { didSet { setNeedsUpdate() } } open var rules = [AnyRule]() //-------------------------------------------------- // 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() //add ContainerStackView //this is the horizontal stack that contains //InputContainer, Icons, Buttons addSubview(fieldStackView) fieldStackView.pinToSuperView(.uniform(VDSLayout.space3X)) bridge_accessibilityLabelBlock = { [weak self] in guard let self else { return "" } var accessibilityLabels = [String]() if let text = titleText?.trimmingCharacters(in: .whitespaces) { accessibilityLabels.append(text) } if isReadOnly { accessibilityLabels.append("read only") } if !isEnabled { accessibilityLabels.append("dimmed") } if let errorText, showError { accessibilityLabels.append("error, \(errorText)") } accessibilityLabels.append("\(Self.self)") return accessibilityLabels.joined(separator: ", ") } bridge_accessibilityHintBlock = { [weak self] in guard let self else { return "" } return isReadOnly || !isEnabled ? "" : "Double tap to open" } bridge_accessibilityValueBlock = { [weak self] in guard let self else { return "" } return value } statusIcon.bridge_accessibilityLabelBlock = { [weak self] in guard let self else { return "" } return showError || hasInternalError ? "error" : nil } } /// Updates the UI open override func updateView() { super.updateView() statusIcon.surface = surface updateContainer() updateError() } open func updateContainer() { //container of self backgroundColor = containerBackgroundColor layer.borderColor = borderColorConfiguration.getColor(self).cgColor layer.borderWidth = VDSFormControls.borderWidth layer.cornerRadius = VDSFormControls.borderRadius } open func updateError() { //dealing with error if showError { statusIcon.name = .error statusIcon.isHidden = !isEnabled || state.contains(.focused) } else if hasInternalError { statusIcon.name = .error statusIcon.isHidden = !isEnabled || state.contains(.focused) } else { statusIcon.isHidden = true } statusIcon.isAccessibilityElement = showError statusIcon.color = iconColorConfiguration.getColor(self) } /// Resets to default settings. open override func reset() { super.reset() showError = false errorText = nil transparentBackground = false width = nil inputId = nil defaultValue = nil isRequired = false isReadOnly = false onChange = nil } open override var canBecomeFirstResponder: Bool { responder?.canBecomeFirstResponder ?? super.canBecomeFirstResponder } open override func becomeFirstResponder() -> Bool { responder?.becomeFirstResponder() ?? super.becomeFirstResponder() } open override var canResignFirstResponder: Bool { responder?.canResignFirstResponder ?? super.canResignFirstResponder } open override func resignFirstResponder() -> Bool { responder?.resignFirstResponder() ?? super.resignFirstResponder() } //-------------------------------------------------- // MARK: - Public Methods //-------------------------------------------------- open func validate(){ updateRules() validator = FormFieldValidator(field: self, rules: rules) validator?.validate() setNeedsUpdate() } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- internal func updateRules() { rules.removeAll() if self.isRequired { let rule = RequiredRule() if let errorText, !errorText.isEmpty { rule.errorMessage = errorText } else { rule.errorMessage = "You must enter a value" } rules.append(.init(rule)) } } }