diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 25045a2f..dc37ca12 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -153,6 +153,9 @@ 444FB7C32821B76B00DFE692 /* TitleLockupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */; }; 4457904E27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */; }; 4B002ACA2BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */; }; + 4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */; }; + 4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */; }; + 4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */; }; 522679C123FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */; }; 522679C223FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */; }; 52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */; }; @@ -579,6 +582,7 @@ EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; }; EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; }; EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */; }; + EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */; }; EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; @@ -772,6 +776,9 @@ 444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupModel.swift; sourceTree = ""; }; 4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageRenderingMode+Extension.swift"; sourceTree = ""; }; 4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDropdownEntryFieldModel+Extension.swift"; sourceTree = ""; }; + 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBarModel.swift; sourceTree = ""; }; + 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; + 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSizeProtocol.swift; sourceTree = ""; }; 522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinks.swift; sourceTree = ""; }; 522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinksModel.swift; sourceTree = ""; }; 52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextAllTextAndLinks.swift; sourceTree = ""; }; @@ -1201,6 +1208,7 @@ EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = ""; }; EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = ""; }; EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = ""; }; + EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEntryField.swift; sourceTree = ""; }; EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = ""; }; EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = ""; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; @@ -2314,8 +2322,11 @@ 94C2D9822386F3E30006CF46 /* Label */, 31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */, 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */, + 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */, D28A838223CCBD3F00DFE4FC /* WheelModel.swift */, 943784F3236B77BB006A1E82 /* Wheel.swift */, + 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */, + 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */, 943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */, 0AE98BB623FF18E9004C5109 /* ArrowModel.swift */, 0AE98BB423FF18D2004C5109 /* Arrow.swift */, @@ -2351,6 +2362,7 @@ children = ( 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */, 0A21DB7E235DECC500C160A2 /* EntryField.swift */, + EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */, 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, @@ -3012,6 +3024,7 @@ D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */, D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */, D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */, + 4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */, 0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */, EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */, D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */, @@ -3032,6 +3045,7 @@ AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */, AA37CBD52519072F0027344C /* Stars.swift in Sources */, 942C378E2412F5B60066E45E /* ModalMoleculeStackTemplate.swift in Sources */, + 4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */, 8D8067D32444473A00203BE8 /* ListRightVariablePriceChangeAllTextAndLinks.swift in Sources */, 8D4687E4242E2DF300802879 /* ListFourColumnDataUsageListItem.swift in Sources */, D2874024249BA6F300BE950A /* MVMCoreUISplitViewController+Extension.swift in Sources */, @@ -3108,6 +3122,7 @@ D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */, D2A6390122CBB1820052ED1F /* Carousel.swift in Sources */, C7F8012123E8303200396FBD /* ListRVWheel.swift in Sources */, + 4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */, BB2C968F24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift in Sources */, D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */, EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */, @@ -3140,6 +3155,7 @@ 323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */, D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */, 525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */, + EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */, D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */, 0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */, D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift index 2e098ad9..36700a9e 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift @@ -345,9 +345,8 @@ import UIKit numberOfDigits = model.digits - if let entryType = model.type { - setAsSecureTextEntry(entryType == .secure || entryType == .password) - } + let entryType = model.type + setAsSecureTextEntry(entryType == .secure || entryType == .password) let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift index 6f69be43..0986a092 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift @@ -7,19 +7,66 @@ // import UIKit +import VDS +open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate { + //------------------------------------------------------ + // MARK: - Properties + //------------------------------------------------------ + open var viewModel: ItemDropdownEntryFieldModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + var fieldKey: String? + var fieldValue: JSONValue? + var groupName: String? + + open var pickerData: [String] = [] { + didSet { + options = pickerData.compactMap({ DropdownOptionModel(text: $0) }) + } + } + + private var isEditting: Bool = false -open class ItemDropdownEntryField: BaseItemPickerEntryField { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public var isValid: Bool = true + + /// Closure passed here will run as picker changes items. + public var observeDropdownChange: ((String?, String) -> ())? - open var pickerData: [String] = [] + /// Closure passed here will run upon dismissing the selection picker. + public var observeDropdownSelection: ((String) -> ())? - public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? { - model as? ItemDropdownEntryFieldModel + /// When selecting for first responder, allow initial selected value to appear in empty text field. + public var setInitialValueInTextField = true + + open override var errorText: String? { + get { + viewModel.dynamicErrorMessage ?? viewModel.errorMessage + } + set {} + } + //-------------------------------------------------- + // MARK: - Delegate Properties + //-------------------------------------------------- + + /// The delegate and block for validation. Validates if the text that the user has entered. + public weak var observingTextFieldDelegate: ObservingTextFieldDelegate? + + /// If you're using a ViewController, you must set this to it + open weak var uiTextFieldDelegate: UITextFieldDelegate? { + get { dropdownField.delegate } + set { dropdownField.delegate = newValue } } + @objc public func dismissFieldInput(_ sender: Any?) { + _ = resignFirstResponder() + } + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -28,7 +75,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField { super.init(frame: frame) } - @objc public convenience init() { + @objc public convenience required init() { self.init(frame: .zero) } @@ -40,76 +87,134 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField { @objc required public init?(coder: NSCoder) { fatalError("ItemDropdownEntryField init(coder:) has not been implemented") } - - required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.init(model: model, delegateObject, additionalData) - } - + //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- + open override func setup() { + super.setup() + useRequiredRule = false + + publisher(for: .valueChanged) + .sink { [weak self] control in + guard let self, let selectedItem else { return } + viewModel.selectedIndex = control.selectId + observeDropdownSelection?(selectedItem.text) + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + }.store(in: &subscribers) + + dropdownField + .publisher(for: .editingDidBegin) + .sink { [weak self] textField in + guard let self else { return } + isEditting = true + setInitialValueFromPicker() + }.store(in: &subscribers) + + dropdownField + .publisher(for: .editingDidEnd) + .sink { [weak self] textField in + guard let self else { return } + isEditting = false + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + if let valid = viewModel.isValid { + updateValidation(valid) + } + performDropdownAction() + }.store(in: &subscribers) + } + + public func viewModelDidUpdate() { + pickerData = viewModel.options + showInlineLabel = viewModel.showInlineLabel + helperTextPlacement = viewModel.feedbackTextPlacement + labelText = viewModel.title + helperText = viewModel.feedback + isEnabled = viewModel.enabled + isReadOnly = viewModel.readOnly + isRequired = viewModel.required + tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + width = viewModel.width + transparentBackground = viewModel.transparentBackground + + if let index = viewModel.selectedIndex { + selectId = index + optionsPicker.selectRow(index, inComponent: 0, animated: false) + pickerView(optionsPicker, didSelectRow: index, inComponent: 0) + } + + if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected { + + viewModel.wasInitiallySelected = true + isEditting = true + } + + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + if isEditting { + DispatchQueue.main.async { + _ = self.becomeFirstResponder() + } + } + + viewModel.updateUI = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + if isEditting { + updateValidation(viewModel.isValid ?? true) + + } else if viewModel.isValid ?? true && showError { + showError = false + } + isEnabled = viewModel.enabled + }) + } + + viewModel.updateUIDynamicError = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + let validState = viewModel.isValid ?? false + if !validState && viewModel.shouldClearText { + selectId = nil + viewModel.shouldClearText = false + } + updateValidation(validState) + }) + } + + } + + public func updateView(_ size: CGFloat) { } /// Sets the textField with the first value of the available picker data. - @objc private func setInitialValueFromPicker() { + private func setInitialValueFromPicker() { guard !pickerData.isEmpty else { return } if setInitialValueInTextField { - let pickerIndex = pickerView.selectedRow(inComponent: 0) - itemDropdownEntryFieldModel?.selectedIndex = pickerIndex - observeDropdownChange?(text, pickerData[pickerIndex]) - text = pickerData[pickerIndex] + let pickerIndex = optionsPicker.selectedRow(inComponent: 0) + viewModel.selectedIndex = pickerIndex + selectId = pickerIndex + observeDropdownChange?(selectedItem?.text, pickerData[pickerIndex]) } } - @objc override func startEditing() { - super.startEditing() - - setInitialValueFromPicker() + private func performDropdownAction() { + guard let actionModel = viewModel.action, + !dropdownField.isFirstResponder + else { return } + MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject) } - @objc override func endInputing() { - super.endInputing() + private func updateValidation(_ isValid: Bool) { + let previousValidity = self.isValid + self.isValid = isValid - guard !pickerData.isEmpty else { return } - - observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)]) - } - - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - - guard let model = model as? ItemDropdownEntryFieldModel else { return } - - pickerData = model.options - - if let index = model.selectedIndex { - self.pickerView.selectRow(index, inComponent: 0, animated: false) - self.pickerView(pickerView, didSelectRow: index, inComponent: 0) + if previousValidity && !isValid { + showError = true + } else if (!previousValidity && isValid) { + showError = false } } - - //-------------------------------------------------- - // MARK: - Picker Delegate - //-------------------------------------------------- - - @objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 } - - @objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - pickerData.count - } - - @objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - guard !pickerData.isEmpty else { return nil } - - return pickerData[row] - } - - @objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - guard !pickerData.isEmpty else { return } - - itemDropdownEntryFieldModel?.selectedIndex = row - observeDropdownChange?(text, pickerData[row]) - text = pickerData[row] - } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift index 1c88ddab..eb83a48a 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift @@ -5,16 +5,22 @@ // Created by Kevin Christiano on 1/22/20. // Copyright © 2020 Verizon Wireless. All rights reserved. // +import VDS -@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel { +@objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- public override class var identifier: String { "dropDown" } - + public var action: ActionModelProtocol? public var options: [String] = [] public var selectedIndex: Int? + public var showInlineLabel: Bool = false + public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom + public var tooltip: TooltipModel? + public var transparentBackground: Bool = false + public var width: CGFloat? public init(with options: [String], selectedIndex: Int? = nil) { self.options = options @@ -42,6 +48,12 @@ private enum CodingKeys: String, CodingKey { case options case selectedIndex + case action + case showInlineLabel + case feedbackTextPlacement + case tooltip + case transparentBackground + case width } //-------------------------------------------------- @@ -58,6 +70,12 @@ self.selectedIndex = selectedIndex baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil } + showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false + feedbackTextPlacement = try typeContainer.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom + action = try typeContainer.decodeModelIfPresent(codingKey: .action) + tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip) + transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false + width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width) } public override func encode(to encoder: Encoder) throws { @@ -65,5 +83,11 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(options, forKey: .options) try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex) + try container.encode(showInlineLabel, forKey: .showInlineLabel) + try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement) + try container.encodeModelIfPresent(action, forKey: .action) + try container.encodeIfPresent(tooltip, forKey: .tooltip) + try container.encode(transparentBackground, forKey: .transparentBackground) + try container.encodeIfPresent(width, forKey: .width) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift index bc3e9617..95f15e06 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift @@ -315,7 +315,9 @@ import UIKit self.showError = false } self.isEnabled = model.enabled - self.text = model.text + if let text = model.text, !text.isEmpty { + self.text = model.text + } }) } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift new file mode 100644 index 00000000..51cbd26c --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift @@ -0,0 +1,347 @@ +// +// InputEntryField.swift +// MVMCoreUI +// +// Created by Matt Bruce on 7/16/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +@objcMembers open class InputEntryField: VDS.InputField, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol { + + //------------------------------------------------------ + // MARK: - Properties + //------------------------------------------------------ + open var viewModel: TextEntryFieldModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + var fieldKey: String? + var fieldValue: JSONValue? + var groupName: String? + + //-------------------------------------------------- + // MARK: - Stored Properties + //-------------------------------------------------- + public var isValid: Bool = true + + /// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well. + private weak var proprietorTextDelegate: UITextFieldDelegate? + + private var isEditting: Bool = false { + didSet { + viewModel.selected = isEditting + } + } + + //-------------------------------------------------- + // MARK: - Stored Properties + //-------------------------------------------------- + + private var observingForChange: Bool = false + + /// Validate when user resigns editing. Default: true + open var validateWhenDoneEditing: Bool = true + + open var shouldMaskWhileRecording: Bool { + return viewModel.shouldMaskRecordedView ?? false + } + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + /// The text of this TextField. + open override var text: String? { + didSet { + viewModel?.text = text + } + } + + open override var errorText: String? { + get { + viewModel.dynamicErrorMessage ?? viewModel.errorMessage + } + set {} + } + + /// Placeholder access for the TextField. + public var placeholder: String? { + get { textField.placeholder } + set { textField.placeholder = newValue } + } + + //-------------------------------------------------- + // MARK: - Delegate Properties + //-------------------------------------------------- + /// The delegate and block for validation. Validates if the text that the user has entered. + public weak var observingTextFieldDelegate: ObservingTextFieldDelegate? + + /// If you're using a ViewController, you must set this to it + open weak var uiTextFieldDelegate: UITextFieldDelegate? + { + get { textField.delegate } + set { + textField.delegate = self + proprietorTextDelegate = newValue + } + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func setup() { + super.setup() + //turn off internal required rule + useRequiredRule = false + + publisher(for: .valueChanged) + .sink { [weak self] control in + guard let self else { return } + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + if (viewModel.type == .email) { + // remove spaces (either user entered Or auto-correct suggestion) for the email field + text = textField.text?.replacingOccurrences(of: " ", with: "") + } + }.store(in: &subscribers) + + textField + .publisher(for: .editingDidBegin) + .sink { [weak self] textView in + guard let self else { return } + isEditting = true + if viewModel.clearTextOnTap { + text = "" + } + }.store(in: &subscribers) + + textField + .publisher(for: .editingDidEnd) + .sink { [weak self] textView in + guard let self else { return } + isEditting = false + if validateWhenDoneEditing, let valid = viewModel.isValid { + updateValidation(valid) + } + regexTextFieldOutputIfAvailable() + + }.store(in: &subscribers) + + } + + open override func updateView() { + super.updateView() + + if let viewModel { + switch viewModel.type { + case .secure: + textField.isSecureTextEntry = true + textField.shouldMaskWhileRecording = true + + case .numberSecure: + textField.isSecureTextEntry = true + textField.shouldMaskWhileRecording = true + textField.keyboardType = .numberPad + + case .email: + textField.keyboardType = .emailAddress + + case .securityCode, .creditCard, .password: + textField.shouldMaskWhileRecording = true + + default: + break; + } + + // Override the preset keyboard set in type. + if let keyboardType = viewModel.assignKeyboardType() { + textField.keyboardType = keyboardType + } + } + } + + open func viewModelDidUpdate() { + + fieldType = viewModel.type.toVDSFieldType() + text = viewModel.text + placeholder = viewModel.placeholder + + labelText = viewModel.title + helperText = viewModel.feedback + isEnabled = viewModel.enabled + isReadOnly = viewModel.readOnly + isRequired = viewModel.required + tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + width = viewModel.width + transparentBackground = viewModel.transparentBackground + + containerView.accessibilityIdentifier = model.accessibilityIdentifier + textField.textAlignment = viewModel.textAlignment + textField.enableClipboardActions = viewModel.enableClipboardActions + textField.placeholder = viewModel.placeholder ?? "" + uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate + observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate + + if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected { + + viewModel.wasInitiallySelected = true + isEditting = true + } + + viewModel.rules = rules + + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + + if isEditting { + DispatchQueue.main.async { + _ = self.becomeFirstResponder() + } + } + + viewModel.updateUI = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + if isEditting { + updateValidation(viewModel.isValid ?? true) + + } else if viewModel.isValid ?? true && showError { + showError = false + } + isEnabled = viewModel.enabled + }) + } + + viewModel.updateUIDynamicError = { + MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in + guard let self = self else { return } + + let validState = viewModel.isValid ?? false + if !validState && viewModel.shouldClearText { + text = "" + viewModel.shouldClearText = false + } + updateValidation(validState) + }) + } + + //Added to override text when view is reloaded. + if let text = viewModel.text, !text.isEmpty { + regexTextFieldOutputIfAvailable() + } + } + + //-------------------------------------------------- + // MARK: - Observing for Change (TextFieldDelegate) + //-------------------------------------------------- + @objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) { + observingTextFieldDelegate = delegate + uiTextFieldDelegate = delegate + } + + func regexTextFieldOutputIfAvailable() { + + if let regex = viewModel?.displayFormat, + let mask = viewModel?.displayMask, + let finalText = text { + + let range = NSRange(finalText.startIndex..., in: finalText) + + if let regex = try? NSRegularExpression(pattern: regex) { + let maskedText = regex.stringByReplacingMatches(in: finalText, + range: range, + withTemplate: mask) + textField.text = maskedText + } + } + } + + @objc public func dismissFieldInput(_ sender: Any?) { + _ = resignFirstResponder() + } + + private func updateValidation(_ isValid: Bool) { + let previousValidity = self.isValid + self.isValid = isValid + + if previousValidity && !isValid { + showError = true + observingTextFieldDelegate?.isValid?(textfield: self) + } else if (!previousValidity && isValid) { + showError = false + observingTextFieldDelegate?.isInvalid?(textfield: self) + } + } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + @objc open func updateView(_ size: CGFloat) {} +} + +extension InputEntryField { + //-------------------------------------------------- + // MARK: - Implemented TextField Delegate + //-------------------------------------------------- + @discardableResult + @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true + } + + @objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) + ?? + super.textField(textField, shouldChangeCharactersIn: range, replacementString: string) + } + + @objc public override func textFieldDidBeginEditing(_ textField: UITextField) { + proprietorTextDelegate?.textFieldDidBeginEditing?(textField) ?? super.textFieldDidBeginEditing(textField) + } + + @objc public override func textFieldDidEndEditing(_ textField: UITextField) { + proprietorTextDelegate?.textFieldDidEndEditing?(textField) ?? super.textFieldDidEndEditing(textField) + } + + @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true + } + + @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true + } + + @objc public func textFieldShouldClear(_ textField: UITextField) -> Bool { + proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true + } +} + +// MARK: - Accessibility +extension InputEntryField { + + @objc open func pushAccessibilityNotification() { + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + UIAccessibility.post(notification: .layoutChanged, argument: containerView) + } + } +} + +internal struct ViewMasking { + static var shouldMaskWhileRecording: UInt8 = 0 +} + +extension VDS.TextField: ViewMaskingProtocol { + public var shouldMaskWhileRecording: Bool { + get { + return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false + } + set { + objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } +} + diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index 910712d5..401dfa6b 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -14,7 +14,7 @@ import MVMCore /** This class provides the convenience of formatting the MDN entered/displayer for the user. */ -@objcMembers open class MdnEntryField: TextEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate { +@objcMembers open class MdnEntryField: InputEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate { //-------------------------------------------------- // MARK: - Stored Properties //-------------------------------------------------- @@ -47,52 +47,17 @@ import MVMCore get { MVMCoreUIUtility.removeMdnFormat(text) } set { text = MVMCoreUIUtility.formatMdn(newValue) } } - - /// Toggles selected or original (unselected) UI. - public override var isSelected: Bool { - get { return entryFieldContainer.isSelected } - set (selected) { - if selected && showError { - showError = false - } - - super.isSelected = selected - } - } - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - - @objc public override init(frame: CGRect) { - super.init(frame: .zero) - } - - @objc public convenience init() { - self.init(frame: .zero) - } - - @objc required public init?(coder: NSCoder) { - super.init(coder: coder) - fatalError("MdnEntryField xib not supported.") - } - - required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.init(model: model, delegateObject, additionalData) - } - + //-------------------------------------------------- // MARK: - Setup //-------------------------------------------------- - @objc public override func setupFieldContainerContent(_ container: UIView) { - super.setupFieldContainerContent(container) - - textField.keyboardType = .numberPad + open override func setup() { + super.setup() + setupTextFieldToolbar() } - open override func setupTextFieldToolbar() { - + open func setupTextFieldToolbar() { let toolbar = UIToolbar.createEmptyToolbar() let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let contacts = UIBarButtonItem(title: MVMCoreUIUtility.hardcodedString(withKey: "textfield_contacts_barbutton"), style: .plain, target: self, action: #selector(getContacts)) @@ -103,40 +68,7 @@ import MVMCore //-------------------------------------------------- // MARK: - Methods - //-------------------------------------------------- - - @objc public func hasValidMDN() -> Bool { - - guard let MDN = mdn, !MDN.isEmpty else { return false } - - if isNationalMDN { - return MVMCoreUIUtility.validateMDNString(MDN) - } - - return MVMCoreUIUtility.validateInternationalMDNString(MDN) - } - - @objc public func validateMDNTextField() -> Bool { - - guard !shouldValidateMDN, let MDN = mdn, !MDN.isEmpty else { - isValid = true - return true - } - - isValid = hasValidMDN() - - if self.isValid { - showError = false - - } else { - entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message") - showError = true - UIAccessibility.post(notification: .layoutChanged, argument: textField) - } - - return isValid - } - + //-------------------------------------------------- @objc public func getContacts(_ sender: Any?) { let picker = CNContactPickerViewController() @@ -152,11 +84,12 @@ import MVMCore //-------------------------------------------------- // MARK: - MoleculeViewProtocol //-------------------------------------------------- - - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - - textField.keyboardType = .phonePad + public override func viewModelDidUpdate() { + viewModel.type = .phone + super.viewModelDidUpdate() + if let phoneNumber = viewModel.text { + text = phoneNumber.formatUSNumber() + } } //-------------------------------------------------- @@ -179,62 +112,47 @@ import MVMCore let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1) unformattedMDN = String(unformedMDN[startIndex...]) } - text = unformattedMDN textFieldShouldReturn(textField) textFieldDidEndEditing(textField) } } - + //-------------------------------------------------- // MARK: - Implemented TextField Delegate //-------------------------------------------------- @discardableResult - @objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool { - - textField.resignFirstResponder() - - return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true + @objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool { + _ = resignFirstResponder() + let superValue = super.textFieldShouldReturn(textField) + return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? superValue } - @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - - if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) { - return false - } - - return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true + @objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let superValue = super.textField(textField, shouldChangeCharactersIn: range, replacementString: string) + return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? superValue } - @objc public func textFieldDidBeginEditing(_ textField: UITextField) { - - textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text) + @objc public override func textFieldDidBeginEditing(_ textField: UITextField) { + super.textFieldDidBeginEditing(textField) proprietorTextDelegate?.textFieldDidBeginEditing?(textField) } - @objc public func textFieldDidEndEditing(_ textField: UITextField) { - + @objc public override func textFieldDidEndEditing(_ textField: UITextField) { proprietorTextDelegate?.textFieldDidEndEditing?(textField) - - if validateMDNTextField() { - if isNationalMDN { - textField.text = MVMCoreUIUtility.formatMdn(textField.text) - } - // Validate the base input field along with triggering form field validation rules. - validateText() - } + super.textFieldDidEndEditing(textField) } - @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + @objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true } - @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + @objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true } - @objc public func textFieldShouldClear(_ textField: UITextField) -> Bool { + @objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool { proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift index 53d0703d..f4b94922 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift @@ -12,4 +12,9 @@ //-------------------------------------------------- public override class var identifier: String { "mdnEntryField" } + + open override func formFieldServerValue() -> AnyHashable? { + guard let value = formFieldValue() as? String else { return nil } + return value.filter { $0.isNumber } + } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index 9d030d0f..d4b0539d 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -11,9 +11,9 @@ import UIKit @objc public protocol ObservingTextFieldDelegate { /// Called when the entered text becomes valid based on the validation block - @objc optional func isValid(textfield: TextEntryField?) + @objc optional func isValid(textfield: Any?) /// Called when the entered text becomes invalid based on the validation block - @objc optional func isInvalid(textfield: TextEntryField?) + @objc optional func isInvalid(textfield: Any?) /// Dismisses the keyboard. @objc optional func dismissFieldInput(_ sender: Any?) } @@ -317,9 +317,9 @@ import UIKit super.shouldShowError(showError) if showError { - observingTextFieldDelegate?.isValid?(textfield: self) - } else { observingTextFieldDelegate?.isInvalid?(textfield: self) + } else { + observingTextFieldDelegate?.isValid?(textfield: self) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift index 053a5ac5..cb40ae9b 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift @@ -5,9 +5,10 @@ // Created by Kevin Christiano on 1/22/20. // Copyright © 2020 Verizon Wireless. All rights reserved. // +import VDS - -@objcMembers open class TextEntryFieldModel: EntryFieldModel { +@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatableProtocol { + //-------------------------------------------------- // MARK: - Types //-------------------------------------------------- @@ -20,6 +21,39 @@ case email case text case phone + + //additional + case inlineAction + case creditCard + case date + case securityCode + + public func toVDSFieldType() -> VDS.InputField.FieldType { + switch self { + case .password: + .password + case .secure: + .text + case .number: + .number + case .numberSecure: + .number + case .email: + .text + case .text: + .text + case .phone: + .telephone + case .inlineAction: + .inlineAction + case .creditCard: + .creditCard + case .date: + .date + case .securityCode: + .securityCode + } + } } //-------------------------------------------------- @@ -33,12 +67,21 @@ public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3) public var textAlignment: NSTextAlignment = .left public var keyboardOverride: String? - public var type: EntryType? + public var type: EntryType = .text public var clearTextOnTap: Bool = false public var displayFormat: String? public var displayMask: String? public var enableClipboardActions: Bool = true + public var tooltip: TooltipModel? + public var transparentBackground: Bool = false + public var width: CGFloat? + + //-------------------------------------------------- + // MARK: - FormFieldInternalValidatableProtocol + //-------------------------------------------------- + open var rules: [AnyRule]? + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -114,6 +157,9 @@ case displayFormat case displayMask case enableClipboardActions + case tooltip + case transparentBackground + case width } //-------------------------------------------------- @@ -128,7 +174,7 @@ displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat) keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride) displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask) - type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) + type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) ?? .text if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) { self.clearTextOnTap = clearTextOnTap @@ -149,6 +195,10 @@ if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) { self.enableClipboardActions = enableClipboardActions } + + tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip) + transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false + width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width) } open override func encode(to encoder: Encoder) throws { @@ -164,5 +214,8 @@ try container.encode(disabledTextColor, forKey: .disabledTextColor) try container.encode(clearTextOnTap, forKey: .clearTextOnTap) try container.encode(enableClipboardActions, forKey: .enableClipboardActions) + try container.encodeIfPresent(tooltip, forKey: .tooltip) + try container.encode(transparentBackground, forKey: .transparentBackground) + try container.encodeIfPresent(width, forKey: .width) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift index b739baf7..e04cc47e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift @@ -53,4 +53,15 @@ open class BadgeModel: MoleculeModelProtocol { try container.encode(numberOfLines, forKey: .numberOfLines) try container.encodeIfPresent(maxWidth, forKey: .maxWidth) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? BadgeModel else { return false } + return self.backgroundColor == model.backgroundColor + && self.fillColor == model.fillColor + && self.numberOfLines == model.numberOfLines + && self.text == model.text + && self.surface == model.surface + && self.accessibilityText == model.accessibilityText + && self.maxWidth == model.maxWidth + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift new file mode 100644 index 00000000..835a1def --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -0,0 +1,128 @@ +// +// CircularProgressBar.swift +// MVMCoreUI +// +// Created by Xi Zhang on 7/5/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import UIKit + +@objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol { + + var heightConstraint: NSLayoutConstraint? + var graphModel: CircularProgressBarModel? { + return model as? CircularProgressBarModel + } + + var viewWidth: CGFloat { + graphModel?.diameter ?? CGFloat(64) + } + + private var progressLayer = CAShapeLayer() + private var tracklayer = CAShapeLayer() + private var labelLayer = CATextLayer() + + var progressColor: UIColor = UIColor.red + var trackColor: UIColor = UIColor.lightGray + + // A path with which CAShapeLayer will be drawn on the screen + private var viewCGPath: CGPath? { + + let width = viewWidth + let height = width + + return UIBezierPath(arcCenter: CGPoint(x: width / 2.0, y: height / 2.0), + radius: (width - 1.5)/2, + startAngle: CGFloat(-0.5 * Double.pi), + endAngle: CGFloat(1.5 * Double.pi), clockwise: true).cgPath + } + +// MARK: setup + override open func setupView() { + super.setupView() + heightConstraint = heightAnchor.constraint(equalToConstant: 0) + heightConstraint?.isActive = true + widthAnchor.constraint(equalTo: heightAnchor).isActive = true + } + + override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + + super.set(with: model, delegateObject, additionalData) + guard let model = model as? CircularProgressBarModel else { return } + + // set background color + backgroundColor = model.backgroundColor?.uiColor ?? UIColor.clear + + configureProgressViewToBeCircular() + + // set progress color + progressColor = model.color?.uiColor ?? .red + progressLayer.strokeColor = progressColor.cgColor + + // set track color + trackColor = model.trackColor?.uiColor ?? .lightGray + tracklayer.strokeColor = trackColor.cgColor + + // show circular progress view with animation. + showProgressWithAnimation(duration: graphModel?.duration ?? 0, value: Float(graphModel?.percent ?? 0) / 100) + + // show progress percentage label. + if let drawText = model.drawText, drawText { + showProgressPercentage() + } + } + + private func configureProgressViewToBeCircular() { + let lineWidth = graphModel?.lineWidth ?? 4.0 + + self.drawShape(using: tracklayer, lineWidth: lineWidth) + self.drawShape(using: progressLayer, lineWidth: lineWidth) + } + + private func drawShape(using shape: CAShapeLayer, lineWidth: CGFloat) { + shape.path = self.viewCGPath + shape.fillColor = UIColor.clear.cgColor + shape.lineWidth = lineWidth + self.layer.addSublayer(shape) + } + + // value range is [0,1] + private func showProgressWithAnimation(duration: TimeInterval, value: Float) { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = duration + + animation.fromValue = 0 //start animation at point 0 + animation.toValue = value //end animation at point specified + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + progressLayer.strokeEnd = CGFloat(value) + progressLayer.add(animation, forKey: "animateCircle") + } + + private func showProgressPercentage() { + + let percent = graphModel?.percent ?? 0 + let percentLen = String(percent).count + + // configure attributed string for progress percentage. + let attributedString = NSMutableAttributedString(string: String(percent) + "%") + // percent value + attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldTitleLarge()], range: NSMakeRange(0, percentLen)) + // % symbol + attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldBodyLarge()], range: NSMakeRange(percentLen, 1)) + + // show progress percentage in a text layer + let width = viewWidth + let height = width + labelLayer.string = attributedString + labelLayer.frame = CGRectMake((width - CGFloat(percentLen * 20))/2, (height - 30)/2, 60, 30) + self.layer.addSublayer(labelLayer) + } + + +//MARK: MVMCoreUIViewConstrainingProtocol + public func needsToBeConstrained() -> Bool { + return true + } +} + diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift new file mode 100644 index 00000000..b2f37e68 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -0,0 +1,119 @@ +// +// CircularProgressBarModel.swift +// MVMCoreUI +// +// https://oneconfluence.verizon.com/display/MFD/Circular+Progress+Tracker +// +// Created by Xi Zhang on 7/5/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class CircularProgressBarModel: GraphSizeBase, MoleculeModelProtocol { + + public static var identifier: String = "circularProgress" + public var id: String = UUID().uuidString + + public var percent: Int = 0 + public var diameter: CGFloat? = 64 + public var lineWidth: CGFloat? = 4 + public var duration : Double? = 0 + public var color: Color? = Color(uiColor: UIColor.mfGet(forHex: "#007AB8")) + public var trackColor: Color? = Color(uiColor: .mvmCoolGray3) + public var drawText: Bool? = true + public var backgroundColor: Color? = Color(uiColor: UIColor.clear) + + public override init() { + super.init() + updateSize() + } + + private enum CodingKeys: String, CodingKey { + case id + case moleculeName + case percent + case size + case diameter + case lineWidth + case duration + case color + case trackColor + case drawText + case backgroundColor + } + + required public init(from decoder: Decoder) throws { + + super.init() + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + + percent = try typeContainer.decode(Int.self, forKey: .percent) + + if let size = try typeContainer.decodeIfPresent(GraphSize.self, forKey: .size) { + self.size = size + } + updateSize() + + if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) { + self.diameter = diameter + } + + if let lineWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .lineWidth) { + self.lineWidth = lineWidth + } + + if let duration = try typeContainer.decodeIfPresent(Double.self, forKey: .duration) { + self.duration = duration + } + + if let drawText = try typeContainer.decodeIfPresent(Bool.self, forKey: .drawText) { + self.drawText = drawText + } + + if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) { + self.color = color + } + + if let trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) { + self.trackColor = trackColor + } + + if let backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) { + self.backgroundColor = backgroundColor + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(percent, forKey: .percent) + try container.encodeIfPresent(size, forKey: .size) + try container.encodeIfPresent(diameter, forKey: .diameter) + try container.encodeIfPresent(lineWidth, forKey: .lineWidth) + try container.encodeIfPresent(duration, forKey: .duration) + try container.encodeIfPresent(drawText, forKey: .drawText) + try container.encodeIfPresent(trackColor, forKey: .trackColor) + try container.encodeIfPresent(color, forKey: .color) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + } + + public override func updateSize() { + switch size { + case .small: + diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + break + case .medium: + diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + break + case .large: + diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + break + } + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift b/MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift new file mode 100644 index 00000000..13000706 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift @@ -0,0 +1,29 @@ +// +// GraphSizeProtocol.swift +// MVMCoreUI +// +// Created by Xi Zhang on 7/15/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation + +public enum GraphSize: String, Codable { + case small, medium, large +} + +public protocol GraphSizeProtocol { + var size: GraphSize { get set } + func updateSize() +} + +public class GraphSizeBase: GraphSizeProtocol { + public var size: GraphSize = .small { + didSet { + updateSize() + } + } + + public func updateSize() { + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift index 4fed14cb..416a1ac2 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift @@ -8,15 +8,11 @@ import UIKit -public enum GraphSize: String, Codable { - case small, medium, large -} - public enum GraphStyle: String, Codable { case unlimited, safetyMode } -public class WheelModel: MoleculeModelProtocol { +public class WheelModel: GraphSizeBase, MoleculeModelProtocol { public static var identifier: String = "wheel" public var id: String = UUID().uuidString @@ -27,11 +23,6 @@ public class WheelModel: MoleculeModelProtocol { } } - public var size: GraphSize = .small { - didSet { - updateSize() - } - } public var diameter: CGFloat = 24 public var lineWidth: CGFloat = 5 public var clockwise: Bool = true @@ -39,7 +30,8 @@ public class WheelModel: MoleculeModelProtocol { public var colors = [Color]() public var backgroundColor: Color? - public init() { + public override init() { + super.init() updateStyle() updateSize() } @@ -58,6 +50,7 @@ public class WheelModel: MoleculeModelProtocol { } required public init(from decoder: Decoder) throws { + super.init() let typeContainer = try decoder.container(keyedBy: CodingKeys.self) id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString @@ -123,7 +116,7 @@ public class WheelModel: MoleculeModelProtocol { } } - func updateSize() { + public override func updateSize() { switch size { case .small: diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20 diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index 943f53ad..22ec9bcd 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -108,8 +108,7 @@ extension TabsListItemModel: AddMolecules { public func moleculesToAdd() -> AddMolecules.AddParameters? { guard addedMolecules == nil else { return nil } let index = tabs.selectedIndex - guard molecules.count >= index else { return nil } - let addedMolecules = molecules[index] + guard let addedMolecules = molecules[safe: index] else { return nil } self.addedMolecules = addedMolecules return (addedMolecules, .below) } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 3ef117da..f84bdfc5 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -43,6 +43,9 @@ open class Carousel: View { /// The models for the molecules. public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]? + /// A list of currently registered cells. + public var registeredMoleculeIds: [String]? + /// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%. public var itemAlignment = UICollectionView.ScrollPosition.left @@ -174,9 +177,7 @@ open class Carousel: View { MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)") if #available(iOS 15.0, *) { - if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel), - originalModel.visibleMolecules.isVisuallyEquivalent(to: molecules ?? []) // Since the carousel model's children are in place replaced and we do not have a deep copy of this model tree, add in this hack to check if the prior captured carousel items match the newly visible ones. - { + if hasSameCellRegistration(with: carouselModel, delegateObject: delegateObject) { // Prevents a carousel reset while still updating the cell backing data through reconfigureItems. MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is visually equivalent. Skipping rebuild...") prepareMolecules(with: carouselModel) @@ -209,6 +210,7 @@ open class Carousel: View { registerCells(with: carouselModel, delegateObject: delegateObject) prepareMolecules(with: carouselModel) + pageIndex = 0 FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate) setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject) @@ -249,8 +251,6 @@ open class Carousel: View { } else { loop = false } - - pageIndex = 0 } open override func reset() { @@ -284,12 +284,29 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { - - for molecule in carouselModel.molecules { + var registeredIds = [String]() + for molecule in carouselModel.visibleMolecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + registeredIds.append(info.identifier) + } else { + registeredIds.append(molecule.moleculeName) } } + registeredMoleculeIds = registeredIds + } + + func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + guard let registeredMoleculeIds else { return false } + + let incomingIds = carouselModel.visibleMolecules.map { molecule in + if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { + return info.identifier + } else { + return molecule.moleculeName + } + } + return incomingIds == registeredMoleculeIds } //-------------------------------------------------- @@ -361,7 +378,7 @@ open class Carousel: View { } func trackSwipeActionAnalyticsforIndex(_ index : Int){ - guard let itemModel = molecules?[index], + guard let itemModel = molecules?[safe:index], let viewControllerObject = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return } MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: viewControllerObject, actionInformation: itemModel.toJSON(), additionalData: nil) } diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index 4bd889bd..c85e7b85 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -52,8 +52,8 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController, Rotor bottomView.updateView(width) showFooter(width) } - tableView.visibleCells.forEach { cell in - (cell as? MVMCoreViewProtocol)?.updateView(width) + MVMCoreUIUtility.findParentViews(by: (UITableViewCell & MVMCoreViewProtocol).self, views: tableView.subviews).forEach { view in + view.updateView(width) } } diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 78191da5..043627e6 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -8,6 +8,7 @@ import UIKit import MVMCore +import Combine @objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol { @@ -38,7 +39,7 @@ import MVMCore public var behaviors: [PageBehaviorProtocol]? public var needsUpdateUI = false - private var observingForResponses: NSObjectProtocol? + private var observingForResponses: AnyCancellable? private var initialLoadFinished = false public var isFirstRender = true public var previousScreenSize = CGSize.zero @@ -66,9 +67,28 @@ import MVMCore (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) else { return } - observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in - self?.responseJSONUpdated(notification: notification) - } + observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) + .receive(on: self.pageUpdateQueue) // Background serial queue. + .compactMap { [weak self] notification in + self?.pullUpdates(from: notification) ?? nil + } + // Merge all page and module updates into one update event. + .scan((nil, nil, nil)) { accumulator, next in + // Always take the latest page and the latest modules with same key. + return (next.0 ?? accumulator.0, next.1 ?? accumulator.1, next.2?.mergingRight(accumulator.2 ?? [:])) + } + // Delay allowing the previous model update to settle before triggering a re-render. + .throttle(for: .seconds(0.25), scheduler: RunLoop.main, latest: true) + .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in + guard let self = self else { return } + if let pageUpdates, pageModel != nil { + self.loadObject?.pageJSON = pageUpdates + } + let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) + self.loadObject?.modulesJSON = mergedModuleUpdates + self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page.") + self.handleNewData(pageModel) + } } open func stopObservingForResponseJSONUpdates() { @@ -77,6 +97,31 @@ import MVMCore self.observingForResponses = nil } + func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)? { + // Get the page data. + let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:]) + // Convert the page data into a new model. + var pageModel: PageModelProtocol? = nil + if let pageUpdates { + do { + // TODO: Rewiring to parse from plain JSON rather than this protocol indirection. + pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates) + } catch { + if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") { + MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) + } + } + } + // Get the module data. + let moduleUpdates = extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) + debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")") + + guard (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) else { return nil } + + // Bundle the transformations. + return (pageUpdates, pageModel, moduleUpdates) + } + open func pagesToListenFor() -> [String]? { guard let pageType = loadObject?.pageType else { return nil } return [pageType] @@ -88,51 +133,22 @@ import MVMCore return requestModules + behaviorModules } - @objc open func responseJSONUpdated(notification: Notification) { - // Checks for a page we are listening for. - var hasDataUpdate = false - var pageModel: PageModelProtocol? = nil - if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap), - let loadObject, - let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in - guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), + private func extractInterestedPageType(from pageMap: [String: Any]) -> [String: Any]? { + guard let pageType = pagesToListenFor()?.first(where: { pageTypeListened -> Bool in + guard let page = pageMap.optionalDictionaryForKey(pageTypeListened), let pageType = page.optionalStringForKey(KeyPageType), pageType == pageTypeListened else { return false } - return true - }) { - hasDataUpdate = true - loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) - - // Separate page updates from the module updates to avoid unecessary resets to behaviors and full re-renders. - do { - pageModel = try parsePageJSON(loadObject: loadObject) - } catch { - if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") { - MVMCoreLoggingHandler.shared()?.addError(toLog: coreError) - } - } + }) else { return nil } + return pageMap.optionalDictionaryForKey(pageType) + } + + private func extractInterestedModules(from moduleMap: [String: Any]) -> [String: Any]? { + guard let modulesListened = modulesToListenFor() else { return nil } + return moduleMap.filter { (key: String, value: Any) in + modulesListened.contains { $0 == key } } - - // Checks for modules we are listening for. - if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap), - let modulesListened = modulesToListenFor() { - for moduleName in modulesListened { - if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { - hasDataUpdate = true - var currentModules = loadObject?.modulesJSON ?? [:] - currentModules.updateValue(module, forKey: moduleName) - loadObject?.modulesJSON = currentModules - } - } - } - - guard hasDataUpdate else { return } - - MVMCoreDispatchUtility.performBlock(onMainThread: { - self.handleNewData(pageModel) - }) } open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer) -> Bool { diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 22f65ade..9524c116 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -41,7 +41,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self) // MARK:- Entry Field - ModelRegistry.register(handler: TextEntryField.self, for: TextEntryFieldModel.self) + ModelRegistry.register(handler: InputEntryField.self, for: TextEntryFieldModel.self) ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self) ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self) ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self) @@ -67,6 +67,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: LoadImageView.self, for: ImageViewModel.self) ModelRegistry.register(handler: Line.self, for: LineModel.self) ModelRegistry.register(handler: Wheel.self, for: WheelModel.self) + ModelRegistry.register(handler: CircularProgressBar.self, for: CircularProgressBarModel.self) ModelRegistry.register(handler: Toggle.self, for: ToggleModel.self) ModelRegistry.register(handler: CheckboxLabel.self, for: CheckboxLabelModel.self) ModelRegistry.register(handler: Arrow.self, for: ArrowModel.self) diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift index ea91f62b..5b70412e 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift +++ b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift @@ -60,6 +60,16 @@ public extension MVMCoreUIUtility { return findViews(by: type, views: queue, excludedViews: excludedViews) + matching } + static func findParentViews(by type: T.Type, views: [UIView]) -> [T] { + return views.reduce(into: [T]()) { matchingViews, view in + if let view = view as? T { + return matchingViews.append(view) // If this view is the type stop here and return, ignoring its children. + } + // Otherwise check downstream. + matchingViews += findParentViews(by: type, views: view.subviews) + } + } + @MainActor static func visibleNavigationBarStlye() -> NavigationItemStyle? { if let navController = NavigationController.navigationController(),