diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 83e2b9a1..5c31b47b 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */; }; 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */; }; 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */; }; + 0A6BF4722360C56C0028F841 /* DropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6BF4712360C56C0028F841 /* DropdownEntryField.swift */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; }; 9455B19C234F8A0400A574DB /* MVMAnimationFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */; }; 948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948DB67D2326DCD90011F916 /* MultiProgress.swift */; }; @@ -217,6 +218,7 @@ 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitEntryField.swift; sourceTree = ""; }; 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CATransaction+Extension.swift"; sourceTree = ""; }; 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; + 0A6BF4712360C56C0028F841 /* DropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownEntryField.swift; sourceTree = ""; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxWithLabelView.swift; sourceTree = ""; }; 0A8321A72355062F00CB7F00 /* MdnTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnTextField.swift; sourceTree = ""; }; @@ -770,6 +772,7 @@ 0A21DB80235DF87300C160A2 /* TextField.swift */, 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */, 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */, + 0A6BF4712360C56C0028F841 /* DropdownEntryField.swift */, ); path = TextFields; sourceTree = ""; @@ -1125,6 +1128,7 @@ D29770FC21F7C77400B2F0D0 /* MVMCoreUITextFieldView.m in Sources */, DBC4391B224421A0001AB423 /* CaretButton.swift in Sources */, 0198F7A82256A80B0066C936 /* MFRadioButton.m in Sources */, + 0A6BF4722360C56C0028F841 /* DropdownEntryField.swift in Sources */, 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */, D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */, D29DF29C21E7ADB9003B2FB9 /* MFProgrammaticTableViewController.m in Sources */, diff --git a/MVMCoreUI/Atoms/TextFields/DropdownEntryField.swift b/MVMCoreUI/Atoms/TextFields/DropdownEntryField.swift new file mode 100644 index 00000000..005c5e8e --- /dev/null +++ b/MVMCoreUI/Atoms/TextFields/DropdownEntryField.swift @@ -0,0 +1,483 @@ +// +// DropdownEntryField.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 10/23/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import UIKit + + +@objcMembers open class DropdownEntryField: TextEntryField { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + private var calendar: Calendar? + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public var dropDownCaretLabel: UILabel? + + public var enabledTextColor: UIColor? + public var disabledTextColor: UIColor? + + public var observingForChanges = false + + private var borderPath: UIBezierPath? + + public override var isEnabled: Bool { + didSet { + + } + } + + // The text of this textField. + public override var text: String? { + get { return textField?.text } + set { + textField?.text = newValue + valueChanged() + } + } + + public var validationBlock: ((_ enteredValue: String?) -> Bool)? { + didSet { + valueChanged() + } + } + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + public var dropDownCaretWidth: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + /// Basic initializer. + public convenience init() { + self.init(frame: .zero) + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + fatalError("init(coder:) has not been implemented") + } + + /// - parameter bothDelegates: Sets both MF/UI Text Field Delegates. + public init(bothDelegates: (UITextFieldDelegate & TextFieldDelegate)?) { + super.init(frame: .zero) + setupView() + setBothTextFieldDelegates(bothDelegates) + } + + /// - parameter hasDropDown: tbd + /// - parameter map: Dictionary of values to setup this TextField + /// - parameter bothDelegate: Sets both MF/UI Text Field Delegates. + public init(hasDropDown: Bool = false, map: [AnyHashable: Any]?, bothDelegates: (UITextFieldDelegate & TextFieldDelegate)?) { + super.init(frame: .zero) + setupView() + dropDownCaretLabel?.isHidden = hasDropDown + self.hasDropDown = !hasDropDown + setWithMap(map, bothDelegates: bothDelegates) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func setupFieldContainerContent(_ container: UIView) { + + let textField = UITextField(frame: .zero) + self.textField = textField + textField.translatesAutoresizingMaskIntoConstraints = false + textField.setContentCompressionResistancePriority(.required, for: .vertical) + textField.heightAnchor.constraint(equalToConstant: 24).isActive = true + textField.font = MFStyler.fontForTextField() + textField.smartQuotesType = .no + textField.smartDashesType = .no + textField.smartInsertDeleteType = .no + MFStyler.styleTextField(textField) + + container.addSubview(textField) + + NSLayoutConstraint.activate([ + textField.topAnchor.constraint(equalTo: container.topAnchor, constant: 10), + textField.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16), + container.bottomAnchor.constraint(equalTo: textField.bottomAnchor, constant: 10)]) + + let dropDownCaretLabel = Label() + self.dropDownCaretLabel = dropDownCaretLabel + dropDownCaretLabel.setContentHuggingPriority(UILayoutPriority(900), for: .horizontal) + dropDownCaretLabel.setContentHuggingPriority(UILayoutPriority(251), for: .vertical) + dropDownCaretLabel.setContentCompressionResistancePriority(UILayoutPriority(900), for: .horizontal) + dropDownCaretLabel.isHidden = true + dropDownCaretLabel.isUserInteractionEnabled = true + let tapOnCarrot = UITapGestureRecognizer(target: self, action: #selector(startEditing)) + dropDownCaretLabel.addGestureRecognizer(tapOnCarrot) + + container.addSubview(dropDownCaretLabel) + + dropDownCaretLabel.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + dropDownCaretLabel.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: 6).isActive = true + container.trailingAnchor.constraint(equalTo: dropDownCaretLabel.trailingAnchor, constant: 16).isActive = true + container.bottomAnchor.constraint(equalTo: dropDownCaretLabel.bottomAnchor).isActive = true + dropDownCarrotWidth = dropDownCaretLabel.widthAnchor.constraint(equalToConstant: 0) + dropDownCarrotWidth?.isActive = true + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + + if let textField = textField { + MFStyler.styleTextField(textField) + } + + layoutIfNeeded() + } + + deinit { + mfTextFieldDelegate = nil + uiTextFieldDelegate = nil + } + + open override func draw(_ rect: CGRect) { + super.draw(rect) + + borderPath?.removeAllPoints() + + if !hideBorder, let frame = fieldContainer?.frame { + + borderPath = UIBezierPath() + borderPath?.move(to: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height)) + borderPath?.addLine(to: CGPoint(x: frame.origin.x, y: frame.origin.y)) + borderPath?.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y)) + borderPath?.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height)) + borderPath?.lineWidth = 1 + + let strokeColor = showError ? UIColor.mfPumpkin() : UIColor.mfSilver() + strokeColor.setStroke() + + borderPath?.stroke() + } + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + public func showDropDown(_ show: Bool) { + + if hasDropDown { + dropDownCaretLabel?.isHidden = !show + dropDownCarrotWidth?.isActive = !show + setNeedsLayout() + layoutIfNeeded() + } + } + + open override func showErrorMessage(_ errorMessage: String?) { + super.showErrorMessage(errorMessage) + + textField?.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textfield_error_message") ?? "", textField?.text ?? "", errorMessage ?? "") + } + + open override func hideError() { + super.hideError() + + textField?.accessibilityValue = nil + } + + public func setBothTextFieldDelegates(_ delegate: (UITextFieldDelegate & TextFieldDelegate)?) { + + mfTextFieldDelegate = delegate + uiTextFieldDelegate = delegate + } + + public override func setWithMap(_ map: [AnyHashable: Any]?) { + super.setWithMap(map) + + guard let map = map, !map.isEmpty else { return } + + if let formText = map[KeyLabel] as? String { + self.formText = formText + } + + if let text = map[KeyValue] as? String { + self.text = text + } + + if let text = map[KeyDisable] as? String, text.isEqual(StringY) || map.boolForKey(KeyDisable) { + formIsDisabled() + } + + if let dropDown = map[KeyType] as? String { + dropDownCaretLabel?.isHidden = false + self.hasDropDown = true + } + + // Key used to send text value to server + if let fieldKey = map[KeyFieldKey] as? String { + self.fieldKey = fieldKey + } + + switch map.stringForkey(KeyType) { + case "dropDown": + dropDownCaretLabel?.isHidden = false + self.hasDropDown = true + + case "password": + textField?.isSecureTextEntry = true + + case "number": + textField?.keyboardType = .numberPad + + case "email": + textField?.keyboardType = .emailAddress + + default: + break + } + + let regex = map.stringForkey("regex") + + if !regex.isEmpty { + validationBlock = { enteredValue in + guard let value = enteredValue else { return false } + return MVMCoreUIUtility.validate(value, withRegularExpression: regex) + } + } else { + defaultValidationBlock() + } + } + + public func setWithMap(_ map: [AnyHashable: Any]?, bothDelegates delegate: (UITextFieldDelegate & TextFieldDelegate)?) { + + guard let textField = textField else { return } + + MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: delegate) + setBothTextFieldDelegates(delegate) + setWithMap(map) + } + + public func defaultValidationBlock() { + + validationBlock = { enteredValue in + return (enteredValue?.count ?? 0) > 0 + } + } + + open override func formIsEnabled() { + super.formIsEnabled() + + textField?.isUserInteractionEnabled = true + textField?.isEnabled = true + showDropDown(true) + } + + open override func formIsDisabled() { + super.formIsDisabled() + + textField?.isUserInteractionEnabled = false + textField?.isEnabled = false + self.showDropDown(false) + } + + //-------------------------------------------------- + // MARK: - Observing for change + //-------------------------------------------------- + + func valueChanged() { + + // Update label for placeholder + if !showError { + feedbackLabel?.text = "" + } + + let previousValidity = isValid + + // If validation not set, input will always be valid + isValid = validationBlock?(text) ?? true + + if previousValidity && !isValid { + if let errMessage = errorMessage { + showErrorMessage(errMessage) + } + + if let mfTextFieldDelegate = mfTextFieldDelegate { + mfTextFieldDelegate.isInvalid?(textfield: self) + } + } else if !previousValidity && isValid { + hideError() + + if let mfTextFieldDelegate = mfTextFieldDelegate { + mfTextFieldDelegate.isValid?(textfield: self) + } + } + } + + func endInputing() { + + if isValid { + hideError() + separatorView?.backgroundColor = .black + } else if let errMessage = errorMessage { + showErrorMessage(errMessage) + } + } + + func startEditing() { + + textField?.becomeFirstResponder() + showErrorDropdown(!showError) + } + + class func getEnabledTextfields(_ textFieldToDetermine: [TextEntryField]?) -> [AnyHashable]? { + + var enabledTextFields = [AnyHashable]() + + for textfield in textFieldToDetermine ?? [] { + if textfield.isEnabled { + enabledTextFields.append(textfield) + } + } + + return enabledTextFields + } +} + +// MARK: - Date Picker +extension TextEntryField { + + private func createDatePicker() { + + guard let textField = textField else { return } + + MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: textField.delegate) + datePicker = MVMCoreUICommonViewsUtility.addDatePicker(to: textField) + + var calendar: Calendar = .current + calendar.timeZone = NSTimeZone.system + self.calendar = calendar + } + + public func inputFromDatePicker(from fromDate: Date?, to toDate: Date?, showFromDateAsDefaultInput show: Bool) { + + createDatePicker() + + if show, let fromDate = fromDate { + if let calendar = calendar, calendar.isDate(fromDate, inSameDayAs: Date()) { + text = MVMCoreUIUtility.hardcodedString(withKey: "textfield_today_string") + } else { + self.text = formatter.string(from: fromDate) + } + } + + datePicker?.minimumDate = fromDate + datePicker?.maximumDate = toDate + } + + public func setDatePickerFrom(_ fromDate: Date?, to toDate: Date?) { + + if let fromDate = fromDate { + if let calendar = calendar, calendar.isDate(fromDate, inSameDayAs: Date()) { + text = MVMCoreUIUtility.hardcodedString(withKey: "textfield_today_string") + } else { + text = formatter.string(from: fromDate) + } + } + + datePicker?.minimumDate = fromDate + datePicker?.maximumDate = toDate + datePicker?.timeZone = NSTimeZone.system + } + + public func dismissDatePicker() -> Date? { + + let pickedDate = datePicker?.date + + if let pickedDate = pickedDate { + if let calendar = calendar, calendar.isDate(pickedDate, inSameDayAs: Date()) { + text = MVMCoreUIUtility.hardcodedString(withKey: "textfield_today_string") + } else { + text = formatter.string(from: pickedDate) + } + } + + textField?.resignFirstResponder() + return pickedDate + } + + public func dismissPicker() { + + textField?.resignFirstResponder() + } +} + +// MARK: - Molecular +extension TextEntryField { + + override open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) + + if let delegateObject = delegateObject { + FormValidator.setupValidation(molecule: self, delegate: delegateObject.formValidationProtocol) + setWithMap(json) + + if let formValidationProtocol = delegateObject.formValidationProtocol { + mfTextFieldDelegate = FormValidator.getFormValidatorFor(delegate: formValidationProtocol) + } + + uiTextFieldDelegate = delegateObject.uiTextFieldDelegate + + if let textField = textField { + MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: uiTextFieldDelegate) + } + } + } + + override open class func estimatedHeight(forRow json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { + return 76 + } +} + +// MARK: - Accessibility +extension TextEntryField { + + open override func pushAccessibilityNotification() { + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + UIAccessibility.post(notification: .layoutChanged, argument: self.textField) + } + } + + open override func setAccessibilityString(_ accessibilityString: String?) { + + guard let textField = textField else { return } + + var accessibilityString = accessibilityString ?? "" + + if hasDropDown, let txtPickerItem = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item") { + accessibilityString += txtPickerItem + + } else if let txtRegular = MVMCoreUIUtility.hardcodedString(withKey: "textfield_regular") { + accessibilityString += txtRegular + } + + textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" + } + +} diff --git a/MVMCoreUI/Atoms/TextFields/FormEntryField.swift b/MVMCoreUI/Atoms/TextFields/FormEntryField.swift index f3fce186..1f92bcd7 100644 --- a/MVMCoreUI/Atoms/TextFields/FormEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/FormEntryField.swift @@ -8,7 +8,6 @@ import UIKit - /** * This class is intended to be subclassed by a class that will add views subclassed under UIControl. * The FieldEntryForm provides the base logic for the description label, placeholder/error label and field container. @@ -18,13 +17,13 @@ import UIKit // MARK: - Outlets //-------------------------------------------------- + private(set) var descriptionLabel: Label? + private(set) var feedbackLabel: Label? + private(set) var fieldContainer: UIView? + public var backgroundView: UIView? - public var formDescriptionLabel: Label? - public var fieldContainer: UIView? - public var placeholderErrorLabel: Label? public var separatorView: UIView? public var dashLine: DashLine? - public var dropDownCaretLabel: UILabel? //-------------------------------------------------- // MARK: - Accessories @@ -38,43 +37,67 @@ import UIKit // MARK: - Properties //-------------------------------------------------- - public var showError = false - public var hasDropDown = false + public var isValid = false + public var fieldKey: String? + private var borderPath: UIBezierPath? - public var isEnabled = true + + public var errorMessage: String? + public var showErrorMessage = false + + public var isEnabled = true { + didSet { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.isUserInteractionEnabled = self.isEnabled + self.descriptionLabel?.textColor = self.isEnabled ? UIColor.mfBattleshipGrey() : UIColor.mfSilver() + self.feedbackLabel?.textColor = self.isEnabled ? .black : UIColor.mfSilver() + self.separatorView?.backgroundColor = self.showErrorMessage ? UIColor.mfPumpkin() : .black + } + } + } /// Determines if a border should be drawn. public var hideBorder = false { - didSet { setNeedsLayout() } + didSet { setNeedsDisplay() } } - public var formText: String? { - get { return formDescriptionLabel?.text } + public var descriptionText: String? { + get { return descriptionLabel?.text } set { - formDescriptionLabel?.text = newValue + descriptionLabel?.text = newValue setAccessibilityString(newValue) } } - // Override this with logic of the textfield(s) that are of focus in this form. + /// Override this to conveniently get/set the textfield(s). public var text: String? { - get { return "" } - set { } + get { return nil } + set { + fatalError("You're supposed to override FormEntryField's 'text' variable.") + } } - public var placeholderTextColor: UIColor = .black - - /// Setgs placeholder text in the textField. - public var placeholder: String? { - get { return placeholderErrorLabel?.text } + /// Sets feedback text in the textField. + public var feedback: String? { + get { return feedbackLabel?.text } set { - guard let newPlaceholderText = newValue else { return } + guard isEnabled, + let newFeedback = newValue + else { return } - if !showError { - placeholderErrorLabel?.text = newPlaceholderText + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.feedbackLabel?.text = newFeedback + self.separatorHeightConstraint?.constant = self.showErrorMessage ? 4 : 1 + self.separatorView?.backgroundColor = self.showErrorMessage ? UIColor.mfPumpkin() : .black + self.setNeedsDisplay() + self.layoutIfNeeded() } - setAccessibilityString(newPlaceholderText) + setAccessibilityString(newFeedback) } } @@ -89,30 +112,18 @@ import UIKit return formatter }() - public var isValid = false - public var fieldKey: String? - - public var enabledTextColor: UIColor? - public var disabledTextColor: UIColor? - - public var errorMessage: String? - //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- - public var heightConstraint: NSLayoutConstraint? + public var textContainerLeading: NSLayoutConstraint? + public var textContainerTrailing: NSLayoutConstraint? - public var dropDownCarrotWidth: NSLayoutConstraint? + public var errorLabelTrailing: NSLayoutConstraint? + public var errorLabelLeading: NSLayoutConstraint? - public var textContainerLeftPin: NSLayoutConstraint? - public var textContainerRightPin: NSLayoutConstraint? - - public var errorLableRightPin: NSLayoutConstraint? - public var errorLableLeftPin: NSLayoutConstraint? - - public var formDescriptionLabelLeftPin: NSLayoutConstraint? - public var formDescriptionLabelRightPin: NSLayoutConstraint? + public var descriptionLabelLeading: NSLayoutConstraint? + public var descriptionLabelTrailing: NSLayoutConstraint? public var separatorHeightConstraint: NSLayoutConstraint? @@ -123,8 +134,8 @@ import UIKit /// This must be overriden by a subclass. public override init(frame: CGRect) { super.init(frame: frame) + setupView() - self.hasDropDown = false } /// This must be overriden by a subclass. @@ -134,7 +145,7 @@ import UIKit required public init?(coder: NSCoder) { super.init(coder: coder) - fatalError("TextEntryField does not support xib.") + fatalError("FormEntryField does not support xib.") } //-------------------------------------------------- @@ -151,21 +162,21 @@ import UIKit setContentCompressionResistancePriority(.required, for: .vertical) backgroundColor = .clear - let formDescriptionLabel = Label() - self.formDescriptionLabel = formDescriptionLabel - formDescriptionLabel.font = MFStyler.fontB3() - formDescriptionLabel.textColor = UIColor.mfBattleshipGrey() - formDescriptionLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) - formDescriptionLabel.setContentHuggingPriority(UILayoutPriority(251), for: .vertical) - formDescriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) + let descriptionLabel = Label() + self.descriptionLabel = descriptionLabel + descriptionLabel.font = MFStyler.fontB3() + descriptionLabel.textColor = UIColor.mfBattleshipGrey() + descriptionLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) + descriptionLabel.setContentHuggingPriority(UILayoutPriority(251), for: .vertical) + descriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) - addSubview(formDescriptionLabel) + addSubview(descriptionLabel) - formDescriptionLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true - formDescriptionLabelLeftPin = formDescriptionLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) - formDescriptionLabelLeftPin?.isActive = true - formDescriptionLabelRightPin = layoutMarginsGuide.trailingAnchor.constraint(equalTo: formDescriptionLabel.trailingAnchor) - formDescriptionLabelRightPin?.isActive = true + descriptionLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true + descriptionLabelLeading = descriptionLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + descriptionLabelLeading?.isActive = true + descriptionLabelTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: descriptionLabel.trailingAnchor) + descriptionLabelLeading?.isActive = true let fieldContainer = UIView(frame: .zero) self.fieldContainer = fieldContainer @@ -174,39 +185,41 @@ import UIKit addSubview(fieldContainer) setupFieldContainer(fieldContainer) - fieldContainer.topAnchor.constraint(equalTo: formDescriptionLabel.bottomAnchor, constant: 4).isActive = true - textContainerLeftPin = fieldContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) - textContainerLeftPin?.isActive = true - textContainerRightPin = layoutMarginsGuide.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor) - textContainerRightPin?.isActive = true + fieldContainer.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 4).isActive = true + textContainerLeading = fieldContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + textContainerLeading?.isActive = true + textContainerTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor) + textContainerTrailing?.isActive = true - let placeholderErrorLabel = Label() - self.placeholderErrorLabel = placeholderErrorLabel - placeholderErrorLabel.font = MFStyler.fontForTextFieldUnderLabel() - placeholderErrorLabel.textColor = .black - placeholderErrorLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) - placeholderErrorLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) - placeholderErrorLabel.setContentCompressionResistancePriority(.required, for: .vertical) + let feedbackLabel = Label() + self.feedbackLabel = feedbackLabel + feedbackLabel.font = MFStyler.fontForTextFieldUnderLabel() + feedbackLabel.textColor = .black + feedbackLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) + feedbackLabel.setContentHuggingPriority(UILayoutPriority(251), for: .horizontal) + feedbackLabel.setContentCompressionResistancePriority(.required, for: .vertical) - addSubview(placeholderErrorLabel) + addSubview(feedbackLabel) - placeholderErrorLabel.topAnchor.constraint(equalTo: fieldContainer.bottomAnchor).isActive = true - errorLableLeftPin = placeholderErrorLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) - errorLableLeftPin?.isActive = true - errorLableRightPin = layoutMarginsGuide.trailingAnchor.constraint(equalTo: placeholderErrorLabel.trailingAnchor) - errorLableRightPin?.isActive = true - layoutMarginsGuide.bottomAnchor.constraint(equalTo: placeholderErrorLabel.bottomAnchor).isActive = true + feedbackLabel.topAnchor.constraint(equalTo: fieldContainer.bottomAnchor).isActive = true + errorLabelLeading = feedbackLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor) + errorLabelLeading?.isActive = true + errorLabelTrailing = layoutMarginsGuide.trailingAnchor.constraint(equalTo: feedbackLabel.trailingAnchor) + errorLabelTrailing?.isActive = true + layoutMarginsGuide.bottomAnchor.constraint(equalTo: feedbackLabel.bottomAnchor).isActive = true setNeedsLayout() } - /// Method to override. - /// Intended to add the interactive content (textField) to the fieldContainer. + /** + Method to override. + Intended to add the interactive content (textField) to the fieldContainer. + */ open func setupFieldContainerContent(_ container: UIView) { - // To Be Overridden + // To Be Overridden By Subclass. } - /// Configuration logic for the text container view. + /// Configuration for the field container view. private func setupFieldContainer(_ parentView: UIView) { let backgroundView = UIView(frame: .zero) @@ -253,13 +266,17 @@ import UIKit open override func updateView(_ size: CGFloat) { super.updateView(size) - formDescriptionLabel?.updateView(size) - placeholderErrorLabel?.font = MFStyler.fontForTextFieldUnderLabel() + descriptionLabel?.updateView(size) + feedbackLabel?.font = MFStyler.fontForTextFieldUnderLabel() dashLine?.updateView(size) layoutIfNeeded() } + //-------------------------------------------------- + // MARK: - Drawing + //-------------------------------------------------- + open override func draw(_ rect: CGRect) { super.draw(rect) @@ -274,7 +291,7 @@ import UIKit borderPath?.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height)) borderPath?.lineWidth = 1 - let strokeColor = showError ? UIColor.mfPumpkin() : UIColor.mfSilver() + let strokeColor = showErrorMessage ? UIColor.mfPumpkin() : UIColor.mfSilver() strokeColor.setStroke() borderPath?.stroke() @@ -282,142 +299,30 @@ import UIKit } //-------------------------------------------------- - // MARK: - Methods + // MARK: - Constraint Methods //-------------------------------------------------- - open func showErrorDropdown(_ show: Bool) { - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.showError = show - self.separatorHeightConstraint?.constant = show ? 4 : 1 - self.separatorView?.backgroundColor = show ? UIColor.mfPumpkin() : .black - self.setNeedsDisplay() - self.layoutIfNeeded() - } - } - - open func showErrorMessage(_ errorMessage: String?) { - - guard isEnabled else { return } - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.separatorHeightConstraint?.constant = 4 - self.showError = true - self.separatorView?.backgroundColor = UIColor.mfPumpkin() - self.placeholderErrorLabel?.text = errorMessage - self.placeholderErrorLabel?.numberOfLines = 0 - self.setNeedsDisplay() - self.layoutIfNeeded() - self.showErrorDropdown(self.showError) - } - } - - open func hideError() { - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.separatorHeightConstraint?.constant = 1 - self.separatorView?.backgroundColor = .black - self.layoutIfNeeded() - self.showError = false - self.placeholderErrorLabel?.textColor = .black - self.placeholderErrorLabel?.text = "" - self.setNeedsDisplay() - self.layoutIfNeeded() - } - } - - public func setWithMap(_ map: [AnyHashable: Any]?) { - - guard let map = map, !map.isEmpty else { return } - - if let formText = map[KeyLabel] as? String { - self.formText = formText - } - - if let text = map[KeyDisable] as? String, text.isEqual(StringY) || map.boolForKey(KeyDisable) { - formIsDisabled() - } - - if let errMessage = map[KeyErrorMessage] as? String { - self.errorMessage = errMessage - } - - if let hideBorder = map["hideBorder"] as? Bool { - self.hideBorder = hideBorder - } - - // Key used to send text value to server - if let fieldKey = map[KeyFieldKey] as? String { - self.fieldKey = fieldKey - } - } - open override func setLeftPinConstant(_ constant: CGFloat) { - textContainerLeftPin?.constant = constant - errorLableLeftPin?.constant = constant - formDescriptionLabelLeftPin?.constant = constant + textContainerLeading?.constant = constant + errorLabelLeading?.constant = constant + descriptionLabelLeading?.constant = constant } open override func setRightPinConstant(_ constant: CGFloat) { - textContainerRightPin?.constant = constant - errorLableRightPin?.constant = constant - formDescriptionLabelRightPin?.constant = constant + textContainerTrailing?.constant = constant + errorLabelTrailing?.constant = constant + descriptionLabelTrailing?.constant = constant } - public func showDropDown(_ show: Bool) { - - if hasDropDown { - dropDownCaretLabel?.isHidden = !show - dropDownCarrotWidth?.isActive = !show - setNeedsLayout() - layoutIfNeeded() - } - } - - open func formIsEnabled() { - - // Set outside the dispatch so that registerAnimations can know about it - isEnabled = true - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.isUserInteractionEnabled = true - self.formDescriptionLabel?.textColor = UIColor.mfBattleshipGrey() - self.placeholderErrorLabel?.textColor = .black - self.separatorView?.backgroundColor = (self.showError) ? UIColor.mfPumpkin() : .black - self.showDropDown(true) - } - } - - open func formIsDisabled() { - - // Set outside the dispatch so that registerAnimations can know about it - isEnabled = false - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.isUserInteractionEnabled = false - self.formDescriptionLabel?.textColor = UIColor.mfSilver() - self.placeholderErrorLabel?.textColor = UIColor.mfSilver() - self.showDropDown(false) - self.hideError() // Should not have error if the field is disabled - self.separatorView?.backgroundColor = UIColor.mfSilver() - } - } + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- open func showPlaceholderErrorLabel(_ show: Bool) { - placeholderErrorLabel?.isHidden = !show + feedbackLabel?.isHidden = !show } open func showDashSeperatorView(_ dash: Bool) { @@ -433,7 +338,31 @@ extension FormEntryField { override open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData) - setWithMap(json) + + guard let dictionary = json, + !dictionary.isEmpty + else { return } + + if let formText = dictionary[KeyLabel] as? String { + self.descriptionText = formText + } + + if let text = dictionary[KeyDisable] as? String, text.isEqual(StringY) || dictionary.boolForKey(KeyDisable) { + isEnabled = false + } + + if let errMessage = dictionary[KeyErrorMessage] as? String { + self.errorMessage = errMessage + } + + if let hideBorder = dictionary["hideBorder"] as? Bool { + self.hideBorder = hideBorder + } + + // Key used to send text value to server + if let fieldKey = dictionary[KeyFieldKey] as? String { + self.fieldKey = fieldKey + } } override open class func estimatedHeight(forRow json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat { diff --git a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift index b943beea..82d27df1 100644 --- a/MVMCoreUI/Atoms/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atoms/TextFields/TextEntryField.swift @@ -24,8 +24,7 @@ import UIKit // MARK: - Outlets //-------------------------------------------------- - public var textField: UITextField? - + private(set) var textField: UITextField? private var calendar: Calendar? //-------------------------------------------------- @@ -52,18 +51,27 @@ import UIKit /// If you're using a MFViewController, you must set this to it public weak var uiTextFieldDelegate: UITextFieldDelegate? { get { return textField?.delegate } - set { - textField?.delegate = newValue - } + set { textField?.delegate = newValue } } //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public var enabledTextColor: UIColor? + public var disabledTextColor: UIColor? + public var observingForChanges = false - private var borderPath: UIBezierPath? + public override var isEnabled: Bool { + didSet { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.textField?.textColor = self.isEnabled ? self.enabledTextColor : self.enabledTextColor + } + } + } // The text of this textField. public override var text: String? { @@ -74,37 +82,7 @@ import UIKit } } - public override var formText: String? { - get { return formDescriptionLabel?.text } - set { - formDescriptionLabel?.text = newValue - setAccessibilityString(newValue) - } - } - - /// Sets placeholder text in the textField. - public override var placeholder: String? { - get { - guard let attributedPlaceholder = textField?.attributedPlaceholder else { return nil } - return attributedPlaceholder.string - } - set { - guard let newPlaceholderText = newValue else { - textField?.attributedPlaceholder = nil - return - } - - textField?.attributedPlaceholder = NSAttributedString(string: newPlaceholderText, attributes: [NSAttributedString.Key.foregroundColor: placeholderTextColor]) - - if !showError { - placeholderErrorLabel?.text = (textField?.text?.count ?? 0) > 0 ? newPlaceholderText : "" - } - - setAccessibilityString(newPlaceholderText) - } - } - - public var validationBlock: ((_ enteredValue: String?) -> Bool)? { + public var validationBlock: ((_ value: String?) -> Bool)? { didSet { valueChanged() } @@ -126,7 +104,7 @@ import UIKit required public init?(coder: NSCoder) { super.init(coder: coder) - fatalError("init(coder:) has not been implemented") + fatalError("TextEntryField does not support xib.") } /// - parameter bothDelegates: Sets both MF/UI Text Field Delegates. @@ -136,17 +114,6 @@ import UIKit setBothTextFieldDelegates(bothDelegates) } - /// - parameter hasDropDown: tbd - /// - parameter map: Dictionary of values to setup this TextField - /// - parameter bothDelegate: Sets both MF/UI Text Field Delegates. - public init(hasDropDown: Bool = false, map: [AnyHashable: Any]?, bothDelegates: (UITextFieldDelegate & TextFieldDelegate)?) { - super.init(frame: .zero) - setupView() - dropDownCaretLabel?.isHidden = hasDropDown - self.hasDropDown = !hasDropDown - setWithMap(map, bothDelegates: bothDelegates) - } - //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -170,25 +137,6 @@ import UIKit textField.topAnchor.constraint(equalTo: container.topAnchor, constant: 10), textField.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16), container.bottomAnchor.constraint(equalTo: textField.bottomAnchor, constant: 10)]) - - let dropDownCaretLabel = Label() - self.dropDownCaretLabel = dropDownCaretLabel - dropDownCaretLabel.setContentHuggingPriority(UILayoutPriority(900), for: .horizontal) - dropDownCaretLabel.setContentHuggingPriority(UILayoutPriority(251), for: .vertical) - dropDownCaretLabel.setContentCompressionResistancePriority(UILayoutPriority(900), for: .horizontal) - dropDownCaretLabel.isHidden = true - dropDownCaretLabel.isUserInteractionEnabled = true - let tapOnCarrot = UITapGestureRecognizer(target: self, action: #selector(startEditing)) - dropDownCaretLabel.addGestureRecognizer(tapOnCarrot) - - container.addSubview(dropDownCaretLabel) - - dropDownCaretLabel.topAnchor.constraint(equalTo: container.topAnchor).isActive = true - dropDownCaretLabel.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: 6).isActive = true - container.trailingAnchor.constraint(equalTo: dropDownCaretLabel.trailingAnchor, constant: 16).isActive = true - container.bottomAnchor.constraint(equalTo: dropDownCaretLabel.bottomAnchor).isActive = true - dropDownCarrotWidth = dropDownCaretLabel.widthAnchor.constraint(equalToConstant: 0) - dropDownCarrotWidth?.isActive = true } open override func updateView(_ size: CGFloat) { @@ -206,31 +154,20 @@ import UIKit uiTextFieldDelegate = nil } - open override func draw(_ rect: CGRect) { - super.draw(rect) - - borderPath?.removeAllPoints() - - if !hideBorder, let frame = fieldContainer?.frame { - - borderPath = UIBezierPath() - borderPath?.move(to: CGPoint(x: frame.origin.x, y: frame.origin.y + frame.size.height)) - borderPath?.addLine(to: CGPoint(x: frame.origin.x, y: frame.origin.y)) - borderPath?.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y)) - borderPath?.addLine(to: CGPoint(x: frame.origin.x + frame.size.width, y: frame.origin.y + frame.size.height)) - borderPath?.lineWidth = 1 - - let strokeColor = showError ? UIColor.mfPumpkin() : UIColor.mfSilver() - strokeColor.setStroke() - - borderPath?.stroke() - } - } - //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- + public func showDropDown(_ show: Bool) { + + if hasDropDown { + dropDownCaretLabel?.isHidden = !show + dropDownCarrotWidth?.isActive = !show + setNeedsLayout() + layoutIfNeeded() + } + } + open override func showErrorMessage(_ errorMessage: String?) { super.showErrorMessage(errorMessage) @@ -327,6 +264,7 @@ import UIKit textField?.isUserInteractionEnabled = true textField?.isEnabled = true + showDropDown(true) } open override func formIsDisabled() { @@ -334,6 +272,7 @@ import UIKit textField?.isUserInteractionEnabled = false textField?.isEnabled = false + self.showDropDown(false) } //-------------------------------------------------- @@ -344,7 +283,7 @@ import UIKit // Update label for placeholder if !showError { - placeholderErrorLabel?.text = "" + feedbackLabel?.text = "" } let previousValidity = isValid