// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSEntryField) open class EntryField: Control, Changeable { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum HelperTextPlacement: String, CaseIterable { case bottom, right } //-------------------------------------------------- // 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 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 = .fillProportionally $0.alignment = .top } }() internal var controlContainerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() //-------------------------------------------------- // 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.borderOnlight, forState: .normal) $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: .disabled) $0.setSurfaceColors(VDSColor.feedbackErrorOnlight, VDSColor.feedbackErrorOndark, forState: .error) } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var onChangeSubscriber: AnyCancellable? { willSet { if let onChangeSubscriber { onChangeSubscriber.cancel() } } } open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.textStyle = .bodySmall } open var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.textStyle = .bodySmall } open var helperLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.textStyle = .bodySmall } open var tooltipView: UIView? open var icon: Icon = Icon().with { $0.size = .small } open var labelText: String? { didSet { setNeedsUpdate() }} open var helperText: String? { didSet { setNeedsUpdate() }} open var showError: Bool = false { didSet { setNeedsUpdate() }} open override var state: UIControl.State { get { var state = super.state if showError { state.insert(.error) } return state } } open var errorText: String? { didSet { setNeedsUpdate() }} open var tooltipTitle: String? { didSet { setNeedsUpdate() }} open var tooltipContent: String? { didSet { setNeedsUpdate() }} open var transparentBackground: Bool = false { didSet { setNeedsUpdate() }} open var width: CGFloat? { didSet { setNeedsUpdate() }} open var maxLength: Int? { didSet { setNeedsUpdate() }} open var inputId: String? { didSet { setNeedsUpdate() }} open var value: AnyHashable? { didSet { setNeedsUpdate() }} 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: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() isAccessibilityElement = true accessibilityTraits = .button addSubview(stackView) //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) //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(.init(top: 12, left: 12, bottom: 12, right: 12)) //add the view to add input fields containerStackView.addArrangedSubview(controlContainerView) containerStackView.addArrangedSubview(icon) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) stackView.addArrangedSubview(errorLabel) stackView.addArrangedSubview(helperLabel) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) stackView .pinTop() .pinBottom() .pinLeading() .trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor).isActive = true titleLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfiguration.eraseToAnyColorable() } open func getContainer() -> UIView { return containerView } open override func reset() { super.reset() titleLabel.reset() errorLabel.reset() helperLabel.reset() titleLabel.textPosition = .left titleLabel.textStyle = .bodySmall errorLabel.textPosition = .left errorLabel.textStyle = .bodySmall helperLabel.textPosition = .left helperLabel.textStyle = .bodySmall labelText = nil helperText = nil showError = false errorText = nil tooltipTitle = nil tooltipContent = nil transparentBackground = false width = nil maxLength = nil inputId = nil value = nil defaultValue = nil required = false readOnly = false } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { containerView.backgroundColor = backgroundColorConfiguration.getColor(self) containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderWidth = VDSFormControls.widthBorder containerView.layer.cornerRadius = VDSFormControls.borderradius updateTitleLabel() updateErrorLabel() updateHelperLabel() backgroundColor = surface.color } 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 !disabled { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, color: VDSColor.elementsSecondaryOnlight) attributes.append(optionColorAttr) } updatedLabelText = "\(oldText) Optional" } if let tooltipTitle, let tooltipContent { attributes.append(TooltipLabelAttribute(surface: surface, title: tooltipTitle, content: tooltipContent)) } //set the titleLabel titleLabel.text = updatedLabelText titleLabel.attributes = attributes titleLabel.surface = surface titleLabel.disabled = disabled } open func updateErrorLabel(){ if showError, let errorText { errorLabel.text = errorText errorLabel.surface = surface errorLabel.disabled = disabled errorLabel.isHidden = false icon.name = .error icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = disabled } 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.disabled = disabled helperLabel.isHidden = false } else { helperLabel.isHidden = true } } }