Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui.git into feature/atomic-vds-textArea

This commit is contained in:
Matt Bruce 2024-07-30 13:36:09 -05:00
commit 344b2f18aa
21 changed files with 1045 additions and 253 deletions

View File

@ -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 = "<group>"; };
4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageRenderingMode+Extension.swift"; sourceTree = "<group>"; };
4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDropdownEntryFieldModel+Extension.swift"; sourceTree = "<group>"; };
4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBarModel.swift; sourceTree = "<group>"; };
4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSizeProtocol.swift; sourceTree = "<group>"; };
522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinks.swift; sourceTree = "<group>"; };
522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinksModel.swift; sourceTree = "<group>"; };
52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextAllTextAndLinks.swift; sourceTree = "<group>"; };
@ -1201,6 +1208,7 @@
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = "<group>"; };
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEntryField.swift; sourceTree = "<group>"; };
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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

View File

@ -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]
}
}

View File

@ -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)
}
}

View File

@ -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
}
})
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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 }
}
}

View File

@ -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)
}
}

View File

@ -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<String>]?
//--------------------------------------------------
// 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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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() {
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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<MVMCoreErrorObject?>) -> Bool {

View File

@ -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)

View File

@ -60,6 +60,16 @@ public extension MVMCoreUIUtility {
return findViews(by: type, views: queue, excludedViews: excludedViews) + matching
}
static func findParentViews<T>(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(),