diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift index 65b13861..811b2e2a 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift @@ -71,13 +71,17 @@ import UIKit public var showError: Bool { get { return entryFieldContainer.showError } set (error) { - self.feedback = error ? entryFieldModel?.errorMessage : entryFieldModel?.feedback + self.feedback = error ? errorMessage : entryFieldModel?.feedback self.feedbackLabel.textColor = error ? entryFieldModel?.errorTextColor?.uiColor ?? .mvmBlack : .mvmBlack self.entryFieldContainer.showError = error self.entryFieldModel?.showError = error } } + var errorMessage: String? { + entryFieldModel?.dynamicErrorMessage ?? entryFieldModel?.errorMessage + } + /// Toggles original or locked UI. public var isLocked: Bool { get { return entryFieldContainer.isLocked } @@ -306,16 +310,30 @@ import UIKit if self.isSelected { self.updateValidation(model.isValid ?? true) + } else if model.isValid ?? true && self.showError { self.showError = false } }) } + model.updateUIDynamicError = { [weak self] in + MVMCoreDispatchUtility.performBlock(onMainThread: { + guard let self = self else { return } + let validState = model.isValid ?? false + self.updateValidation(validState) + if !validState && model.shouldClearText { + self.text = "" + model.shouldClearText = false + } + }) + } + title = model.title feedback = model.feedback isEnabled = model.enabled entryFieldContainer.disableAllBorders = model.hideBorders + accessibilityIdentifier = model.accessibilityIdentifier ?? model.fieldKey if let isLocked = model.locked { self.isLocked = isLocked diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index d64f70ae..ef5c57e9 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -19,8 +19,16 @@ import Foundation } public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var title: String? public var feedback: String? + public var shouldClearText: Bool = false + public var dynamicErrorMessage: String? { + didSet { + isValid = dynamicErrorMessage?.isEmpty ?? true + updateUIDynamicError?() + } + } public var errorMessage: String? public var errorTextColor: Color? public var enabled: Bool = true @@ -40,6 +48,9 @@ import Foundation /// Temporary binding mechanism for the view to update on enable changes. public var updateUI: ActionBlock? + + // TODO: Remove once updateUI is fixed with isSelected + public var updateUIDynamicError: ActionBlock? //-------------------------------------------------- // MARK: - Keys @@ -48,6 +59,7 @@ import Foundation private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case title case enabled case feedback @@ -67,6 +79,9 @@ import Foundation //-------------------------------------------------- public func formFieldValue() -> AnyHashable? { + if dynamicErrorMessage != nil { + dynamicErrorMessage = nil + } return text } @@ -96,6 +111,7 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback) errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) @@ -117,6 +133,7 @@ import Foundation var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(feedback, forKey: .feedback) try container.encodeIfPresent(text, forKey: .text) diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 4b6b565e..ba057444 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -19,7 +19,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol var observer: NSKeyValueObservation? public var templateModel: ListPageTemplateModel? - + //-------------------------------------------------- // MARK: - Computed Properties //-------------------------------------------------- @@ -41,7 +41,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- - + open override func parsePageJSON() throws { try parseTemplate(json: loadObject?.pageJSON) try super.parsePageJSON() @@ -54,8 +54,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func viewForTop() -> UIView { guard let headerModel = templateModel?.header, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar) - else { return super.viewForTop() } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar) + else { return super.viewForTop() } // Temporary, Default the horizontal padding if var container = templateModel?.header as? ContainerModelProtocol, container.useHorizontalMargins == nil { @@ -67,8 +67,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol override open func viewForBottom() -> UIView { guard let footerModel = templateModel?.footer, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) - else { return super.viewForBottom() } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) + else { return super.viewForBottom() } return molecule } @@ -86,7 +86,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol //Handle scroll handleScrollToSpecificRow() } - + //-------------------------------------------------- // MARK: - Handle scroll to spefic row @@ -117,12 +117,12 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { guard let moleculeInfo = getMoleculeInfo(for: indexPath), - let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject) - else { return 0 } + let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject) + else { return 0 } return estimatedHeight } - + open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return moleculesInfo?.count ?? 0 } @@ -130,8 +130,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let moleculeInfo = getMoleculeInfo(for: indexPath), - let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier) - else { return UITableViewCell() } + let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier) + else { return UITableViewCell() } (cell as? MoleculeViewProtocol)?.reset() (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath) @@ -222,8 +222,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol func createMoleculeInfo(with listItem: MoleculeModelProtocol?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? { guard let listItem = listItem, - let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) - else { return nil } + let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) + else { return nil } let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index f8347cb3..f0ed1ba4 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -18,12 +18,8 @@ import UIKit @objc public var loadObject: MVMCoreLoadObject? public var model: MVMControllerModelProtocol? public var pageModel: PageModelProtocol? { - get { - return model - } - set { - model = newValue as? MVMControllerModelProtocol - } + get { model } + set { model = newValue as? MVMControllerModelProtocol } } /// Set if this page is containted in a manager. @@ -31,12 +27,10 @@ import UIKit /// A temporary iVar backer for delegateObject() until we change the protocol public lazy var delegateObjectIVar: MVMCoreUIDelegateObject = { - return MVMCoreUIDelegateObject.create(withDelegateForAll: self) + MVMCoreUIDelegateObject.create(withDelegateForAll: self) }() - public func delegateObject() -> DelegateObject? { - return delegateObjectIVar - } + public func delegateObject() -> DelegateObject? { delegateObjectIVar } public var formValidator: FormValidator? @@ -52,7 +46,7 @@ import UIKit /// Checks if the screen width has changed open func screenSizeChanged() -> Bool { - return !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) + !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) } //-------------------------------------------------- @@ -61,8 +55,8 @@ import UIKit open func observeForResponseJSONUpdates() { guard !observingForResponses, - (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) - else { return } + (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) + else { return } observingForResponses = true NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) @@ -80,28 +74,28 @@ import UIKit } open func modulesToListenFor() -> [String]? { - return loadObject?.requestParameters?.modules as? [String] + loadObject?.requestParameters?.modules as? [String] } @objc open func responseJSONUpdated(notification: Notification) { // Checks for a page we are listening for. var newData = false if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap), - let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in - guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), - let pageType = page.optionalStringForKey(KeyPageType), - pageType == pageTypeListened - else { return false } - - return true - }) { + let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in + guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), + let pageType = page.optionalStringForKey(KeyPageType), + pageType == pageTypeListened + else { return false } + + return true + }) { newData = true loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) } // Checks for modules we are listening for. if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap), - let modulesListened = modulesToListenFor() { + let modulesListened = modulesToListenFor() { for moduleName in modulesListened { if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { newData = true @@ -196,9 +190,9 @@ import UIKit open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer) -> Bool { guard let pageType = loadObject?.pageType, - var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType), - !modulesRequired.isEmpty - else { return true } + var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType), + !modulesRequired.isEmpty + else { return true } guard let loadedModules = loadObject?.modulesJSON else { return false } @@ -263,8 +257,8 @@ import UIKit /// Sets the navigation item for this view controller. open func setNavigationItem() { guard let navigationItemModel = getNavigationModel(), - let navigationController = navigationController - else { return } + let navigationController = navigationController + else { return } // Utilize helper function to set the navigation item state. NavigationController.setNavigationItem(navigationController: navigationController, navigationItemModel: navigationItemModel, viewController: self) @@ -273,9 +267,9 @@ import UIKit /// Sets the appearance of the navigation bar based on the model. open func setNavigationBar() { guard let navigationItemModel = getNavigationModel(), - let navigationController = navigationController else { - MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() - return + let navigationController = navigationController else { + MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() + return } // Utilize helper function to set the split view and navigation item state. @@ -405,7 +399,7 @@ import UIKit } open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait + MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait } open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -424,6 +418,7 @@ import UIKit open func viewControllerReady(inManager manager: UIViewController & MVMCoreViewManagerProtocol) { pageShown() } + //-------------------------------------------------- // MARK: - MVMCoreLoadDelegateProtocol //-------------------------------------------------- @@ -435,15 +430,30 @@ import UIKit // Open the support panel if error == nil, - loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true { + loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true { MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true) } } - + /// Override this method to avoid adding form params. open func addFormParams(_ requestParameters: MVMCoreRequestParameters) { formValidator?.addFormParams(requestParameters: requestParameters) } + + public func handleFieldErrors(_ fieldErrors: [Any]?, loadObject: MVMCoreLoadObject) { + + for case let fieldError as [AnyHashable: Any] in fieldErrors ?? [] { + + guard let fieldName = fieldError["fieldName"] as? String, + let userError = fieldError["userMessage"] as? String, + let entryFieldModel = formValidator?.fields[fieldName] as? EntryFieldModel + else { continue } + + entryFieldModel.shouldClearText = fieldError["clearText"] as? Bool ?? true + entryFieldModel.dynamicErrorMessage = userError + } + } + //-------------------------------------------------- // MARK: - MVMCoreActionDelegateProtocol //-------------------------------------------------- @@ -469,9 +479,9 @@ import UIKit open func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? { guard let moduleJSON = loadObject?.modulesJSON?.optionalDictionaryForKey(moleculeName), - let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), - let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) - else { return nil } + let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), + let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) + else { return nil } do { return try modelType.decode(jsonDict: moduleJSON) as? MoleculeModelProtocol @@ -483,8 +493,8 @@ import UIKit } // Needed otherwise when subclassed, the extension gets called. - open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) {} - open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { return nil } + open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } + open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { nil } open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation) { } open func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation) { } @@ -514,13 +524,13 @@ import UIKit } open func showRightPanelForScreenBeforeLaunchApp() -> Bool { - return loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false + loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false } // TODO: make molecular open func isOverridingRightButton() -> Bool { guard let rightPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("rightPanelButtonLink") - else { return false } + else { return false } MVMCoreActionHandler.shared()?.handleAction(with: rightPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } @@ -528,7 +538,7 @@ import UIKit // TODO: make molecular open func isOverridingLeftButton() -> Bool { guard let leftPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("leftPanelButtonLink") - else { return false } + else { return false } MVMCoreActionHandler.shared()?.handleAction(with: leftPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } @@ -536,8 +546,8 @@ import UIKit // Eventually will be moved to Model open func bottomProgress() -> Float? { guard let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent), - let progress = Float(progressString) - else { return nil } + let progress = Float(progressString) + else { return nil } return progress / Float(100) } @@ -558,8 +568,8 @@ import UIKit // TODO: Make this into a protocol if UIAccessibility.isVoiceOverRunning { if let toolBar = textField.inputAccessoryView as? UIToolbar, - let _ = toolBar.items?.last, - let pickerView = textField.inputView as? UIPickerView { + let _ = toolBar.items?.last, + let pickerView = textField.inputView as? UIPickerView { view.accessibilityElements = [pickerView, toolBar] } @@ -610,6 +620,6 @@ import UIKit //-------------------------------------------------- func executeBehaviors(_ behaviorBlock:(_ behavior:T)->Void) { - model?.behaviors?.compactMap({ $0 as? T }).forEach { behaviorBlock($0) } + model?.behaviors?.compactMap { $0 as? T }.forEach { behaviorBlock($0) } } } diff --git a/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift b/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift index b9d2defe..f769e040 100644 --- a/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift @@ -22,7 +22,6 @@ public protocol FormFieldProtocol: FormItemProtocol { } extension FormFieldProtocol { - var baseValue: AnyHashable? { - return nil - } + + var baseValue: AnyHashable? { nil } }