diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 17fc19f7..a080e482 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 01DF567021FA5AB300CC099B /* TextFieldListFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DF566F21FA5AB300CC099B /* TextFieldListFormViewController.swift */; }; 01E569D3223FFFA500327251 /* ThreeLayerViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = D2A5146A2214905000345BFB /* ThreeLayerViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */; }; + 0A41BA7F23453A6400D4C0BC /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA7E23453A6400D4C0BC /* TextField.swift */; }; 948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948DB67D2326DCD90011F916 /* MultiProgress.swift */; }; B8200E152280C4CF007245F4 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E142280C4CF007245F4 /* ProgressBar.swift */; }; B8200E192281DC1A007245F4 /* CornerLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8200E182281DC1A007245F4 /* CornerLabels.swift */; }; @@ -203,6 +204,7 @@ 01DF55DF21F8FAA800CC099B /* MFTextFieldListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MFTextFieldListView.swift; sourceTree = ""; }; 01DF566F21FA5AB300CC099B /* TextFieldListFormViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldListFormViewController.swift; sourceTree = ""; }; 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionDetailWithImage.swift; sourceTree = ""; }; + 0A41BA7E23453A6400D4C0BC /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; 948DB67D2326DCD90011F916 /* MultiProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgress.swift; sourceTree = ""; }; B8200E142280C4CF007245F4 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = ""; }; B8200E182281DC1A007245F4 /* CornerLabels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerLabels.swift; sourceTree = ""; }; @@ -740,6 +742,7 @@ D29DF24321E6A176003B2FB9 /* MFDigitTextField.h */, D29DF24821E6A177003B2FB9 /* MFDigitTextField.m */, D29DF24A21E6A177003B2FB9 /* MFDigitTextField.xib */, + 0A41BA7E23453A6400D4C0BC /* TextField.swift */, ); path = TextFields; sourceTree = ""; @@ -1046,6 +1049,7 @@ D22479962316AF6E003FCCF9 /* HeadlineBodyTextButton.swift in Sources */, D2E1FADD2268B25E00AEFD8C /* MoleculeTableViewCell.swift in Sources */, D29DF2AE21E7B3A4003B2FB9 /* MFTextView.m in Sources */, + 0A41BA7F23453A6400D4C0BC /* TextField.swift in Sources */, D29DF18121E69E50003B2FB9 /* MFView.m in Sources */, D29DF18321E69E54003B2FB9 /* SeparatorView.m in Sources */, D29DF17A21E69E1F003B2FB9 /* MFCustomButton.m in Sources */, diff --git a/MVMCoreUI/Atoms/TextFields/TextField.swift b/MVMCoreUI/Atoms/TextFields/TextField.swift new file mode 100644 index 00000000..0205b76a --- /dev/null +++ b/MVMCoreUI/Atoms/TextFields/TextField.swift @@ -0,0 +1,618 @@ +// +// TextField.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 10/2/19. +// Copyright © 2019 Verizon Wireless. All rights reserved. +// + +import MVMCoreUI +import UIKit + +@objc protocol MFTextFieldDelegate: NSObjectProtocol { + // Called when the entered text becomes valid based on the validation block + @objc optional func entryIsValid(_ textfield: MFTextField?) + // Called when the entered text becomes invalid based on the validation block + @objc optional func entryIsInvalid(_ textfield: MFTextField?) + // Dismisses the keyboard. + @objc optional func dismissFieldInput(_ sender: Any?) +} + +class MFTextField: ViewConstrainingView, MVMCoreUIMoleculeViewProtocol, FormValidationProtocol { + weak var view: UIView? + @IBOutlet weak var textFieldContainerView: UIView? + @IBOutlet weak var backgroundView: UIView? + @IBOutlet weak var textField: UITextField? + @IBOutlet weak var formLabel: Label? + @IBOutlet weak var separatorView: UIView? + /*make it public so outsider class can know the posistion of it. */ @IBOutlet weak var heightConstraint: NSLayoutConstraint? + @IBOutlet weak var formLabelRightPin: NSLayoutConstraint? + var enabled = false + // To set the placeholder and text + weak var text: String? + weak var formText: String? + weak var fieldKey: String? + weak var placeholder: String? + /* will move out in Feb release */ var hideBorder = false + + weak var datePicker: UIDatePicker? + private(set) weak var toolbar: UIToolbar? + //helper + var formatter: DateFormatter? + // If you're using a MFViewController, you must set this to it + weak var uiTextFieldDelegate: UITextFieldDelegate? + // The delegate and block for validation. Validates if the text that the user has entered is valid or not. Checked after each change if there is a delegate. + weak var mfTextFieldDelegate: MFTextFieldDelegate? + var valid = false + var validationBlock: ((_ enteredValue: String?) -> Bool)? + // custom text colors + var customEnabledTextColor: UIColor? + var customDisabledTextColor: UIColor? + //default error message + var errMessage: String? + var editCompleteAction: ((_ text: String?) -> Void)? + + + private var customPlaceHolderColor: UIColor? + @IBOutlet private weak var separatorHeightConstraint: NSLayoutConstraint! + private var borderPath: UIBezierPath? + private var calendar: Calendar? + @IBOutlet private weak var textContainerLeftPin: NSLayoutConstraint! + @IBOutlet private weak var errorLableLeftPin: NSLayoutConstraint! + @IBOutlet private weak var formLabelLeftPin: NSLayoutConstraint! + @IBOutlet private weak var textContainerRightPin: NSLayoutConstraint! + @IBOutlet private weak var errorLableRightPin: NSLayoutConstraint! + + // MARK: - setup + func updateView(_ size: CGFloat) { + super.updateView(size) + MVMCoreDispatchUtility.performBlock(onMainThread: { + self.formLabel.updateView(size) + self.label.font = MFStyler.fontForTextFieldUnderLabel() + MFStyler.styleTextField(self.textField) + self.dashLine.updateView(size) + }) + } + + func setupView() { + if !self.view { + backgroundColor = UIColor.clear + let nib = getNib() + let views = nib?.instantiate(withOwner: self, options: nil) + let view = views?.first as? UIView + view?.translatesAutoresizingMaskIntoConstraints = false + view?.setContentHuggingPriority(.required, for: .vertical) + view?.setContentCompressionResistancePriority(.required, for: .vertical) + self.view = view + view?.frame = frame + if let view = view { + addSubview(view) + } + pinView(toSuperView: view) + + textField.font = MFStyler.fontForTextField() + translatesAutoresizingMaskIntoConstraints = false + + formLabel.font = MFStyler.fontB3() + formLabel.textColor = UIColor.mfBattleshipGrey() + + label.font = MFStyler.fontForTextFieldUnderLabel() + label.textColor = UIColor.black + + dropDownCarrotLabel.hidden = true + separatorView.backgroundColor = UIColor.black + dashLine.backgroundColor = UIColor.white + dashLine.hidden = true + MFStyler.styleTextField(textField) + + dropDownCarrotLabel.isUserInteractionEnabled = true + let tapOnCarrot = UITapGestureRecognizer(target: self, action: #selector(startEditing)) + dropDownCarrotLabel.addGestureRecognizer(tapOnCarrot) + enabled = true + + // Disable SmartQuotes + if #available(iOS 11.0, *) { + textField.smartQuotesType = .no + textField.smartDashesType = .no + textField.smartInsertDeleteType = .no + } + } + } + + func getNib() -> UINib? { + return UINib(nibName: NSStringFromClass(type(of: self).self), bundle: MVMCoreUIUtility.bundleForMVMCoreUI()) + } + + class func mfTextField() -> Self? { + let view = self.init() + view?.hasDropDown = false + return view + } + + class func mfTextField(withBothDelegates delegate: (UITextFieldDelegate & MFTextFieldDelegate)?) -> Self? { + let textField = self.mfTextField() + textField?.bothTextFieldDelegates = delegate + return textField + } + + class func mfTextField(withMap map: [AnyHashable : Any]?, bothDelegates delegate: (UITextFieldDelegate & MFTextFieldDelegate)?) -> Self? { + let textField = self.mfTextField() + textField?.translatesAutoresizingMaskIntoConstraints = false + textField?.setWithMap(map, bothDelegates: delegate) + return textField + } + + class func mfTextFieldForDropDown() -> Self? { + let textField = self.mfTextField() + textField?.dropDownCarrotLabel().hidden = false + textField?.hasDropDown = true + return textField + } + + class func mfTextFieldForDropDown(withBothDelegates delegate: (UITextFieldDelegate & MFTextFieldDelegate)?) -> Self? { + let textField = self.mfTextFieldForDropDown() + textField?.bothTextFieldDelegates = delegate + return textField + } + + // MARK: - Utilities + func createDatePicker() { + //tool bar + MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: textField.delegate) + + //date picker + datePicker = MVMCoreUICommonViewsUtility.addDatePicker(to: textField) + + let calendar = Calendar.current + calendar.timeZone = NSTimeZone.system + self.calendar = calendar + } + + func inputFromDatePicker(from fromDate: Date?, to toDate: Date?, showFromDateAsDefaultInput show: Bool) { + createDatePicker() + if show { + if let fromDate = fromDate { + if 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 + } + + func setDatePickerFrom(_ fromDate: Date?, to toDate: Date?) { + if let fromDate = fromDate { + if 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 + datePicker.timeZone = NSTimeZone.system + } + + func dismissDatePicker() -> Date? { + let pickedDate = datePicker.date + if let pickedDate = pickedDate { + if calendar.isDate(pickedDate, inSameDayAs: Date()) { + text = MVMCoreUIUtility.hardcodedString(withKey: "textfield_today_string") + } else { + self.text = formatter.string(from: pickedDate) + } + } + textField.resignFirstResponder() + return pickedDate + } + + func dismissPicker() { + textField.resignFirstResponder() + } + + func setErrorMessage(_ errorMessage: String?) { + + MVMCoreDispatchUtility.performBlock(onMainThread: { + if self.enabled == true { + self.separatorHeightConstraint.constant = 4 + self.errorShowing = true + self.separatorView.backgroundColor = UIColor.mfPumpkin() + self.label.text() = errorMessage + self.label.numberOfLines = 0 + self.textField.accessibilityValue() = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textfield_error_message"), self.textField.text() ?? "", errorMessage ?? "") + self.setNeedsDisplay() + self.layoutIfNeeded() + } + }) + } + + func pushAccessibilityNotification() { + MVMCoreDispatchUtility.performBlock(onMainThread: { + UIAccessibilityPostNotification(UIAccessibility.Notification.layoutChanged, self.textField) + }) + } + + func hideError() { + + MVMCoreDispatchUtility.performBlock(onMainThread: { + self.separatorHeightConstraint.constant = 1 + self.separatorView.backgroundColor = UIColor.black + self.layoutIfNeeded() + self.errorShowing = false + self.label.textColor = UIColor.black + self.label.text() = "" + self.textField.accessibilityValue() = nil + self.setNeedsDisplay() + self.layoutIfNeeded() + }) + } + + func placeholder() -> String? { + return textField.attributedPlaceholder.string + } + + func text() -> String? { + return textField.text() + } + + // MARK: - Setters + + func setPlaceholder(_ placeholder: String?, with color: UIColor?) { + customPlaceHolderColor = color + // fixed crash issue + if placeholder != nil { + if let color = color { + textField.attributedPlaceholder = NSAttributedString(string: placeholder ?? "", attributes: [ + NSAttributedString.Key.foregroundColor: color + ]) + } + } + if textField.text.length > 0 && !errorShowing { + label.text = placeholder + } else if !errorShowing { + label.text = "" + } + setAccessibilityString(placeholder) + } + + func setAccessibilityString(_ accessibilityString: String?) { + var accessibilityString = accessibilityString + // adding missing accessibilityLabel value + // if we have some value in accessibilityLabel, + // then only can append regular and picker item + if hasDropDown { + // MFDLog(@"Label: %@", self.textField.accessibilityLabel); + accessibilityString = accessibilityString ?? "" + (MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item")) + // MFDLog(@"Label: %@", self.textField.accessibilityLabel); + } else { + accessibilityString = accessibilityString ?? "" + (MVMCoreUIUtility.hardcodedString(withKey: "textfield_regular")) + // MFDLog(@"Label: %@", self.textField.accessibilityLabel); + } + + textField.accessibilityLabel() = "\(accessibilityString ?? "") \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state"))" + } + + func setFormText(_ formText: String?) { + formLabel.text = formText + setAccessibilityString(formText) + } + + func setPlaceholder(_ placeholder: String?) { + setPlaceholder(placeholder, with: customPlaceHolderColor ?? UIColor.black) + } + + func setText(_ text: String?) { + textField.text = text + valueChanged() + } + + func setMfTextFieldDelegate(_ mfTextFieldDelegate: MFTextFieldDelegate?) { + self.mfTextFieldDelegate = mfTextFieldDelegate + if mfTextFieldDelegate != nil && !observingForChanges { + observingForChanges = true + NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextField.textDidChangeNotification, object: textField) + NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextField.textDidEndEditingNotification, object: textField) + NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextField.textDidBeginEditingNotification, object: textField) + } else if mfTextFieldDelegate == nil && observingForChanges { + observingForChanges = false + NotificationCenter.default.removeObserver(self, name: UITextField.textDidChangeNotification, object: textField) + NotificationCenter.default.removeObserver(self, name: UITextField.textDidEndEditingNotification, object: textField) + NotificationCenter.default.removeObserver(self, name: UITextField.textDidBeginEditingNotification, object: textField) + } + } + + func setUiTextFieldDelegate(_ uiTextFieldDelegate: UITextFieldDelegate?) { + self.uiTextFieldDelegate = uiTextFieldDelegate + textField.delegate = uiTextFieldDelegate + } + + func setBothTextFieldDelegates(_ delegate: (UITextFieldDelegate & MFTextFieldDelegate)?) { + mfTextFieldDelegate = delegate + uiTextFieldDelegate = delegate + } + + func setWithMap(_ map: [AnyHashable : Any]?) { + if map?.count == 0 { + return + } + + var string = map?.string(KeyLabel) + if (string?.count ?? 0) > 0 { + formText = string + } + string = map?.string(KeyValue) + if (string?.count ?? 0) > 0 { + text = string + } + string = map?.string(forKey: KeyDisable) + if string?.isEqual(StringY) ?? false || map?.bool(forKey: KeyDisable) != nil { + enable(false) + } + string = map?.string(KeyErrorMessage) + if (string?.count ?? 0) > 0 { + errMessage = string + } + + // key used to send text value to server + string = map?.string(KeyFieldKey) + if (string?.count ?? 0) > 0 { + fieldKey = string + } + + string = map?.string(KeyType) + if (string == "dropDown") { + dropDownCarrotLabel().hidden = false + self.hasDropDown = true + } else if (string == "password") { + textField.isSecureTextEntry = true + } else if (string == "number") { + textField.keyboardType = .numberPad + } else if (string == "email") { + textField.keyboardType = .emailAddress + } + + string = map?.string("regex") + if (string?.count ?? 0) != 0 { + validationBlock = { enteredValue in + return MVMCoreUIUtility.validate(enteredValue, withRegularExpression: string) + } + } else { + setDefaultValidationBlock() + } + } + + func setWithMap(_ map: [AnyHashable : Any]?, bothDelegates delegate: (UITextFieldDelegate & MFTextFieldDelegate)?) { + MVMCoreUICommonViewsUtility.addDismissToolbar(textField, delegate: delegate) + self.bothTextFieldDelegates = delegate + setWithMap(map) + } + + func setValidationBlock(_ validationBlock: @escaping (String?) -> Bool) { + self.validationBlock = validationBlock + valueChanged() + } + + func setDefaultValidationBlock() { + self.validationBlock = { enteredValue in + if (enteredValue?.count ?? 0) > 0 { + return true + } else { + return false + } + } + } + + func setLeftPinConstant(_ constant: CGFloat) { + textContainerLeftPin.constant = constant + errorLableLeftPin.constant = constant + formLabelLeftPin.constant = constant + } + + func setRightPinConstant(_ constant: CGFloat) { + textContainerRightPin.constant = constant + errorLableRightPin.constant = constant + formLabelRightPin.constant = constant + } + + deinit { + self.bothTextFieldDelegates = nil + } + + // MARK: - XIB Helpers + func showDropDown(_ show: Bool) { + if hasDropDown { + dropDownCarrotLabel.hidden = !show + dropDownCarrotWidth.active = !show + setNeedsLayout() + layoutIfNeeded() + } + } + + func enable(_ enable: Bool) { + enabled = enable //Set outside the dispatch so that registerAnimations can know about it + MVMCoreDispatchUtility.performBlock(onMainThread: { + self.isUserInteractionEnabled = enable + self.textField.userInteractionEnabled = enable + self.textField.isEnabled = enable + if enable { + self.textField.textColor = self.customEnabledTextColor ?? UIColor.black + self.formLabel.textColor = UIColor.mfBattleshipGrey() + self.label.textColor = UIColor.black + if self.errorShowing { + self.separatorView.backgroundColor = UIColor.mfPumpkin() + } else { + self.separatorView.backgroundColor = UIColor.black + } + self.showDropDown(true) + } else { + self.textField.textColor = self.customDisabledTextColor ?? UIColor.mfSilver() + self.formLabel.textColor = UIColor.mfSilver() + self.label.textColor = UIColor.mfSilver() + self.showDropDown(false) + self.hideError() //should not have error if the field is disabled + self.separatorView.backgroundColor = UIColor.mfSilver() + } + }) + } + + func showLabel(_ show: Bool) { + label.hidden = !show + } + + func dashSeperatorView(_ dash: Bool) { + if dash { + dashLine.hidden = false + //never hide seperator view because it could be possiblely used by other classes for positioning + separatorView.backgroundColor = UIColor.clear + } else { + dashLine.hidden = true + separatorView.backgroundColor = UIColor.black + } + } + + // MARK: - Observing for change + func validateBlock() { + valueChanged() + } + + func valueChanged() { + + // update label for placeholder + if !errorShowing { + label.text = "" + } + + // Check validity. + let previousValidity = valid + if validationBlock { + valid = validationBlock(text) + } else { + //if validation not set, input will always be valid + valid = true + } + + if previousValidity && !valid { + if errMessage { + self.errorMessage = errMessage + } + if mfTextFieldDelegate.responds(to: #selector(entryIsInvalid(_:))) { + mfTextFieldDelegate.entryIsInvalid(self) + } + } else if !previousValidity && valid { + hideError() + if mfTextFieldDelegate.responds(to: #selector(entryIsValid(_:))) { + mfTextFieldDelegate.entryIsValid(self) + } + } + } + + func endInputing() { + if isValid { + hideError() + separatorView.backgroundColor = UIColor.black + } else { + if errMessage { + self.errorMessage = errMessage + } + } + } + + // MARK: - helper + + func startEditing() { + textField.becomeFirstResponder() + if !errorShowing { + separatorView.backgroundColor = UIColor.black + separatorHeightConstraint.constant = 1 + } + } + + class func getEnabledTextfields(_ textFieldToDetermine: [MFTextField]?) -> [AnyHashable]? { + var enabledTextFields: [AnyHashable] = [] + for textfield in textFieldToDetermine ?? [] { + if textfield.isEnabled { + enabledTextFields.append(textfield) + } + } + return enabledTextFields + } + + //#pragma mark - Accessibility + + + func formatter() -> DateFormatter? { + if !formatter { + formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.system + formatter.locale = NSLocale.current + formatter.formatterBehavior = .default + } + return formatter + } + + func draw(_ rect: CGRect) { + super.draw(rect) + borderPath.removeAllPoints() + if !hideBorder { + let frame = textFieldContainerView.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 + + var strokeColor: UIColor? + if errorShowing { + strokeColor = UIColor.mfPumpkin() + } else { + strokeColor = UIColor.mfSilver() + } + + strokeColor?.setStroke() + + borderPath.stroke() + } + } + + ////////////////////// + + #pragma mark - MVMCoreUIMoleculeViewProtocol + + - (void)setWithJSON:(NSDictionary *)json delegateObject:(MVMCoreUIDelegateObject *)delegateObject additionalData:(NSDictionary *)additionalData { + if ([delegateObject isKindOfClass:[MVMCoreUIDelegateObject class]]) { + [FormValidator setupValidationWithMolecule:self delegate:delegateObject.formValidationProtocol]; + FormValidator *formValidator = [FormValidator getFormValidatorForDelegate:delegateObject.formValidationProtocol]; + + [self setWithMap:json]; + self.mfTextFieldDelegate = formValidator; + self.uiTextFieldDelegate = delegateObject.uiTextFieldDelegate; + [MVMCoreUICommonViewsUtility addDismissToolbar:self.textField delegate:self.uiTextFieldDelegate]; + } + } + + + + (CGFloat)estimatedHeightForRow:(NSDictionary *)json delegateObject:(MVMCoreUIDelegateObject *)delegateObject { + return 76; + } + + #pragma mark - FormValidationProtocol + + + - (BOOL)isValidField { + return self.isValid; + } + + - (nullable NSString *)formFieldName { + return self.fieldKey; + } + + - (nullable id)formFieldValue { + return self.text; + } +}