From 525f0f8f0abbb84d8184dac91f616181edd827f6 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 09:13:49 -0400 Subject: [PATCH 01/38] Digital PCT265 defect CXTDT-579049: Add indexing safety to track action call. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 3ef117da..fc79b5bd 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -361,7 +361,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) } From 080f77581f6d48a4cc21a646cd481bc7e7c5ff64 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 09:18:13 -0400 Subject: [PATCH 02/38] Digital PCT265 defect CXTDT-579049: Fix indexing safety to mocules to add. (Another crash in App Store reports.) --- MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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) } From 91eb4fa87a793c141c22563a9540e541460f37e9 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 28 Jun 2024 15:23:17 -0400 Subject: [PATCH 03/38] Digital PCT265 defect CXTDT-579050: Prevent updateViews from triggering tableView(_:cellForRowAt:) through visibleCells. --- .../ThreeLayerTableViewController.swift | 4 ++-- MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) 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/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(), From d8d4b37d1d423d2f4b7bf011d095183f2f788e02 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 2 Jul 2024 19:05:53 -0400 Subject: [PATCH 04/38] Digital PCT265 defect CXTDT-579050: Rewire asynchronous response JSON parsing to filter, queue, and delay drop UI changes. --- .../BaseControllers/ViewController.swift | 97 ++++++++++--------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 78191da5..e67188a0 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,46 @@ 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. + .map { [weak self] notification in + guard let self = self else { return (nil, nil, nil) } + // Get the page data. + let pageUpdates = self.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 = self.extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) + // Bundle the transformations. + return (pageUpdates, pageModel, moduleUpdates) + } + .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in + // Skip any non-updates. + (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) + } + // Opportunity: Merge all module only updates into one event. + // Delay allowing the previous model update to settle before triggering a re-render. + .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest) + .flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(0.1), scheduler: RunLoop.main) } + .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in + guard let self = self else { return } + if let pageUpdates, let pageModel { + self.loadObject?.pageJSON = pageUpdates + } + var mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) + self.loadObject?.modulesJSON = mergedModuleUpdates + self.handleNewData(pageModel) + } } open func stopObservingForResponseJSONUpdates() { @@ -88,51 +126,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 { From 4cbf15a3a75e250c033e7abcd7150da276f8fa86 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 15:42:12 -0400 Subject: [PATCH 05/38] Digital PCT265 defect CXTDT-579050: Change batching to the UI. --- .../BaseControllers/ViewController.swift | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index e67188a0..950b330e 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -70,41 +70,34 @@ import Combine observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .receive(on: self.pageUpdateQueue) // Background serial queue. .map { [weak self] notification in - guard let self = self else { return (nil, nil, nil) } - // Get the page data. - let pageUpdates = self.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 = self.extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:]) - // Bundle the transformations. - return (pageUpdates, pageModel, moduleUpdates) + self?.pullUpdates(from: notification) ?? (nil, nil, nil) } .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in // Skip any non-updates. (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) } - // Opportunity: Merge all module only updates into one event. + // Merge all page and module updates into one update event. + //.print("[Update pipe] merging") + .scan((nil, nil, nil)) { accumulator, next in + // Always take the latest page and the latest modules with same key. + return (next.0, next.1, next.2?.mergingRight(accumulator.2 ?? [:])) + } + //.print("[Update pipe] into buffer") + // Hold onto the latest merged state until UI is ready for an update. Keep only the latest from scan. + .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) + //.print("[Update pipe] out of buffer") // Delay allowing the previous model update to settle before triggering a re-render. - .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest) - .flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(0.1), scheduler: RunLoop.main) } + .flatMap(maxPublishers: .max(1)) { buffer in + Just(buffer).delay(for: .seconds(0.1), scheduler: RunLoop.main) + } .sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in guard let self = self else { return } - if let pageUpdates, let pageModel { + if let pageUpdates, pageModel != nil { self.loadObject?.pageJSON = pageUpdates } - var mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:]) + 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) } } @@ -115,6 +108,28 @@ import Combine 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 ?? "")") + // Bundle the transformations. + return (pageUpdates, pageModel, moduleUpdates) + } + open func pagesToListenFor() -> [String]? { guard let pageType = loadObject?.pageType else { return nil } return [pageType] From 90f9a0bcf57cd7508d5971dbbbecaf761ee29d71 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:39:28 -0400 Subject: [PATCH 06/38] Digital PCT265 defect CXTDT-579050: Code simplification and fixes of combine pipeline for page updates. --- .../Atomic/Organisms/Carousel/Carousel.swift | 11 ++++++++ .../BaseControllers/ViewController.swift | 26 +++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index fc79b5bd..84124880 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -285,6 +285,17 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { + for molecule in carouselModel.molecules { + if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { + collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + } + } + let moleculeInfo = carouselModel.molecules.map { + getMoleculeInfo(with: $0, delegateObject: delegateObject) + } + // For each molecule with info, register it. + moleculeInfo.compactMap({ $0 }).forEach { collectionView.register($0.class, forCellWithReuseIdentifier: $0.identifier) } + for molecule in carouselModel.molecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 950b330e..a6c43b2d 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -69,27 +69,16 @@ import Combine observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded)) .receive(on: self.pageUpdateQueue) // Background serial queue. - .map { [weak self] notification in - self?.pullUpdates(from: notification) ?? (nil, nil, nil) - } - .filter { (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in - // Skip any non-updates. - (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) + .compactMap { [weak self] notification in + self?.pullUpdates(from: notification) ?? nil } // Merge all page and module updates into one update event. - //.print("[Update pipe] merging") .scan((nil, nil, nil)) { accumulator, next in // Always take the latest page and the latest modules with same key. - return (next.0, next.1, next.2?.mergingRight(accumulator.2 ?? [:])) + return (next.0 ?? accumulator.0, next.1 ?? accumulator.1, next.2?.mergingRight(accumulator.2 ?? [:])) } - //.print("[Update pipe] into buffer") - // Hold onto the latest merged state until UI is ready for an update. Keep only the latest from scan. - .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest) - //.print("[Update pipe] out of buffer") // Delay allowing the previous model update to settle before triggering a re-render. - .flatMap(maxPublishers: .max(1)) { buffer in - Just(buffer).delay(for: .seconds(0.1), scheduler: RunLoop.main) - } + .throttle(for: .seconds(0.05), 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 { @@ -108,7 +97,7 @@ import Combine self.observingForResponses = nil } - func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) { + 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. @@ -125,7 +114,10 @@ import Combine } // 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 ?? "")") + 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) } From b2ad684f0081838c258af082fe0f7b0540f5e98c Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:40:43 -0400 Subject: [PATCH 07/38] Digital PCT265 defect CXTDT-579050: Carousel cell registration check. --- .../Atomic/Organisms/Carousel/Carousel.swift | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 84124880..6df0f7c2 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) @@ -284,23 +285,27 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { - + var registeredIds = [String]() for molecule in carouselModel.molecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + registeredIds.append(info.identifier) + } else { + registeredIds.append(molecule.moleculeName) } } - let moleculeInfo = carouselModel.molecules.map { - getMoleculeInfo(with: $0, delegateObject: delegateObject) - } - // For each molecule with info, register it. - moleculeInfo.compactMap({ $0 }).forEach { collectionView.register($0.class, forCellWithReuseIdentifier: $0.identifier) } - - for molecule in carouselModel.molecules { + registeredMoleculeIds = registeredIds + } + + func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + let incomingIds = carouselModel.molecules.map { molecule in if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { - collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) + return info.identifier + } else { + return molecule.moleculeName } } + return incomingIds == registeredMoleculeIds } //-------------------------------------------------- From cae92360a254a8a31975b4f7e3f9eb5f6959c7c7 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Wed, 3 Jul 2024 18:45:29 -0400 Subject: [PATCH 08/38] Digital PCT265 defect CXTDT-579050: Carousel cell registration check break early. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index 6df0f7c2..ad627d35 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -298,6 +298,8 @@ open class Carousel: View { } func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + guard let registeredMoleculeIds else { return false } + let incomingIds = carouselModel.molecules.map { molecule in if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { return info.identifier From 7fec6f540e944e02ce3a30714c8a9b0c1752e11c Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Fri, 5 Jul 2024 15:10:34 -0400 Subject: [PATCH 09/38] Add support for circular progress bar. --- MVMCoreUI.xcodeproj/project.pbxproj | 8 ++ .../Atoms/Views/CircularProgressBar.swift | 75 ++++++++++++++ .../Views/CircularProgressBarModel.swift | 99 +++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift create mode 100644 MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 3433e17b..0a1ea43d 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -153,6 +153,8 @@ 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 */; }; 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 */; }; @@ -770,6 +772,8 @@ 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 = ""; }; 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 = ""; }; @@ -2311,6 +2315,8 @@ 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */, D28A838223CCBD3F00DFE4FC /* WheelModel.swift */, 943784F3236B77BB006A1E82 /* Wheel.swift */, + 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */, + 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */, 943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */, 0AE98BB623FF18E9004C5109 /* ArrowModel.swift */, 0AE98BB423FF18D2004C5109 /* Arrow.swift */, @@ -3005,6 +3011,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 */, @@ -3025,6 +3032,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 */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift new file mode 100644 index 00000000..9383425a --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -0,0 +1,75 @@ +// +// 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? + weak var gradientLayer: CALayer? + var graphModel: CircularProgressBarModel? { + return model as? CircularProgressBarModel + } + +// MARK: setup + open override 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 } + createGraphCircle(model) + } + + class func getAngle(_ piValue: Double) -> Double { + return piValue / (2.0 * Double.pi) * 360.0 + } + + class func getPiValue(_ angle: Double) -> Double { + return angle / 360.0 * 2.0 * Double.pi + } + +// MARK: circle + open func createGraphCircle(_ graphObject: CircularProgressBarModel) { + if let sublayers = layer.sublayers { + for sublayer in sublayers { + sublayer.removeAllAnimations() + sublayer.removeFromSuperlayer() + } + } + heightConstraint?.constant = graphObject.diameter + + let gradient = CAGradientLayer() + gradient.type = .conic + gradient.startPoint = CGPoint(x: 0.5, y: 0.5) + gradient.endPoint = CGPoint(x: 0.5, y: 0.0) + gradient.frame = CGRect(x: 0, y: 0, width: graphObject.diameter, height: graphObject.diameter) + gradientLayer = gradient + layer.addSublayer(gradient) + + let center = CGPoint(x: gradient.bounds.midX, y: gradient.bounds.midY) + let radius = (graphObject.diameter - graphObject.lineWidth) / 2.0 + let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: (3 / 2 * .pi), endAngle: -(1 / 2 * .pi), clockwise: false) + let mask = CAShapeLayer() + mask.fillColor = UIColor.clear.cgColor + mask.strokeColor = UIColor.white.cgColor + mask.lineWidth = graphObject.lineWidth + mask.path = path.cgPath + gradient.mask = mask + } + +//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..671607b1 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -0,0 +1,99 @@ +// +// CircularProgressBarModel.swift +// MVMCoreUI +// +// Created by Xi Zhang on 7/5/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation + + +public class CircularProgressBarModel: MoleculeModelProtocol { + + public static var identifier: String = "circularProgress" + public var id: String = UUID().uuidString + + public var size: GraphSize = .small { + didSet { + updateSize() + } + } + public var diameter: CGFloat = 84 + public var lineWidth: CGFloat = 5 + public var color: Color? + public var backgroundColor: Color? + public var percent: Int? + + public init() { + updateSize() + } + + private enum CodingKeys: String, CodingKey { + case id + case size + case diameter + case lineWidth + case color + case backgroundColor + case percent + case moleculeName + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + + 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 + } + + color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + percent = try typeContainer.decodeIfPresent(Int.self, forKey: .percent) + } + + 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(size, forKey: .size) + try container.encode(diameter, forKey: .diameter) + try container.encode(lineWidth, forKey: .lineWidth) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(color, forKey: .color) + try container.encodeIfPresent(percent, forKey: .percent) + } + + func getCGColorsFromArray(_ colorArray: [String]) -> [Color] { + return colorArray.map { (colorString) -> Color in + return Color(uiColor: UIColor.mfGet(forHex: colorString)) + } + } + + func updateSize() { + switch size { + case .small: + diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + break + case .medium: + diameter = MFSizeObject(standardSize: 100)?.getValueBasedOnApplicationWidth() ?? 100 + lineWidth = MFSizeObject(standardSize: 8)?.getValueBasedOnApplicationWidth() ?? 8 + break + case .large: + diameter = MFSizeObject(standardSize: 180)?.getValueBasedOnApplicationWidth() ?? 180 + lineWidth = MFSizeObject(standardSize: 12)?.getValueBasedOnApplicationWidth() ?? 12 + break + } + } +} From 1b0197ed2c180a4584e11ef6cd80fed3edb67c75 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Tue, 9 Jul 2024 18:49:10 -0400 Subject: [PATCH 10/38] Add support for circular progress bar part 2 --- .../Atoms/Views/CircularProgressBar.swift | 120 ++++++++++++------ .../Views/CircularProgressBarModel.swift | 12 +- .../OtherHandlers/CoreUIModelMapping.swift | 1 + 3 files changed, 90 insertions(+), 43 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index 9383425a..df5fe946 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -11,11 +11,82 @@ import UIKit @objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol { var heightConstraint: NSLayoutConstraint? - weak var gradientLayer: CALayer? var graphModel: CircularProgressBarModel? { return model as? CircularProgressBarModel } + private var progressLayer = CAShapeLayer() + private var tracklayer = CAShapeLayer() + private var labelLayer = CATextLayer() + + var setProgressColor: UIColor = UIColor.red { + didSet { + progressLayer.strokeColor = setProgressColor.cgColor + } + } + + var setTrackColor: UIColor = UIColor.white { + didSet { + tracklayer.strokeColor = setTrackColor.cgColor + } + } + /** + A path that consists of straight and curved line segments that you can render in your custom views. + Meaning our CAShapeLayer will now be drawn on the screen with the path we have specified here + */ + private var viewCGPath: CGPath? { + + let width = graphModel?.diameter ?? 84 + 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 + } + + private func configureProgressViewToBeCircular() { + let lineWidth = graphModel?.lineWidth ?? 2.0 + self.backgroundColor = UIColor.clear + + 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) + } + + func setProgressWithAnimation(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") + } + + func drawLabel() { + + let percent = graphModel?.percent ?? 0 + let percentLen = percent > 9 ? 2 : 1 + let attributedString = NSMutableAttributedString(string: String(percent) + "%") + attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldTitleLarge()], range: NSMakeRange(0, percentLen)) + attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldBodyLarge()], range: NSMakeRange(percentLen, 1)) + + // Text layer + let width = graphModel?.diameter ?? 84 + let height = width + labelLayer.string = attributedString + labelLayer.frame = CGRectMake((width - CGFloat(percentLen * 20))/2, (height - 30)/2, 60, 30) + self.layer.addSublayer(labelLayer) + } + // MARK: setup open override func setupView() { super.setupView() @@ -27,46 +98,21 @@ import UIKit 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 } - createGraphCircle(model) - } - - class func getAngle(_ piValue: Double) -> Double { - return piValue / (2.0 * Double.pi) * 360.0 - } - - class func getPiValue(_ angle: Double) -> Double { - return angle / 360.0 * 2.0 * Double.pi - } - -// MARK: circle - open func createGraphCircle(_ graphObject: CircularProgressBarModel) { - if let sublayers = layer.sublayers { - for sublayer in sublayers { - sublayer.removeAllAnimations() - sublayer.removeFromSuperlayer() - } + + configureProgressViewToBeCircular() + + if let color = model.color { + setProgressColor = color.uiColor } - heightConstraint?.constant = graphObject.diameter - let gradient = CAGradientLayer() - gradient.type = .conic - gradient.startPoint = CGPoint(x: 0.5, y: 0.5) - gradient.endPoint = CGPoint(x: 0.5, y: 0.0) - gradient.frame = CGRect(x: 0, y: 0, width: graphObject.diameter, height: graphObject.diameter) - gradientLayer = gradient - layer.addSublayer(gradient) + if let backgroundColor = model.backgroundColor { + setTrackColor = backgroundColor.uiColor + } - let center = CGPoint(x: gradient.bounds.midX, y: gradient.bounds.midY) - let radius = (graphObject.diameter - graphObject.lineWidth) / 2.0 - let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: (3 / 2 * .pi), endAngle: -(1 / 2 * .pi), clockwise: false) - let mask = CAShapeLayer() - mask.fillColor = UIColor.clear.cgColor - mask.strokeColor = UIColor.white.cgColor - mask.lineWidth = graphObject.lineWidth - mask.path = path.cgPath - gradient.mask = mask + setProgressWithAnimation(duration: 0.5, value: Float(graphModel?.percent ?? 0) / 100) + drawLabel() } - + //MARK: MVMCoreUIViewConstrainingProtocol public func needsToBeConstrained() -> Bool { return true diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 671607b1..3a865b62 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -83,16 +83,16 @@ public class CircularProgressBarModel: MoleculeModelProtocol { func updateSize() { switch size { case .small: - diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20 - lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64 + lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 break case .medium: - diameter = MFSizeObject(standardSize: 100)?.getValueBasedOnApplicationWidth() ?? 100 - lineWidth = MFSizeObject(standardSize: 8)?.getValueBasedOnApplicationWidth() ?? 8 + diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84 + lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 break case .large: - diameter = MFSizeObject(standardSize: 180)?.getValueBasedOnApplicationWidth() ?? 180 - lineWidth = MFSizeObject(standardSize: 12)?.getValueBasedOnApplicationWidth() ?? 12 + diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124 + lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 break } } diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 22f65ade..d310a126 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -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) From 144efea342b4b32df1b43103dd1d1b29c6366704 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 10 Jul 2024 12:54:50 -0500 Subject: [PATCH 11/38] added inverted Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift index 190d3a01..ca6f1e52 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift @@ -7,6 +7,7 @@ // import Foundation +import VDS @objcMembers open class FormFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol { @@ -28,6 +29,9 @@ import Foundation public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + + public var inverted: Bool = false + public var surface: Surface { inverted ? .dark : .light } public var dynamicErrorMessage: String? { didSet { @@ -66,6 +70,7 @@ import Foundation case required case fieldKey case groupName + case inverted } //-------------------------------------------------- @@ -103,6 +108,10 @@ import Foundation readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) ?? FormValidator.defaultGroupName + + if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) { + self.inverted = inverted + } } open func encode(to encoder: Encoder) throws { @@ -116,5 +125,6 @@ import Foundation try container.encode(readOnly, forKey: .readOnly) try container.encode(enabled, forKey: .enabled) try container.encode(required, forKey: .required) + try container.encode(inverted, forKey: .inverted) } } From 8bf4a28b0fd55be2d16fea3e5597f3a0c682fad7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 10 Jul 2024 14:36:47 -0500 Subject: [PATCH 12/38] added extension Signed-off-by: Matt Bruce --- MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift index 2b12cead..974cca8c 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift @@ -96,3 +96,17 @@ open class TooltipModel: MoleculeModelProtocol { } } + +extension TooltipModel { + public func toVDSTooltipModel() -> Tooltip.TooltipModel { + var moleculeView: MoleculeViewProtocol? + if let molecule, let view = ModelRegistry.createMolecule(molecule) { + moleculeView = view + } + return .init(closeButtonText: closeButtonText, + title: title, + content: content, + contentView: moleculeView + ) + } +} From 981bdac2922f2eb1505cce6b9a9db8844e1d0e30 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 10 Jul 2024 14:37:04 -0500 Subject: [PATCH 13/38] refactored to use VDS Dropdown Select Signed-off-by: Matt Bruce --- .../ItemDropdownEntryField.swift | 143 ++++++++++-------- .../ItemDropdownEntryFieldModel.swift | 12 +- 2 files changed, 91 insertions(+), 64 deletions(-) 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..734d39e8 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,41 @@ // import UIKit +import VDS +open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol { + //------------------------------------------------------ + // 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) }) + } + } -open class ItemDropdownEntryField: BaseItemPickerEntryField { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public var isValid: Bool = false + + /// Closure passed here will run as picker changes items. + public var observeDropdownChange: ((String?, String) -> ())? - open var pickerData: [String] = [] - - public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? { - model as? ItemDropdownEntryFieldModel - } + /// Closure passed here will run upon dismissing the selection picker. + public var observeDropdownSelection: ((String) -> ())? + /// When selecting for first responder, allow initial selected value to appear in empty text field. + public var setInitialValueInTextField = true + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -28,7 +50,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField { super.init(frame: frame) } - @objc public convenience init() { + @objc public convenience required init() { self.init(frame: .zero) } @@ -40,14 +62,36 @@ 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() + publisher(for: .valueChanged) + .sink { [weak self] control in + guard let self, let selectedItem else { return } + viewModel.selectedIndex = control.selectId + observeDropdownSelection?(selectedItem.text) + if let valid = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) { + isValid = valid + } + }.store(in: &subscribers) + + dropdownField + .publisher(for: .editingDidBegin) + .sink { [weak self] textField in + guard let self else { return } + setInitialValueFromPicker() + }.store(in: &subscribers) + + dropdownField + .publisher(for: .editingDidEnd) + .sink { [weak self] textField in + guard let self else { return } + performDropdownAction() + }.store(in: &subscribers) + } /// Sets the textField with the first value of the available picker data. @objc private func setInitialValueFromPicker() { @@ -55,61 +99,36 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField { 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() + public func viewModelDidUpdate() { + pickerData = viewModel.options + labelText = viewModel.title + helperText = viewModel.feedback + isEnabled = viewModel.enabled + isReadOnly = viewModel.readOnly + isRequired = viewModel.required + isSelected = viewModel.selected ?? false + tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + if let index = viewModel.selectedIndex { + selectId = index + optionsPicker.selectRow(index, inComponent: 0, animated: false) + pickerView(optionsPicker, didSelectRow: index, inComponent: 0) + } + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) } - @objc override func endInputing() { - super.endInputing() - - 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) - } + func performDropdownAction() { + guard let actionModel = viewModel.action, + !dropdownField.isFirstResponder + else { return } + MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject) } - //-------------------------------------------------- - // 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] - } + public func updateView(_ size: CGFloat) { } } 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..131c2123 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,18 @@ // 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 tooltip: TooltipModel? public init(with options: [String], selectedIndex: Int? = nil) { self.options = options @@ -42,6 +44,8 @@ private enum CodingKeys: String, CodingKey { case options case selectedIndex + case action + case tooltip } //-------------------------------------------------- @@ -58,6 +62,8 @@ self.selectedIndex = selectedIndex baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil } + action = try typeContainer.decodeModelIfPresent(codingKey: .action) + tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip) } public override func encode(to encoder: Encoder) throws { @@ -65,5 +71,7 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(options, forKey: .options) try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex) + try container.encodeModelIfPresent(action, forKey: .action) + try container.encodeIfPresent(tooltip, forKey: .tooltip) } } From 79d552999165b26d32de0b2d1c2618b710b80197 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 10 Jul 2024 14:51:10 -0500 Subject: [PATCH 14/38] added delegates Signed-off-by: Matt Bruce --- .../ItemDropdownEntryField.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 734d39e8..51504dc8 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 @@ -9,7 +9,7 @@ import UIKit import VDS -open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol { +open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate { //------------------------------------------------------ // MARK: - Properties //------------------------------------------------------ @@ -42,6 +42,23 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol { /// When selecting for first responder, allow initial selected value to appear in empty text field. public var setInitialValueInTextField = true + //-------------------------------------------------- + // 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 //-------------------------------------------------- From b1c59124a7237aa9c0fe804cca356d5426be6d45 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 11 Jul 2024 11:20:17 -0500 Subject: [PATCH 15/38] fixed bugs in validation to match textViewentryfield Signed-off-by: Matt Bruce --- .../ItemDropdownEntryField.swift | 115 ++++++++++++++---- 1 file changed, 89 insertions(+), 26 deletions(-) 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 51504dc8..ad38f476 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 @@ -27,11 +27,13 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, options = pickerData.compactMap({ DropdownOptionModel(text: $0) }) } } + + private var isEditting: Bool = false //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var isValid: Bool = false + public var isValid: Bool = true /// Closure passed here will run as picker changes items. public var observeDropdownChange: ((String?, String) -> ())? @@ -42,6 +44,12 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, /// 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 //-------------------------------------------------- @@ -90,15 +98,14 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, guard let self, let selectedItem else { return } viewModel.selectedIndex = control.selectId observeDropdownSelection?(selectedItem.text) - if let valid = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) { - isValid = valid - } - }.store(in: &subscribers) + _ = 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) @@ -106,12 +113,76 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, .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 + labelText = viewModel.title + helperText = viewModel.feedback + isEnabled = viewModel.enabled + isReadOnly = viewModel.readOnly + isRequired = viewModel.required + tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + + 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 } @@ -123,29 +194,21 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, } } - public func viewModelDidUpdate() { - pickerData = viewModel.options - labelText = viewModel.title - helperText = viewModel.feedback - isEnabled = viewModel.enabled - isReadOnly = viewModel.readOnly - isRequired = viewModel.required - isSelected = viewModel.selected ?? false - tooltipModel = viewModel.tooltip?.toVDSTooltipModel() - if let index = viewModel.selectedIndex { - selectId = index - optionsPicker.selectRow(index, inComponent: 0, animated: false) - pickerView(optionsPicker, didSelectRow: index, inComponent: 0) - } - FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) - } - - func performDropdownAction() { + private func performDropdownAction() { guard let actionModel = viewModel.action, !dropdownField.isFirstResponder else { return } MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject) } - - public func updateView(_ size: CGFloat) { } + + private func updateValidation(_ isValid: Bool) { + let previousValidity = self.isValid + self.isValid = isValid + + if previousValidity && !isValid { + showError = true + } else if (!previousValidity && isValid) { + showError = false + } + } } From 20d818b2c2df472e465bb563e89fdf7c6cbb91d7 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Thu, 11 Jul 2024 13:12:29 -0400 Subject: [PATCH 16/38] Refactor codes for circular progress UI. --- .../Atoms/Views/CircularProgressBar.swift | 98 ++++++++++++------- .../Views/CircularProgressBarModel.swift | 15 ++- 2 files changed, 71 insertions(+), 42 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index df5fe946..011359c8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -15,6 +15,10 @@ import UIKit return model as? CircularProgressBarModel } + var viewWidth: CGFloat { + graphModel?.diameter ?? CGFloat(84) + } + private var progressLayer = CAShapeLayer() private var tracklayer = CAShapeLayer() private var labelLayer = CATextLayer() @@ -25,18 +29,16 @@ import UIKit } } - var setTrackColor: UIColor = UIColor.white { + var setTrackColor: UIColor = UIColor.lightGray { didSet { tracklayer.strokeColor = setTrackColor.cgColor } } - /** - A path that consists of straight and curved line segments that you can render in your custom views. - Meaning our CAShapeLayer will now be drawn on the screen with the path we have specified here - */ + + // A path with which CAShapeLayer will be drawn on the screen private var viewCGPath: CGPath? { - let width = graphModel?.diameter ?? 84 + let width = viewWidth let height = width return UIBezierPath(arcCenter: CGPoint(x: width / 2.0, y: height / 2.0), @@ -45,9 +47,51 @@ import UIKit 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 + if let backgroundColor = model.backgroundColor { + self.backgroundColor = backgroundColor.uiColor + } else { + self.backgroundColor = UIColor.clear + } + + configureProgressViewToBeCircular() + + // set progress color + if let color = model.color { + setProgressColor = color.uiColor + } else { + setProgressColor = UIColor.red + } + + // set track color + if let trackColor = model.trackColor { + setTrackColor = trackColor.uiColor + } else { + setProgressColor = UIColor.lightGray + } + + // show circular progress view with animation. + showProgressWithAnimation(duration: 0.5, value: Float(graphModel?.percent ?? 0) / 100) + + // show progress percentage label. + showProgressPercentage() + } + private func configureProgressViewToBeCircular() { - let lineWidth = graphModel?.lineWidth ?? 2.0 - self.backgroundColor = UIColor.clear + let lineWidth = graphModel?.lineWidth ?? 5.0 self.drawShape(using: tracklayer, lineWidth: lineWidth) self.drawShape(using: progressLayer, lineWidth: lineWidth) @@ -60,7 +104,8 @@ import UIKit self.layer.addSublayer(shape) } - func setProgressWithAnimation(duration: TimeInterval, value: Float) { + // value range is [0,1] + private func showProgressWithAnimation(duration: TimeInterval, value: Float) { let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration @@ -71,47 +116,26 @@ import UIKit progressLayer.add(animation, forKey: "animateCircle") } - func drawLabel() { + private func showProgressPercentage() { let percent = graphModel?.percent ?? 0 let percentLen = percent > 9 ? 2 : 1 + + // 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)) - // Text layer - let width = graphModel?.diameter ?? 84 + // 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: setup - open override 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 } - - configureProgressViewToBeCircular() - - if let color = model.color { - setProgressColor = color.uiColor - } - - if let backgroundColor = model.backgroundColor { - setTrackColor = backgroundColor.uiColor - } - - setProgressWithAnimation(duration: 0.5, value: Float(graphModel?.percent ?? 0) / 100) - drawLabel() - } //MARK: MVMCoreUIViewConstrainingProtocol public func needsToBeConstrained() -> Bool { diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 3a865b62..6c58be43 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -22,9 +22,10 @@ public class CircularProgressBarModel: MoleculeModelProtocol { public var diameter: CGFloat = 84 public var lineWidth: CGFloat = 5 public var color: Color? - public var backgroundColor: Color? + public var trackColor: Color? public var percent: Int? - + public var backgroundColor: Color? = Color(uiColor: UIColor.clear) + public init() { updateSize() } @@ -35,8 +36,9 @@ public class CircularProgressBarModel: MoleculeModelProtocol { case diameter case lineWidth case color - case backgroundColor + case trackColor case percent + case backgroundColor case moleculeName } @@ -58,8 +60,10 @@ public class CircularProgressBarModel: MoleculeModelProtocol { } color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) percent = try typeContainer.decodeIfPresent(Int.self, forKey: .percent) + + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) } public func encode(to encoder: Encoder) throws { @@ -69,9 +73,10 @@ public class CircularProgressBarModel: MoleculeModelProtocol { try container.encode(size, forKey: .size) try container.encode(diameter, forKey: .diameter) try container.encode(lineWidth, forKey: .lineWidth) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(trackColor, forKey: .trackColor) try container.encodeIfPresent(color, forKey: .color) try container.encodeIfPresent(percent, forKey: .percent) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } func getCGColorsFromArray(_ colorArray: [String]) -> [Color] { From be5a69136569601652c2077c8e45377119ac36a1 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 11 Jul 2024 12:41:45 -0500 Subject: [PATCH 17/38] added more properties Signed-off-by: Matt Bruce --- .../Item Dropdown/ItemDropdownEntryField.swift | 3 +++ .../Item Dropdown/ItemDropdownEntryFieldModel.swift | 13 +++++++++++++ 2 files changed, 16 insertions(+) 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 ad38f476..456000d3 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 @@ -129,7 +129,10 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, isEnabled = viewModel.enabled isReadOnly = viewModel.readOnly isRequired = viewModel.required + showInlineLabel = viewModel.showInlineLabel tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + width = viewModel.width + transparentBackground = viewModel.transparentBackground if let index = viewModel.selectedIndex { selectId = index 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 131c2123..4ff79af8 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 @@ -16,7 +16,11 @@ import VDS 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 @@ -45,7 +49,10 @@ import VDS case options case selectedIndex case action + case showInlineLabel case tooltip + case transparentBackground + case width } //-------------------------------------------------- @@ -62,8 +69,11 @@ import VDS self.selectedIndex = selectedIndex baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil } + showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false 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 { @@ -71,7 +81,10 @@ import VDS 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.encodeModelIfPresent(action, forKey: .action) try container.encodeIfPresent(tooltip, forKey: .tooltip) + try container.encode(transparentBackground, forKey: .transparentBackground) + try container.encodeIfPresent(width, forKey: .width) } } From 4c283121915cc646e7c99abd912218fbdade0885 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 11 Jul 2024 12:47:31 -0500 Subject: [PATCH 18/38] updated helper text placement Signed-off-by: Matt Bruce --- .../Item Dropdown/ItemDropdownEntryField.swift | 5 ++++- .../Item Dropdown/ItemDropdownEntryFieldModel.swift | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 456000d3..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 @@ -93,6 +93,8 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, //-------------------------------------------------- open override func setup() { super.setup() + useRequiredRule = false + publisher(for: .valueChanged) .sink { [weak self] control in guard let self, let selectedItem else { return } @@ -124,12 +126,13 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, 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 - showInlineLabel = viewModel.showInlineLabel tooltipModel = viewModel.tooltip?.toVDSTooltipModel() width = viewModel.width transparentBackground = viewModel.transparentBackground 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 4ff79af8..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 @@ -50,6 +50,7 @@ import VDS case selectedIndex case action case showInlineLabel + case feedbackTextPlacement case tooltip case transparentBackground case width @@ -70,6 +71,7 @@ import VDS 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 @@ -82,6 +84,7 @@ import VDS 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) From de60fdfaf93085a185b343f05299c41fba5cdb83 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Thu, 11 Jul 2024 17:22:11 -0400 Subject: [PATCH 19/38] Make animation duration configurable. --- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift | 2 +- .../Atomic/Atoms/Views/CircularProgressBarModel.swift | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index 011359c8..651b7f65 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -84,7 +84,7 @@ import UIKit } // show circular progress view with animation. - showProgressWithAnimation(duration: 0.5, value: Float(graphModel?.percent ?? 0) / 100) + showProgressWithAnimation(duration: graphModel?.duration ?? 1.0, value: Float(graphModel?.percent ?? 0) / 100) // show progress percentage label. showProgressPercentage() diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 6c58be43..252f4893 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -21,6 +21,7 @@ public class CircularProgressBarModel: MoleculeModelProtocol { } public var diameter: CGFloat = 84 public var lineWidth: CGFloat = 5 + public var duration : Double = 1.0 public var color: Color? public var trackColor: Color? public var percent: Int? @@ -35,6 +36,7 @@ public class CircularProgressBarModel: MoleculeModelProtocol { case size case diameter case lineWidth + case duration case color case trackColor case percent @@ -59,6 +61,10 @@ public class CircularProgressBarModel: MoleculeModelProtocol { self.lineWidth = lineWidth } + if let duration = try typeContainer.decodeIfPresent(Double.self, forKey: .duration) { + self.duration = duration + } + color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) percent = try typeContainer.decodeIfPresent(Int.self, forKey: .percent) @@ -73,6 +79,7 @@ public class CircularProgressBarModel: MoleculeModelProtocol { try container.encode(size, forKey: .size) try container.encode(diameter, forKey: .diameter) try container.encode(lineWidth, forKey: .lineWidth) + try container.encode(duration, forKey: .duration) try container.encodeIfPresent(trackColor, forKey: .trackColor) try container.encodeIfPresent(color, forKey: .color) try container.encodeIfPresent(percent, forKey: .percent) From 53c4bd6c466529fd8191089924d499ebcaeece32 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Fri, 12 Jul 2024 16:12:54 -0400 Subject: [PATCH 20/38] Revise codes following MR comments --- .../Atoms/Views/CircularProgressBar.swift | 41 ++++------- .../Views/CircularProgressBarModel.swift | 68 +++++++++++-------- 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index 651b7f65..bc9083d0 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -23,17 +23,8 @@ import UIKit private var tracklayer = CAShapeLayer() private var labelLayer = CATextLayer() - var setProgressColor: UIColor = UIColor.red { - didSet { - progressLayer.strokeColor = setProgressColor.cgColor - } - } - - var setTrackColor: UIColor = UIColor.lightGray { - didSet { - tracklayer.strokeColor = setTrackColor.cgColor - } - } + 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? { @@ -61,37 +52,29 @@ import UIKit guard let model = model as? CircularProgressBarModel else { return } // set background color - if let backgroundColor = model.backgroundColor { - self.backgroundColor = backgroundColor.uiColor - } else { - self.backgroundColor = UIColor.clear - } + backgroundColor = model.backgroundColor?.uiColor ?? UIColor.clear configureProgressViewToBeCircular() // set progress color - if let color = model.color { - setProgressColor = color.uiColor - } else { - setProgressColor = UIColor.red - } - + progressColor = model.color?.uiColor ?? .red + progressLayer.strokeColor = progressColor.cgColor + // set track color - if let trackColor = model.trackColor { - setTrackColor = trackColor.uiColor - } else { - setProgressColor = UIColor.lightGray - } + trackColor = model.trackColor?.uiColor ?? .lightGray + tracklayer.strokeColor = trackColor.cgColor // show circular progress view with animation. showProgressWithAnimation(duration: graphModel?.duration ?? 1.0, value: Float(graphModel?.percent ?? 0) / 100) // show progress percentage label. - showProgressPercentage() + if let drawText = model.drawText, drawText { + showProgressPercentage() + } } private func configureProgressViewToBeCircular() { - let lineWidth = graphModel?.lineWidth ?? 5.0 + let lineWidth = graphModel?.lineWidth ?? 4.0 self.drawShape(using: tracklayer, lineWidth: lineWidth) self.drawShape(using: progressLayer, lineWidth: lineWidth) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 252f4893..7e2f4cbd 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -2,29 +2,31 @@ // 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: MoleculeModelProtocol { public static var identifier: String = "circularProgress" public var id: String = UUID().uuidString - public var size: GraphSize = .small { + public var percent: Int = 0 + public var size: GraphSize? = .small { didSet { updateSize() } } - public var diameter: CGFloat = 84 - public var lineWidth: CGFloat = 5 - public var duration : Double = 1.0 - public var color: Color? - public var trackColor: Color? - public var percent: Int? + public var diameter: CGFloat? = 84 + public var lineWidth: CGFloat? = 4 + public var duration : Double? = 1.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 init() { @@ -33,21 +35,24 @@ public class CircularProgressBarModel: MoleculeModelProtocol { private enum CodingKeys: String, CodingKey { case id + case moleculeName + case percent case size case diameter case lineWidth case duration case color case trackColor - case percent + case drawText case backgroundColor - case moleculeName } required public init(from decoder: Decoder) throws { 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 } @@ -65,46 +70,53 @@ public class CircularProgressBarModel: MoleculeModelProtocol { self.duration = duration } - color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) - trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) - percent = try typeContainer.decodeIfPresent(Int.self, forKey: .percent) + if let drawText = try typeContainer.decodeIfPresent(Bool.self, forKey: .drawText) { + self.drawText = drawText + } - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + 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(size, forKey: .size) - try container.encode(diameter, forKey: .diameter) - try container.encode(lineWidth, forKey: .lineWidth) - try container.encode(duration, forKey: .duration) + 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(percent, forKey: .percent) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - func getCGColorsFromArray(_ colorArray: [String]) -> [Color] { - return colorArray.map { (colorString) -> Color in - return Color(uiColor: UIColor.mfGet(forHex: colorString)) - } - } - func updateSize() { switch size { case .small: diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64 - lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 break case .medium: diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84 - lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 break case .large: diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124 - lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5 + lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 + break + case .none: break } } From 5cbd472a2da34508e6ccd0a25ab83c42d0d809a4 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Mon, 15 Jul 2024 16:04:34 -0400 Subject: [PATCH 21/38] Make diameter default value to 64 consistent with the default small size diameter value. --- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift | 2 +- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index bc9083d0..f55e85c8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -16,7 +16,7 @@ import UIKit } var viewWidth: CGFloat { - graphModel?.diameter ?? CGFloat(84) + graphModel?.diameter ?? CGFloat(64) } private var progressLayer = CAShapeLayer() diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 7e2f4cbd..e92dddbf 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -21,7 +21,7 @@ public class CircularProgressBarModel: MoleculeModelProtocol { updateSize() } } - public var diameter: CGFloat? = 84 + public var diameter: CGFloat? = 64 public var lineWidth: CGFloat? = 4 public var duration : Double? = 1.0 public var color: Color? = Color(uiColor: UIColor.mfGet(forHex: "#007AB8")) From 29466e4e336aab101c29be67114d6939fbb879f0 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Mon, 15 Jul 2024 17:02:28 -0400 Subject: [PATCH 22/38] Move GraphSize to a higher level for common use. --- MVMCoreUI.xcodeproj/project.pbxproj | 4 +++ .../Views/CircularProgressBarModel.swift | 16 ++++------ .../Atoms/Views/GraphSizeProtocol.swift | 29 +++++++++++++++++++ MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift | 17 ++++------- 4 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 0a1ea43d..12d16ced 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 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 */; }; @@ -774,6 +775,7 @@ 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 = ""; }; @@ -2313,6 +2315,7 @@ 94C2D9822386F3E30006CF46 /* Label */, 31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */, 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */, + 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */, D28A838223CCBD3F00DFE4FC /* WheelModel.swift */, 943784F3236B77BB006A1E82 /* Wheel.swift */, 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */, @@ -3109,6 +3112,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 */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index e92dddbf..61c503f6 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -10,17 +10,12 @@ import Foundation -public class CircularProgressBarModel: MoleculeModelProtocol { +public class CircularProgressBarModel: GraphSizeBase, MoleculeModelProtocol { public static var identifier: String = "circularProgress" public var id: String = UUID().uuidString public var percent: Int = 0 - public var size: GraphSize? = .small { - didSet { - updateSize() - } - } public var diameter: CGFloat? = 64 public var lineWidth: CGFloat? = 4 public var duration : Double? = 1.0 @@ -29,7 +24,8 @@ public class CircularProgressBarModel: MoleculeModelProtocol { public var drawText: Bool? = true public var backgroundColor: Color? = Color(uiColor: UIColor.clear) - public init() { + public override init() { + super.init() updateSize() } @@ -48,6 +44,8 @@ public class CircularProgressBarModel: 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 @@ -102,7 +100,7 @@ public class CircularProgressBarModel: MoleculeModelProtocol { try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) } - func updateSize() { + public override func updateSize() { switch size { case .small: diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64 @@ -116,8 +114,6 @@ public class CircularProgressBarModel: MoleculeModelProtocol { diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124 lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 break - case .none: - 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 From f001f98d3a09557172b0152b4d7372e22c79e3ed Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 16 Jul 2024 14:20:37 -0400 Subject: [PATCH 23/38] Digital PCT265 story DE307-834: Add missing BadgeModel isEqual(to:). --- MVMCoreUI/Atomic/Atoms/Views/BadgeModel.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 + } } From 46d57733e4618104af8a1cf07210987e46179a75 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 16 Jul 2024 14:32:48 -0400 Subject: [PATCH 24/38] Digital PCT265 story DE307-834: Increase throttle time for collision safety. As a throttle this is to space the updates at regular intervals. This should also help reduce some of the page stuttering as there will be bigger update batches. --- MVMCoreUI/BaseControllers/ViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index a6c43b2d..043627e6 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -78,7 +78,7 @@ import Combine 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.05), scheduler: RunLoop.main, latest: true) + .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 { From bc72d81396cb2b3192e21f9674925bb597fb287e Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 16 Jul 2024 15:16:56 -0400 Subject: [PATCH 25/38] Digital PCT265 story DE307-834: Prevent scroll reset on reconfigure. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index ad627d35..fb333772 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -210,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) @@ -250,8 +251,6 @@ open class Carousel: View { } else { loop = false } - - pageIndex = 0 } open override func reset() { From 273f45def04f66a6eafb2183ca27580a7b786d65 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Jul 2024 15:27:14 -0500 Subject: [PATCH 26/38] updated model Signed-off-by: Matt Bruce --- .../TextFields/TextEntryFieldModel.swift | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) 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) } } From 65be46c7678f1ff138eb07fe16b025f99e87f6c9 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Jul 2024 15:27:27 -0500 Subject: [PATCH 27/38] added inputentryfield Signed-off-by: Matt Bruce --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + .../TextFields/InputEntryField.swift | 350 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 25045a2f..a5c48b12 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -579,6 +579,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 */; }; @@ -1201,6 +1202,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 = ""; }; @@ -2351,6 +2353,7 @@ children = ( 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */, 0A21DB7E235DECC500C160A2 /* EntryField.swift */, + EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */, 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, @@ -3140,6 +3143,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/InputEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift new file mode 100644 index 00000000..198daad4 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift @@ -0,0 +1,350 @@ +// +// 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) + + } + + + @objc open func updateView(_ size: CGFloat) {} + + @objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) { + observingTextFieldDelegate = delegate + uiTextFieldDelegate = delegate + } + + //-------------------------------------------------- + // MARK: - Observing for Change (TextFieldDelegate) + //-------------------------------------------------- + + 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() + } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + 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() + } + } + + 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) + } + } +} + +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) + } + } +} + From 36669b61cbd25ff3fc476290eff080dcf2b099e7 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Jul 2024 15:27:42 -0500 Subject: [PATCH 28/38] used new input in mapping for now Signed-off-by: Matt Bruce --- MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 22f65ade..af99d1be 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) From 239af70710c65230e405d57e57894cc61c2d0acf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Jul 2024 15:27:54 -0500 Subject: [PATCH 29/38] fix initial updates Signed-off-by: Matt Bruce --- .../Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift | 5 ++--- .../Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) 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/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index 910712d5..6181cfdc 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -144,8 +144,8 @@ import MVMCore picker.displayedPropertyKeys = ["phoneNumbers"] picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0") picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'") - Task(priority: .userInitiated) { - await NavigationHandler.shared().present(viewController: picker, animated: true) + if let viewContoller = UIApplication.topViewController() { + viewContoller.present(picker, animated: true) } } From 018fe9a25e23f44f258e7537252625fb817a6633 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 16 Jul 2024 16:16:39 -0500 Subject: [PATCH 30/38] updated MDN Field to use new inputfield Signed-off-by: Matt Bruce --- .../FormFields/TextFields/MdnEntryField.swift | 138 ++++-------------- .../TextFields/MdnEntryFieldModel.swift | 5 + 2 files changed, 33 insertions(+), 110 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index 6181cfdc..f80f89d3 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 } + } } From 16c20507ce19f11df7326c5761df432843c5c164 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 16 Jul 2024 19:58:26 -0400 Subject: [PATCH 31/38] Digital PCT265 story DE307-834: Crash prevention on registered cells check for gone change. --- MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index fb333772..f84bdfc5 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -285,7 +285,7 @@ open class Carousel: View { /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { var registeredIds = [String]() - for molecule in carouselModel.molecules { + for molecule in carouselModel.visibleMolecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) registeredIds.append(info.identifier) @@ -299,7 +299,7 @@ open class Carousel: View { func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { guard let registeredMoleculeIds else { return false } - let incomingIds = carouselModel.molecules.map { molecule in + let incomingIds = carouselModel.visibleMolecules.map { molecule in if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { return info.identifier } else { From 3b5b03a3689f7185e1811b9525afee1ead5ec086 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Wed, 17 Jul 2024 10:23:50 -0400 Subject: [PATCH 32/38] Fix the showing issue when percent is 100%. --- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index f55e85c8..09009a57 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -102,7 +102,7 @@ import UIKit private func showProgressPercentage() { let percent = graphModel?.percent ?? 0 - let percentLen = percent > 9 ? 2 : 1 + let percentLen = String(percent).count // configure attributed string for progress percentage. let attributedString = NSMutableAttributedString(string: String(percent) + "%") From 0f4a97b49cbb2b1160a736b23d230193a1e25961 Mon Sep 17 00:00:00 2001 From: Xi Zhang Date: Wed, 17 Jul 2024 14:35:55 -0400 Subject: [PATCH 33/38] Set the default value of duration to 0 to disable the animation by default following confluence page. --- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift | 2 +- MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift index 09009a57..835a1def 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift @@ -65,7 +65,7 @@ import UIKit tracklayer.strokeColor = trackColor.cgColor // show circular progress view with animation. - showProgressWithAnimation(duration: graphModel?.duration ?? 1.0, value: Float(graphModel?.percent ?? 0) / 100) + showProgressWithAnimation(duration: graphModel?.duration ?? 0, value: Float(graphModel?.percent ?? 0) / 100) // show progress percentage label. if let drawText = model.drawText, drawText { diff --git a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift index 61c503f6..b2f37e68 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift @@ -18,7 +18,7 @@ public class CircularProgressBarModel: GraphSizeBase, MoleculeModelProtocol { public var percent: Int = 0 public var diameter: CGFloat? = 64 public var lineWidth: CGFloat? = 4 - public var duration : Double? = 1.0 + 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 From 2888eebb6996a350567ba3bdd6fe5b29a3b5b419 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 19 Jul 2024 14:16:55 -0500 Subject: [PATCH 34/38] updated validity Signed-off-by: Matt Bruce --- .../Atomic/Atoms/FormFields/TextFields/TextEntryField.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index 9d030d0f..368f634c 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -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) } } From 6db3a58782e129936d0099f2c634cb3a8578ff60 Mon Sep 17 00:00:00 2001 From: Nandhini Rajendran Date: Fri, 26 Jul 2024 01:55:36 +0530 Subject: [PATCH 35/38] CXTDT-590886 Fix to make placeholder text visible. --- MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 + } }) } From 796215f64dfb9ce5172e8bdff6cb76c11d792620 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 30 Jul 2024 08:07:34 -0500 Subject: [PATCH 36/38] rearranged methods and comments Signed-off-by: Matt Bruce --- .../TextFields/InputEntryField.swift | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift index 198daad4..d1e37047 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift @@ -131,42 +131,6 @@ import VDS } - - @objc open func updateView(_ size: CGFloat) {} - - @objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) { - observingTextFieldDelegate = delegate - uiTextFieldDelegate = delegate - } - - //-------------------------------------------------- - // MARK: - Observing for Change (TextFieldDelegate) - //-------------------------------------------------- - - 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() - } - - //-------------------------------------------------- - // MARK: - MoleculeViewProtocol - //-------------------------------------------------- open override func updateView() { super.updateView() @@ -196,9 +160,8 @@ import VDS textField.keyboardType = keyboardType } } - } - + open func viewModelDidUpdate() { fieldType = viewModel.type.toVDSFieldType() @@ -269,7 +232,36 @@ import VDS 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 @@ -282,6 +274,11 @@ import VDS //observingTextFieldDelegate?.isInvalid?(textfield: self) } } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + @objc open func updateView(_ size: CGFloat) {} } extension InputEntryField { From b754b476a67f7d72a1d2dc079ca106cffee59819 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 30 Jul 2024 08:29:07 -0500 Subject: [PATCH 37/38] reverted code Signed-off-by: Matt Bruce --- .../Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index f80f89d3..401dfa6b 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -76,8 +76,8 @@ import MVMCore picker.displayedPropertyKeys = ["phoneNumbers"] picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0") picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'") - if let viewContoller = UIApplication.topViewController() { - viewContoller.present(picker, animated: true) + Task(priority: .userInitiated) { + await NavigationHandler.shared().present(viewController: picker, animated: true) } } From 68083819beb33fe433ce74474de0377d3de474ac Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 30 Jul 2024 13:16:16 -0500 Subject: [PATCH 38/38] change protocol to any Signed-off-by: Matt Bruce --- .../Atomic/Atoms/FormFields/TextFields/InputEntryField.swift | 4 ++-- .../Atomic/Atoms/FormFields/TextFields/TextEntryField.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift index d1e37047..51cbd26c 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift @@ -268,10 +268,10 @@ import VDS if previousValidity && !isValid { showError = true - //observingTextFieldDelegate?.isValid?(textfield: self) + observingTextFieldDelegate?.isValid?(textfield: self) } else if (!previousValidity && isValid) { showError = false - //observingTextFieldDelegate?.isInvalid?(textfield: self) + observingTextFieldDelegate?.isInvalid?(textfield: self) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index 368f634c..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?) }