Merge branch 'develop' into feature/twobuttonview_vds_buttongroup

This commit is contained in:
Matt Bruce 2023-12-15 15:06:26 -06:00
commit e1b6d82007
76 changed files with 1389 additions and 729 deletions

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -168,6 +168,8 @@
526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */; };
52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */; };
52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; };
7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; };
71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */; };
8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */; };
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */; };
8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */; };
@ -296,6 +298,8 @@
AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */; };
AFE4A1D127DFB5EE00C458D0 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */; };
AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */; };
B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBE29DF34730005D28B /* BadgeModel.swift */; };
B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBC29DF34680005D28B /* Badge.swift */; };
BB105859248DEFF70069D008 /* UICollectionViewLeftAlignedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */; };
BB1D17E0244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */; };
BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */; };
@ -572,8 +576,8 @@
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; };
EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */; };
EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilelet.swift */; };
EA985C402970939A00F2FF2E /* TileletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3F2970939A00F2FF2E /* TileletModel.swift */; };
EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilelet.swift */; };
EA985C602970A3F000F2FF2E /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C5F2970A3F000F2FF2E /* VDS.framework */; };
EA985C642970A40E00F2FF2E /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA985C632970A40E00F2FF2E /* VDSTypographyTokens.xcframework */; };
EA985C852981AA9C00F2FF2E /* VDS-Enums+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C842981AA9C00F2FF2E /* VDS-Enums+Codable.swift */; };
@ -588,6 +592,7 @@
EAB14BC327D9378D0012AB2C /* RuleAnyModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */; };
EABFC1412763BB8D00E78B40 /* FormLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFC1402763BB8D00E78B40 /* FormLabel.swift */; };
EABFC152276913E800E78B40 /* FormLabelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFC151276913E800E78B40 /* FormLabelModel.swift */; };
EACCF38C2ABB346700E0F104 /* VDS-Interpreters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACCF38B2ABB346700E0F104 /* VDS-Interpreters.swift */; };
FD99130028E21E4900542CC3 /* RuleNotEqualsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */; };
/* End PBXBuildFile section */
@ -755,6 +760,8 @@
526A265D240D200500B0D828 /* ListTwoColumnCompareChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnCompareChanges.swift; sourceTree = "<group>"; };
52B201D024081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethod.swift; sourceTree = "<group>"; };
52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = "<group>"; };
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; };
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = "<group>"; };
8D070BAF241B56530099AC56 /* ListRightVariableTotalDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalDataModel.swift; sourceTree = "<group>"; };
8D070BB1241B56AD0099AC56 /* ListRightVariableTotalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableTotalData.swift; sourceTree = "<group>"; };
8D084ACF2410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextBodyTextModel.swift; sourceTree = "<group>"; };
@ -883,6 +890,8 @@
AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = "<group>"; };
AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = "<group>"; };
AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = "<group>"; };
B4CC8FBE29DF34730005D28B /* BadgeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeModel.swift; sourceTree = "<group>"; };
B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = "<group>"; };
BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewLeftAlignedLayout.swift; sourceTree = "<group>"; };
BB1D17DF244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMediumModel.swift; sourceTree = "<group>"; };
BB1D17E1244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDeviceComplexButtonMedium.swift; sourceTree = "<group>"; };
@ -1176,6 +1185,7 @@
EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyModelProtocol.swift; sourceTree = "<group>"; };
EABFC1402763BB8D00E78B40 /* FormLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormLabel.swift; sourceTree = "<group>"; };
EABFC151276913E800E78B40 /* FormLabelModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormLabelModel.swift; sourceTree = "<group>"; };
EACCF38B2ABB346700E0F104 /* VDS-Interpreters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VDS-Interpreters.swift"; sourceTree = "<group>"; };
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleNotEqualsModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -1453,6 +1463,15 @@
path = OneColumn;
sourceTree = "<group>";
};
7199C8142A4F3A40001568B7 /* Accessibility */ = {
isa = PBXGroup;
children = (
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */,
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */,
);
path = Accessibility;
sourceTree = "<group>";
};
8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = {
isa = PBXGroup;
children = (
@ -1977,6 +1996,7 @@
D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */ = {
isa = PBXGroup;
children = (
7199C8142A4F3A40001568B7 /* Accessibility */,
01F2C1FC27C81F9700DC3D36 /* Managers */,
D2ED27D8254B0C1F00A1C293 /* Alerts */,
27F973512466071600CAB5C5 /* Behaviors */,
@ -2218,8 +2238,10 @@
AA37CBD42519072F0027344C /* Stars.swift */,
AA07EA902510A442009A2AE3 /* StarModel.swift */,
AA07EA922510A451009A2AE3 /* Star.swift */,
EA985C3D2970938F00F2FF2E /* Tilelet.swift */,
B4CC8FBE29DF34730005D28B /* BadgeModel.swift */,
B4CC8FBC29DF34680005D28B /* Badge.swift */,
EA985C3F2970939A00F2FF2E /* TileletModel.swift */,
EA985C3D2970938F00F2FF2E /* Tilelet.swift */,
);
path = Views;
sourceTree = "<group>";
@ -2418,6 +2440,7 @@
011B58EE23A2AA850085F53C /* ModelProtocols */,
27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */,
EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */,
EACCF38B2ABB346700E0F104 /* VDS-Interpreters.swift */,
);
path = Protocols;
sourceTree = "<group>";
@ -2834,6 +2857,7 @@
0A7ECC702441001C00C828E8 /* UIToolbar+Extension.swift in Sources */,
D29DF26D21E6AA0B003B2FB9 /* FLAnimatedImageView.m in Sources */,
AA3561AC24C9684400452EB1 /* ListRightVariableRightCaretAllTextAndLinksModel.swift in Sources */,
7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */,
D209234F244F77FD0044AD09 /* ThreeLayerCenterTemplate.swift in Sources */,
525019E52406852100EED91C /* ListFourColumnDataUsageDividerModel.swift in Sources */,
32D2609624C19E2100B56344 /* LockupsPlanSMLXL.swift in Sources */,
@ -2920,6 +2944,7 @@
27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */,
D2A5146B2214905000345BFB /* ThreeLayerViewController.swift in Sources */,
526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */,
B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */,
8D24041523E7FC0B009E23BE /* ListLeftVariableIconWithRightCaretModel.swift in Sources */,
3265B30224BCA737000D154B /* HeadersH1NoButtonsBodyTextModel.swift in Sources */,
D28A838F23CCDEDE00DFE4FC /* TwoButtonViewModel.swift in Sources */,
@ -2976,6 +3001,7 @@
D28BA741248025A300B75CB8 /* TabBarModel.swift in Sources */,
D224798A2314445E003FCCF9 /* LabelToggle.swift in Sources */,
D2A92882241AAB67004E01C6 /* ScrollingViewController.swift in Sources */,
EACCF38C2ABB346700E0F104 /* VDS-Interpreters.swift in Sources */,
C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */,
0AE98BB523FF18D2004C5109 /* Arrow.swift in Sources */,
D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */,
@ -3004,6 +3030,7 @@
D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */,
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */,
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */,
AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */,
8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */,
D243859923A16B1800332775 /* Container.swift in Sources */,
@ -3027,6 +3054,7 @@
22B678F929E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift in Sources */,
BB2FB3BB247E7EBC00DF73CD /* TagCollectionViewCell.swift in Sources */,
012A88C6238DA34000FE3DA1 /* ModuleMoleculeModel.swift in Sources */,
B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */,
94C2D9A123872BCC0006CF46 /* LabelAttributeUnderlineModel.swift in Sources */,
EA985C852981AA9C00F2FF2E /* VDS-Enums+Codable.swift in Sources */,
AAB8549824DC01BD00477C40 /* ListThreeColumnBillHistoryDividerModel.swift in Sources */,

View File

@ -0,0 +1,275 @@
//
// AccessibilityHandler.swift
// MVMCoreUI
//
// Created by Bandaru, Krishna Kishore on 30/06/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import Combine
import MVMCore
import WebKit
//MARK: - AccessibilityOperation
public class AccessibilityOperation: MVMCoreOperation {
private let argument: Any?
private let notificationType: UIAccessibility.Notification
public init(notificationType: UIAccessibility.Notification, argument: Any?) {
self.notificationType = notificationType
self.argument = argument
}
/**
This method will post accessibility notification.
*/
public override func main() {
guard !checkAndHandleForCancellation() else { return }
guard UIAccessibility.isVoiceOverRunning else {
markAsFinished()
return
}
Task { @MainActor [weak self] in
guard let self = self, !self.isCancelled else {
self?.markAsFinished()
return
}
UIAccessibility.post(notification: self.notificationType, argument: self.argument)
self.markAsFinished()
}
}
}
//MARK: - AccessibilityHandler
/**
AccessibilityHandler will observe the page visibility of every view controller and post notification to the first interactive element on the screen.
If we have to shift/focus custom element on the screen on controller shown then we need to pass accessibilityId in the page response.
*/
open class AccessibilityHandler {
public static func shared() -> Self? {
guard let shared = CoreUIObject.sharedInstance()?.accessibilityHandler else { return nil }
return MVMCoreActionUtility.fatalClassCheck(object: shared)
}
//TODO: Revisit to avoid state properties to store in handler.
public var accessibilityId: String? ///This property is used to post accessibility to the UIElement mapped to this accessibilityId
public var previousAccessiblityElement: Any? ///This property is capture accessiblity element
public var anyCancellable: Set<AnyCancellable> = []
public weak var currentController: UIViewController? { MVMCoreUIUtility.getCurrentVisibleController() }
public var delegateObject: MVMCoreUIDelegateObject?
//TODO: Revisit to identify the page has top notification or not.
private var hasTopNotificationInPage: Bool { (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseJSON?.optionalDictionaryForKey("TopNotification") != nil || (currentController as? MVMCoreViewControllerProtocol)?.loadObject??.responseInfoMap?.optionalStringForKey("messageStyle") != nil }
private let accessibilityOperationQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
lazy var rotorHandler = RotorHandler(accessibilityHandler: self)
/**
init method will register for focus changes
*/
public init() {
registerForFocusChanges()
}
/**
This method will capture current focused element
*/
open func capturePreviousFocusElement() {
previousAccessiblityElement = UIAccessibility.focusedElement(using: .notificationVoiceOver)
}
/**
This method will post accessibility notification to previous captured element
*/
open func postAccessibilityToPrevElement() {
post(notification: .layoutChanged, argument: previousAccessiblityElement)
previousAccessiblityElement = nil
}
/**
This method will check if voice over is running then will post notification to the mentioned argument
*/
public func post(notification type: UIAccessibility.Notification, argument: Any? = nil) {
guard UIAccessibility.isVoiceOverRunning else { return }
let AccessibilityOperation = AccessibilityOperation(notificationType: type, argument: argument)
accessibilityOperationQueue.addOperation(AccessibilityOperation)
}
/**
This method return first focused element from the screen.
If navigationBar is hidden then we are returning nil so that voice over will shift to the first interactive element.
*/
open func getFirstFocusedElementOnScreen() -> Any? {
((currentController as? PageProtocol)?.pageModel?.navigationBar?.hidden ?? false) ? nil : currentController?.navigationController?.navigationBar
}
/**
This method is used to decide if AccessibilityHandler can post screen change notification or specific classes will take care of posting Accessibility notification
*/
open func canPostAccessibilityNotification(for viewController: UIViewController) -> Bool { true }
/**
This method is used to identify the UIElement that is mapped to accessibilityId from server response.
*/
func getPreDefinedFocusedElementIfAny() -> UIView? {
guard let accessibilityId, let view = currentController?.view else { return nil }
return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [view]).first {
$0.model?.id == accessibilityId
}
}
}
extension AccessibilityHandler {
/**
This method is used to notify rotor handler about page loaded and capturing the accessibilityId if any.
*/
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
previousAccessiblityElement = nil
rotorHandler.onPageNew(rootMolecules: rootMolecules, delegateObject)
guard let loadObject = (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.loadObject else { return }
accessibilityId = loadObject?.pageJSON?.optionalStringForKey("accessibilityId")
self.delegateObject = delegateObject
}
/**
This method is used to capture accessibility views on the screen.
If the page has accessibilityId, then it will not post any accessibility notification because respective UI mapped element can be identified only on page shown.
If it has top notification then we are capturing the first focused element and will not post any accessibility notification.
If page doesn't have any top notification or accessibilityId then it will post notification to shift focus to first focused element on the screen.
*/
public func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
updateAccessibilityViews(delegateObject)
guard let controller = delegateObject?.moleculeDelegate as? UIViewController,
canPostAccessibilityNotification(for: controller),
accessibilityId == nil else { return }
if hasTopNotificationInPage {
previousAccessiblityElement = getFirstFocusedElementOnScreen()
} else {
post(notification: .layoutChanged, argument: getFirstFocusedElementOnScreen())
}
}
/**
This method is used to notify rotor handler about page visibility
Temp fix: - We are resetting the view accessibilityElements when focus shifted to first focused element on the screen not to have voice over struck in between view elements.
https://developer.apple.com/forums/thread/655359
https://developer.apple.com/forums/thread/675427
If the page has accessibilityId, i.e if server decides to manually shift focus to one of the UIElement then we are identifying the id mapped UIElement & shifting focus to that element.
If we have top notification as well in the page then we take that as priority and holding the server driven UIElement in previousAccessiblityElement and post accessibility notification.
https://oneconfluence.verizon.com/display/MFD/Accessibility+-+Focus+Retain
*/
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
defer {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
(delegateObject?.moleculeDelegate as? UIViewController)?.view.accessibilityElements = nil
}
}
rotorHandler.onPageShown(delegateObject)
guard let accessibilityElement = getPreDefinedFocusedElementIfAny() else { return }
accessibilityId = nil
if hasTopNotificationInPage {
previousAccessiblityElement = accessibilityElement
} else {
post(notification: .layoutChanged, argument: accessibilityElement)
}
}
/**
This method is used to set view elements as accessibilityElements due to the accessibility behaviour change when new controller is pushed on navigation stack from iOS13+
*/
private func updateAccessibilityViews(_ delegateObject: MVMCoreUIDelegateObject?) {
guard var currentController = delegateObject?.moleculeDelegate as? UIViewController,
currentController.navigationController != nil else { return }///If no navigationController, we can go with the default behaviour of Accessibility voiceover.
var accessibilityElements: [Any?] = [MVMCoreUISplitViewController.main()?.topAlertView, MVMCoreUISplitViewController.main()?.navigationController]
if let manager = ((delegateObject?.moleculeDelegate as? MVMCoreViewManagerViewControllerProtocol)?.manager as? MVMCoreViewManagerProtocol & UIViewController),
let managerAccessibilityElements = manager.getAccessibilityElements() {
accessibilityElements.append(contentsOf: managerAccessibilityElements)
accessibilityElements.append(contentsOf: manager.view.subviews)
currentController = manager
} else {
accessibilityElements.append(contentsOf: currentController.view.subviews)
}
accessibilityElements.append(MVMCoreUISplitViewController.main()?.tabBar)
currentController.view.accessibilityElements = accessibilityElements.compactMap { $0 }
}
}
// MARK: - AccessibilityHandler listeners
@objc extension AccessibilityHandler {
/**
This method listens for focus changes.
When focus is changed manually then we are cancelling existing operations.
*/
private func registerForFocusChanges() {
//Since focus shifted to other elements cancelling existing focus shift notifications if any
NotificationCenter.default.publisher(for: UIAccessibility.elementFocusedNotification)
.sink { [weak self] _ in
self?.accessibilityOperationQueue.cancelAllOperations()
}.store(in: &anyCancellable)
}
/**
This method listens for top notification changes.
When top notification is about to display it will capture previous focused element.
When top notification is displayed it will post notification to that notification view
When top notification is about to dismiss then it will post announcement that top alert is closed.
When top notification is dimissed then it will post notification back to previously captured element.
*/
func registerForTopNotificationsChanges() {
NotificationHandler.shared()?.onNotificationWillShow
.sink { [weak self] (_, model) in
if self?.previousAccessiblityElement == nil {
self?.capturePreviousFocusElement()
}
}.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationShown
.sink { [weak self] (view, model) in
self?.post(notification: .layoutChanged, argument: view)
}.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationWillDismiss
.sink { [weak self] (view, model) in
self?.post(notification: .announcement, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed"))
}.store(in: &anyCancellable)
NotificationHandler.shared()?.onNotificationDismissed
.sink { [weak self] (view, model) in
self?.postAccessibilityToPrevElement()
}.store(in: &anyCancellable)
}
}
// MARK: - AccessibilityHandlerBehaviour
/**
To notify AccessibilityHandler about the page visibility changes
*/
//TODO: Revisit why we need a behavior as a notifier.
open class AccessibilityHandlerBehavior: PageVisibilityBehavior, PageMoleculeTransformationBehavior {
public let accessibilityHandler: AccessibilityHandler?
public init(accessibilityHandler: AccessibilityHandler?) {
self.accessibilityHandler = accessibilityHandler
}
required public init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
accessibilityHandler = AccessibilityHandler.shared() //Protocol Mandatory init method.
}
open func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
accessibilityHandler?.onPageNew(rootMolecules: rootMolecules, delegateObject)
}
open func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {
accessibilityHandler?.willShowPage(delegateObject)
}
open func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
accessibilityHandler?.onPageShown(delegateObject)
}
}

View File

@ -0,0 +1,357 @@
//
// RotorHandler.swift
// MVMCoreUI
//
// Created by Bandaru, Krishna Kishore on 13/10/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import Combine
//MARK: - RotorType that our app supports
fileprivate enum RotorType: String, CaseIterable {
case button = "Buttons"
case header = "Header"
case link = "Link"
var trait: UIAccessibilityTraits {
switch self {
case .button:
return .button
case .header:
return .header
case .link:
return .link
}
}
///Filter block on model elements based on rotor type
var modelFilter: ((MoleculeModelProtocol) -> Bool) {
switch self {
default:
return { $0.accessibilityTraits?.contains(trait) ?? false }
}
}
///Filter block on model UIElements based on rotor type
var filter: ((UIView) -> Bool) {
switch self {
default:
return { $0.accessibilityTraits.contains(trait) && !$0.isHidden }
}
}
///Returns custom rotor of the specific type
func getUIAccessibilityCustomRotor(itemSearch: @escaping UIAccessibilityCustomRotor.Search) -> UIAccessibilityCustomRotor? {
var accessibilityCustomRotor: UIAccessibilityCustomRotor?
switch self {
case .header:
accessibilityCustomRotor = UIAccessibilityCustomRotor(systemType: .heading, itemSearch: itemSearch)
default:
accessibilityCustomRotor = UIAccessibilityCustomRotor(name: rawValue, itemSearch: itemSearch)
}
return accessibilityCustomRotor
}
}
//MARK: - RotorViewElementsProtocol
public protocol RotorViewElementsProtocol: MVMCoreViewControllerProtocol {
var topView: UIView? { get set }
var middleView: UIView? { get set }
var bottomView: UIView? { get set }
}
//MARK: - RotorProtocols
public protocol RotorScrollDelegateProtocol: MVMCoreViewControllerProtocol {
func scrollRectToVisible(_ rect: CGRect, animated: Bool)
}
public protocol RotorListTypeDelegateProtocol: RotorScrollDelegateProtocol {
func scrollToRow(at indexPath: IndexPath, animated: Bool) -> Void
func cellForRow(at indexPath: IndexPath) -> UIView?
func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath?
}
//MARK: - Rotor Element - RotorElement info when we are traversing on model elements in the page.
struct RotorElement {
let indexPath: IndexPath?
let model: MoleculeModelProtocol
let carouselItemModel: MoleculeModelProtocol?///This element is the parent of model item if we have rotor element inside carousel. This is used to scroll to specific carousel item when rotor mode is enabled.
init(indexPath: IndexPath? = nil, model: MoleculeModelProtocol, carouselItemModel: MoleculeModelProtocol? = nil) {
self.indexPath = indexPath
self.model = model
self.carouselItemModel = carouselItemModel
}
}
//MARK: - Rotor Handler
class RotorHandler {
private var rotorIndexes: [RotorType: Int] = [:]
private var rotorElements: [RotorType: [Any]] = [:]
private var anyCancellable: Set<AnyCancellable> = []
private weak var delegateObject: MVMCoreUIDelegateObject?
private weak var delegate: MVMCoreViewControllerProtocol? { delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol }
private weak var accessibilityHandler: AccessibilityHandler?
init(accessibilityHandler: AccessibilityHandler?) {
self.accessibilityHandler = accessibilityHandler
registerForVoiceOverChanges()
}
///Preparing rotors when accessibility voiceover is turned after page is loaded.
private func registerForVoiceOverChanges() {
NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
.sink { [weak self] _ in
guard UIAccessibility.isVoiceOverRunning, (self?.rotorElements.isEmpty ?? true) else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self?.identifyAndPrepareRotors(self?.delegateObject)
}
}.store(in: &anyCancellable)
}
//MARK: - Pagevisibility behaviour methods.
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
rotorIndexes = [:]
rotorElements = [:]
self.delegateObject = delegateObject
}
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
identifyAndPrepareRotors(delegateObject)
}
//MARK: - Rotor methods
/**
This method prepares custom rotors that will be assigned to the current controller.
Rotor will be created only if the page contains that trait mapped element or the conditions met.
*/
private func identifyAndPrepareRotors(_ delegateObject: MVMCoreUIDelegateObject?) {
guard UIAccessibility.isVoiceOverRunning,
let currentViewController = (delegateObject?.moleculeDelegate as? (MVMCoreViewControllerProtocol & UIViewController)) else { return }
var customRotors: [UIAccessibilityCustomRotor] = []
for type in RotorType.allCases {
if let elements = getTraitMappedElements(currentViewController, type: type),
!elements.isEmpty,
let rotor = createRotor(for: type) {
rotorElements[type] = elements
customRotors.append(rotor)
}
}
currentViewController.view.accessibilityCustomRotors = customRotors
}
/**
This method prepares trait mapped elements of the current controller and from its manager if exists.
*/
private func getTraitMappedElements(_ template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
var accessibilityElements: [Any?] = []
if let manager = (template as? MVMCoreViewManagerViewControllerProtocol)?.manager as? (MVMCoreViewControllerProtocol & UIViewController) {
accessibilityElements.append(contentsOf: getRotorElementsFrom(manager: manager, type: type) ?? [])
}
if let rotorElements = getRotorElementsFrom(template: template, type: type) {
accessibilityElements.append(contentsOf: rotorElements)
}
if let tabBarHidden = (template as? TabPageModelProtocol)?.tabBarHidden, !tabBarHidden {
accessibilityElements.append(contentsOf: (MVMCoreUISplitViewController.main()?.tabBar?.subviews ?? []).filter { type.filter($0) })
}
return accessibilityElements.compactMap { $0 }
}
/**
This method prepares trait mapped elements from manager
*/
private func getRotorElementsFrom(manager: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
guard let elements = (manager as? MVMCoreViewManagerProtocol)?.getAccessibilityElements() as? [UIView] else { return nil }
return MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: elements).filter { type.filter($0) } as [Any]
}
/**
This method prepares triat mapped elements from the current viewcontroller.
For BAU pages: This will traverse through the view hierarchy for trait mapped elements.
For Molecular pages: This will traverse through the models to identify the trait mapped model. Along with traversed models, again we are traversing on view hierarchy if any subViews are added to view to get trait mapped elements.
*/
private func getRotorElementsFrom(template: (MVMCoreViewControllerProtocol & UIViewController)?, type: RotorType) -> [Any]? {
guard let template = template as? (RotorViewElementsProtocol & MVMCoreViewControllerProtocol & ViewController) else {
return MVMCoreUIUtility.findViews(by: UIView.self, views: [template?.view].compactMap { $0 }).filter { type.filter($0) } as [Any] //BAU pages
}
let rotorElements = getRotorElements(from: template, type: type)
let remainingRotorElements = MVMCoreUIUtility.findViews(by: UIView.self, views: [template.view], excludedViews: [template.topView, template.middleView, template.bottomView].compactMap { $0 }).filter { type.filter($0) } as [Any] //Other elements added to view if any.
return rotorElements + remainingRotorElements
}
/**
This method is to get rotor elements form Molecular pages only.
We are filtering out header, molecules, footer from rootMolcules because rootMolcules may or maynot have the correct order of molecule models(header, body, footer). If we don't maintain order, Voiceover might first go footer then body elements then header.(Safety step)
*/
private func getRotorElements(from template: (MVMCoreViewControllerProtocol & ViewController & RotorViewElementsProtocol)?, type: RotorType) -> [Any] {
guard let templateModel = template?.model as? ThreeLayerModelBase else { return [] }
var rotorElements: [Any] = []
if let headerView = template?.topView {
rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [headerView].compactMap { $0 }).filter { type.filter($0) })
}
let molecules = templateModel.rootMolecules.filter { !($0.id == templateModel.header?.id || $0.id == templateModel.footer?.id) }
rotorElements.append(contentsOf: getRotorElements(from: molecules, template: template, type: type))
if let bottomView = template?.bottomView {
rotorElements.append(contentsOf: MVMCoreUIUtility.findViews(by: UIView.self, views: [bottomView].compactMap { $0 }).filter { type.filter($0) })
}
return rotorElements
}
/**
This method actually travers through the molecules and identify triat mapped model element along with indexPath of the element if its List/Collection templates.
Along with model, indexPath we are also capturing carouselItemModel because when we have Carousel inside the molecule, we need to scroll to carousel item if we have trait mapped rotor element inside the Carousel - (Multi Scroll behaviour)
Identiying the CarouselModel, CarouselItemModel by depth.
*/
private func getRotorElements(from molecules: [MoleculeModelProtocol], template: (MVMCoreViewControllerProtocol & ViewController)?, type: RotorType) -> [Any] {
var traitIndexPath: IndexPath?
var carouselItemModel: MoleculeModelProtocol?
var carouselDepth: Int?
let rotorElements: [RotorElement] = molecules.reduceDepthFirstTraverse(options: .parentFirst, depth: 0, initialResult: []) { result, model, depth in
if let listModel = model as? (ListItemModelProtocol & MoleculeModelProtocol),
let indexPath = (template as? RotorListTypeDelegateProtocol)?.getIndexPathFor(molecule: listModel) {
traitIndexPath = indexPath
}
if let _carouselDepth = carouselDepth, depth < _carouselDepth {
carouselDepth = nil
carouselItemModel = nil
}
if model is CarouselModel { carouselDepth = depth }
carouselItemModel = model is CarouselItemModelProtocol ? model : carouselItemModel
var result = result
if type.modelFilter(model) {
result.append(.init(indexPath: traitIndexPath, model: model, carouselItemModel: carouselItemModel))
}
return result
}
return rotorElements as [Any]
}
/**
This method creates a rotor based on the RotorType.
UIAccessibilityCustomRotor.Search Predicate block is used to return the current rotor UI element
If the rotor element is of type UIElement(subclasses of UIView) then it will post the accessibility notification directly to that UI element and scrolling that element to the visible area.
If rotor element is of type RotorElement then
1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
2. After the cell view is captured, traversing the cell hierarchy which matches the trait & id of that view's model.
3. After identifying the element, then will post the accessibility notification directly to that UI element
If we have carouselItemModel in RotorElement then
1. If we have indexPath in rotorElement then the template will scroll (list/collection) to that indexPath, and try to capture the cell view.
2. After cell is identified then we are identifying Carousel from the view hierarchy & scroll to the Carousel item which matches the carouselItemModel.id
3. After carouselItemModel is scrolled then traversing the carouselCellItem hierarchy which matches the trait & id of that view's model
4. After identifying the element, then will post the accessibility notification directly to that UI element
*/
private func createRotor(for type: RotorType) -> UIAccessibilityCustomRotor? {
return type.getUIAccessibilityCustomRotor { [weak self] predicate in
guard let self, let elements = self.rotorElements[type] else { return UIAccessibilityCustomRotorItemResult() }
var rotorIndex = self.rotorIndexes[type] ?? -1
switch predicate.searchDirection {
case .next:
if rotorIndex + 1 < elements.count {
rotorIndex += 1
}
case .previous:
if rotorIndex > 0 {
rotorIndex -= 1
}
@unknown default:
rotorIndex = 0
}
guard rotorIndex >= 0, !elements.isEmpty else { return UIAccessibilityCustomRotorItemResult() } //Safety check to avoid crash.
var rotorElement = elements[rotorIndex]
if let element = rotorElement as? RotorElement,
let controller = self.delegate as? (RotorViewElementsProtocol & ViewController) {
var cellView: UIView?
if let indexPath = element.indexPath, let controller = self.delegate as? (any RotorListTypeDelegateProtocol) {
controller.scrollToRow(at: indexPath, animated: false)
cellView = controller.cellForRow(at: indexPath)
} else {
cellView = controller.view
}
if let cauroselItemModel = element.carouselItemModel {
guard let carousel = MVMCoreUIUtility.findViews(by: Carousel.self, views: [cellView].compactMap { $0 }).first,
let index = (carousel.molecules?.firstIndex { $0.id == cauroselItemModel.id }) else { return UIAccessibilityCustomRotorItemResult() }
let collectionIndexPath = IndexPath(item: index, section: 0)
carousel.collectionView.scrollToItem(at: collectionIndexPath, at: .left, animated: false)
let collectionViewCell = carousel.collectionView.cellForItem(at: collectionIndexPath)
rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [collectionViewCell].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any
} else {
rotorElement = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [cellView].compactMap { $0 }).filter { type.filter($0) && $0.model?.id == element.model.id }.first as Any
if let viewElement = (rotorElement as? UIView) {
let convertedFrame = viewElement.convert(viewElement.frame, to: controller.view)
(self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false)
}
}
} else {
if let viewElement = (rotorElement as? UIView) {
let convertedFrame = viewElement.convert(viewElement.frame, to: (self.delegate as? UIViewController)?.view)
(self.delegate as? RotorScrollDelegateProtocol)?.scrollRectToVisible(convertedFrame, animated: false)
}
}
self.rotorIndexes[type] = rotorIndex
self.accessibilityHandler?.post(notification: .layoutChanged, argument: rotorElement)
return UIAccessibilityCustomRotorItemResult(targetElement: rotorElement as! NSObjectProtocol, targetRange: nil)
}
}
}
//MARK: - Protocol Extensions
public extension RotorViewElementsProtocol {
var topView: UIView? { nil }
var middleView: UIView? { nil }
var bottomView: UIView? { nil }
}
extension RotorScrollDelegateProtocol {
public func scrollRectToVisible(_ rect: CGRect, animated: Bool) { }
}
extension RotorListTypeDelegateProtocol {
public func scrollToRow(at indexPath: IndexPath, animated: Bool) { }
public func cellForRow(at indexPath: IndexPath) -> UIView? { nil }
public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? { nil }
}
extension ThreeLayerTableViewController: RotorListTypeDelegateProtocol {
public func scrollToRow(at indexPath: IndexPath, animated: Bool) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
}
public func cellForRow(at indexPath: IndexPath) -> UIView? {
tableView.cellForRow(at: indexPath)
}
public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? {
guard let molecule = molecule as? (ListItemModelProtocol & MoleculeModelProtocol) else { return nil }
return (self as? MoleculeListTemplate)?.getIndexPath(for: molecule)
}
}
extension ThreeLayerViewController: RotorScrollDelegateProtocol {
public func scrollRectToVisible(_ rect: CGRect, animated: Bool) {
scrollView?.scrollRectToVisible(rect, animated: animated)
}
}
extension ThreeLayerCollectionViewController: RotorListTypeDelegateProtocol {
public func scrollToRow(at indexPath: IndexPath, animated: Bool) {
collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated)
}
public func cellForRow(at indexPath: IndexPath) -> UIView? {
collectionView?.cellForItem(at: indexPath)
}
public func getIndexPathFor(molecule: MoleculeModelProtocol) -> IndexPath? {
guard let molecule = molecule as? (CollectionItemModelProtocol & MoleculeModelProtocol) else { return nil }
return (self as? MoleculeCollectionListProtocol)?.getIndexPath(for: molecule)
}
}

View File

@ -73,7 +73,7 @@ public class AlertOperation: MVMCoreOperation {
if await !self.properties.getIsDisplayed() {
self.markAsFinished()
} else {
(CoreUIObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject)
(MVMCoreObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject)
if self.isCancelled {
await self.dismissAlertView()
}

View File

@ -9,6 +9,7 @@
import UIKit
import VDSColorTokens
import VDS
import MVMCore
public typealias FacadeElements = (fill: UIColor?, text: UIColor?, border: UIColor?)
@ -26,54 +27,12 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
public var action: ActionModelProtocol
public var enabled: Bool = true
public var width: CGFloat?
public var style: Use? {
didSet {
guard let style = style else { return }
setFacade(by: style)
}
}
public var style: Use = .primary
public var size: VDS.Button.Size = .large
public var groupName: String = ""
public var inverted: Bool = false
public lazy var enabledColors: FacadeElements = (fill: enabled_fillColor(),
text: enabled_textColor(),
border: enabled_borderColor())
public lazy var disabledColors: FacadeElements = (fill: disabled_fillColor(),
text: disabled_textColor(),
border: disabled_borderColor())
public var enabledFillColor: Color?
public var enabledTextColor: Color?
public var enabledBorderColor: Color?
public var enabledFillColor_inverted: Color?
public var enabledTextColor_inverted: Color?
public var enabledBorderColor_inverted: Color?
public var disabledFillColor: Color?
public var disabledTextColor: Color?
public var disabledBorderColor: Color?
public var disabledFillColor_inverted: Color?
public var disabledTextColor_inverted: Color?
public var disabledBorderColor_inverted: Color?
private var _backgroundColor: Color?
public var backgroundColor: Color? {
get {
if let backgroundColor = _backgroundColor { return backgroundColor }
if inverted {
return enabled ? enabledFillColor_inverted : disabledFillColor_inverted
}
return enabled ? enabledFillColor : disabledFillColor
}
set {
_backgroundColor = newValue
}
}
public var backgroundColor: Color?
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
@ -87,88 +46,18 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
public init(with title: String, action: ActionModelProtocol) {
self.title = title
self.action = action
setFacade(by: .primary)
}
public init(secondaryButtonWith title: String, action: ActionModelProtocol) {
self.title = title
self.action = action
style = .secondary
setFacade(by: .secondary)
}
public init(primaryButtonWith title: String, action: ActionModelProtocol) {
self.title = title
self.action = action
style = .primary
setFacade(by: .primary)
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
public func enabled_fillColor() -> UIColor? {
(inverted ? enabledFillColor_inverted : enabledFillColor)?.uiColor
}
public func enabled_textColor() -> UIColor? {
(inverted ? enabledTextColor_inverted : enabledTextColor)?.uiColor
}
public func enabled_borderColor() -> UIColor? {
(inverted ? enabledBorderColor_inverted : enabledBorderColor)?.uiColor
}
public func disabled_fillColor() -> UIColor? {
(inverted ? disabledFillColor_inverted : disabledFillColor)?.uiColor
}
public func disabled_textColor() -> UIColor? {
(inverted ? disabledTextColor_inverted : disabledTextColor)?.uiColor
}
public func disabled_borderColor() -> UIColor? {
(inverted ? disabledBorderColor_inverted : disabledBorderColor)?.uiColor
}
/// Defines the default appearance for the primary style.
func setPrimaryFacade() {
enabledFillColor = Color(uiColor: VDSColor.elementsPrimaryOnlight)
enabledTextColor = Color(uiColor: VDSColor.elementsPrimaryOndark)
disabledFillColor = Color(uiColor: VDSColor.interactiveDisabledOnlight)
disabledTextColor = Color(uiColor: VDSColor.elementsPrimaryOndark)
enabledFillColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOndark)
enabledTextColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOnlight)
disabledFillColor_inverted = Color(uiColor: VDSColor.interactiveDisabledOndark)
disabledTextColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOnlight)
}
/// Defines the default appearance for the Secondary style.
func setSecondaryFacade() {
enabledTextColor = Color(uiColor: VDSColor.elementsPrimaryOnlight)
enabledFillColor = Color(uiColor: UIColor.clear)
enabledBorderColor = Color(uiColor: VDSColor.elementsPrimaryOnlight)
disabledTextColor = Color(uiColor: VDSColor.interactiveDisabledOnlight)
disabledBorderColor = Color(uiColor: VDSColor.interactiveDisabledOnlight)
enabledTextColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOndark)
enabledFillColor_inverted = Color(uiColor: UIColor.clear)
enabledBorderColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOndark)
disabledTextColor_inverted = Color(uiColor: VDSColor.interactiveDisabledOndark)
disabledBorderColor_inverted = Color(uiColor: VDSColor.interactiveDisabledOndark)
}
public func setFacade(by style: VDS.Use) {
switch style {
case .primary:
setPrimaryFacade()
case .secondary:
setSecondaryFacade()
@unknown default:
setPrimaryFacade()
}
}
//--------------------------------------------------
@ -178,7 +67,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
private enum CodingKeys: String, CodingKey {
case id
case moleculeName
case backgroundColor
case accessibilityIdentifier
case accessibilityText
case title
@ -188,12 +76,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
case style
case size
case groupName
case fillColor
case textColor
case borderColor
case disabledFillColor
case disabledTextColor
case disabledBorderColor
case width
}
@ -213,12 +95,8 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
///Style captured from the JSON
if let style = try typeContainer.decodeIfPresent(Use.self, forKey: .style) {
self.style = style
setFacade(by: style)
} else if let style = decoder.context?.value(forKey: CodingKeys.style.stringValue) as? Use { ///Reading the style param from context which is set is molecules, ex: TwoButtonView
self.style = style
setFacade(by: style)
} else { ///Default style
setFacade(by: .primary)
}
if let size = try typeContainer.decodeIfPresent(VDS.Button.Size.self, forKey: .size) {
@ -229,40 +107,57 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
self.enabled = enabled
}
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
}
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
self.groupName = groupName
}
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
} else {
try setInverted(deprecatedFrom: decoder)
}
}
private enum DeprecatedCodingKeys: String, CodingKey {
case fillColor
case textColor
case borderColor
case backgroundColor
}
private func setInverted(deprecatedFrom decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: DeprecatedCodingKeys.self)
if let enabledFillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .fillColor) {
self.enabledFillColor = enabledFillColor
if (self.style == .secondary) {
self.inverted = enabledFillColor.uiColor.isDark()
} else {
self.inverted = !enabledFillColor.uiColor.isDark()
}
}
if let enabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .textColor) {
self.enabledTextColor = enabledTextColor
if (self.style == .secondary) {
self.inverted = !enabledTextColor.uiColor.isDark()
} else {
self.inverted = enabledTextColor.uiColor.isDark()
}
}
if let enabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .borderColor) {
self.enabledBorderColor = enabledBorderColor
if (self.style == .secondary) {
self.inverted = !enabledBorderColor.uiColor.isDark()
}
}
if let disabledFillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledFillColor) {
self.disabledFillColor = disabledFillColor
if let backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
if (self.style == .secondary) {
self.inverted = backgroundColor.uiColor.isDark()
} else {
self.inverted = !backgroundColor.uiColor.isDark()
}
}
if let disabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledTextColor) {
self.disabledTextColor = disabledTextColor
}
if let disabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBorderColor) {
self.disabledBorderColor = disabledBorderColor
}
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
}
open func encode(to encoder: Encoder) throws {
@ -273,17 +168,10 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat
try container.encode(enabled, forKey: .enabled)
try container.encode(inverted, forKey: .inverted)
try container.encodeModel(action, forKey: .action)
try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
try container.encodeIfPresent(enabledFillColor, forKey: .fillColor)
try container.encodeIfPresent(enabledTextColor, forKey: .textColor)
try container.encodeIfPresent(enabledBorderColor, forKey: .borderColor)
try container.encodeIfPresent(disabledFillColor, forKey: .disabledFillColor)
try container.encodeIfPresent(disabledTextColor, forKey: .disabledTextColor)
try container.encodeIfPresent(disabledBorderColor, forKey: .disabledBorderColor)
try container.encodeIfPresent(style, forKey: .style)
try container.encodeIfPresent(size, forKey: .size)
try container.encode(style, forKey: .style)
try container.encode(size, forKey: .size)
try container.encodeIfPresent(groupName, forKey: .groupName)
try container.encodeIfPresent(width, forKey: .width)
}

View File

@ -11,30 +11,26 @@ import UIKit
open class ExternalLink: Link {
//--------------------------------------------------
// MARK: - Properties
// MARK: - Public Properties
//--------------------------------------------------
public var exportImageView: UIImageView?
open var exportImageView: UIImageView?
open var exportImageHeight: NSLayoutConstraint?
open var exportImageWidth: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
// MARK: - Overrides
//--------------------------------------------------
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? ExternalLinkModel else { return }
exportImageView?.tintColor = titleColor(for: model.enabled ? .normal : .disabled)
open override func viewModelDidUpdate() {
super.viewModelDidUpdate()
exportImageView?.tintColor = textColor
exportImageWidth?.constant = textStyle.lineHeight
exportImageHeight?.constant = textStyle.lineHeight
}
//--------------------------------------------------
// MARK: - MVMCoreViewProtocol
//--------------------------------------------------
open override func setupView() {
super.setupView()
open override func setup() {
super.setup()
contentHorizontalAlignment = .left
let image = MVMCoreUIUtility.imageNamed("externalLink")
exportImageView = UIImageView(image: image?.withRenderingMode(.alwaysTemplate))
@ -46,10 +42,10 @@ open class ExternalLink: Link {
addSubview(exportIcon)
trailingAnchor.constraint(greaterThanOrEqualTo: exportIcon.trailingAnchor).isActive = true
if let titleLabel = titleLabel {
let dimension = titleLabel.font.pointSize
exportIcon.heightAnchor.constraint(equalToConstant: dimension).isActive = true
exportIcon.widthAnchor.constraint(equalToConstant: dimension).isActive = true
exportImageHeight = exportIcon.heightAnchor.constraint(equalToConstant: textStyle.pointSize).activate()
exportImageWidth = exportIcon.widthAnchor.constraint(equalToConstant: textStyle.pointSize).activate()
if let titleLabel {
exportIcon.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 4).isActive = true
exportIcon.bottomAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor, constant: 3).isActive = true
}

View File

@ -8,83 +8,59 @@
import UIKit
import VDSColorTokens
import VDS
open class Link: VDS.TextLink, VDSMoleculeViewProtocol {
@objcMembers open class Link: Button {
//--------------------------------------------------
// MARK: - Draw
// MARK: - Public Properties
//--------------------------------------------------
open var viewModel: LinkModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
open override func draw(_ rect: CGRect) {
//--------------------------------------------------
// MARK: - Public Functions
//--------------------------------------------------
open func viewModelDidUpdate() {
isEnabled = viewModel.enabled
size = viewModel.size
text = viewModel.title
surface = viewModel.surface
guard let textRect = titleLabel?.frame,
let context = UIGraphicsGetCurrentContext()
else { return }
onClick = { [weak self] control in
guard let self else { return }
MVMCoreUIActionHandler.performActionUnstructured(with: self.viewModel.action,
sourceModel: self.viewModel,
additionalData: self.additionalData,
delegateObject: self.delegateObject)
}
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func updateAccessibility() {
super.updateAccessibility()
// Set line to the same color as the text
if let color = titleLabel?.textColor?.cgColor {
context.setStrokeColor(color)
guard let viewModel = viewModel else { return }
if let accessibilityText = viewModel.accessibilityText {
self.accessibilityLabel = accessibilityText
}
// x should be according to the text, not the button
let x = textRect.origin.x
// Line is 0 point below the text
let y = textRect.origin.y + textRect.size.height
context.move(to: CGPoint(x: x, y: y))
context.addLine(to: CGPoint(x: x + textRect.size.width, y: y))
context.strokePath()
}
open override var intrinsicContentSize: CGSize {
guard let size = titleLabel?.intrinsicContentSize else { return super.intrinsicContentSize }
return CGSize(width: size.width, height: size.height + 1)
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? LinkModel else { return }
setTitle(model.title, for: .normal)
if let accessibilityText = model.accessibilityText {
accessibilityLabel = accessibilityText
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
self.accessibilityIdentifier = accessibilityIdentifier
}
setTitleColor((model.inverted ? model.enabledColor_inverted : model.enabledColor).uiColor, for: .normal)
setTitleColor((model.inverted ? model.disabledColor_inverted : model.disabledColor).uiColor, for: .disabled)
setTitleColor((model.inverted ? model.activeColor_inverted : model.activeColor).uiColor, for: .highlighted)
isEnabled = model.enabled
titleLabel?.font = model.getFont(model.size)
set(with: model.action, delegateObject: delegateObject, additionalData: additionalData)
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 31 }
}
//--------------------------------------------------
// MARK: - MVMCoreViewProtocol
//--------------------------------------------------
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 31 }
// MARK: - MVMCoreViewProtocol
extension Link {
open func updateView(_ size: CGFloat) { }
open override func updateView(_ size: CGFloat) {
super.updateView(size)
}
open override func setupView() {
super.setupView()
backgroundColor = .clear
contentMode = .redraw
setTitleColor(VDSColor.elementsPrimaryOnlight, for: .normal)
setTitleColor(VDSColor.interactiveDisabledOnlight, for: .disabled)
setTitleColor(VDSColor.interactiveActiveOnlight, for: .highlighted)
titleLabel?.numberOfLines = 1
titleLabel?.lineBreakMode = .byTruncatingTail
titleLabel?.textAlignment = .left
contentHorizontalAlignment = .left
contentVerticalAlignment = .top
}
open func setupView() {}
}
// MARK: - MVMCoreUIViewConstrainingProtocol

View File

@ -7,7 +7,7 @@
//
import UIKit
import VDSColorTokens
import VDS
open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableModelProtocol {
//--------------------------------------------------
@ -23,15 +23,9 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode
public var accessibilityText: String?
public var action: ActionModelProtocol
public var enabled = true
public var enabledColor = Color(uiColor: VDSColor.elementsPrimaryOnlight)
public var enabledColor_inverted = Color(uiColor: VDSColor.elementsPrimaryOndark)
public var disabledColor = Color(uiColor: VDSColor.interactiveDisabledOnlight)
public var disabledColor_inverted = Color(uiColor: VDSColor.interactiveDisabledOndark)
public var activeColor = Color(uiColor: VDSColor.interactiveActiveOnlight)
public var activeColor_inverted = Color(uiColor: VDSColor.interactiveActiveOndark)
public var inverted = false
public var size:linkFontSize = linkFontSize.small
public var size: TextLink.Size = .small
public var shouldMaskRecordedView: Bool? = false
@ -57,34 +51,10 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode
case title
case action
case enabled
case enabledColor
case enabledColor_inverted
case disabledColor
case disabledColor_inverted
case activeColor
case activeColor_inverted
case inverted
case size
case shouldMaskRecordedView
}
public enum linkFontSize: String, Codable {
case small
case large
}
//--------------------------------------------------
// MARK: - Method
//--------------------------------------------------
func getFont(_ type: linkFontSize) -> UIFont {
switch type {
case .small:
return MFStyler.fontRegularBodySmall()
case .large:
return MFStyler.fontRegularBodyLarge()
}
}
//--------------------------------------------------
// MARK: - Codec
@ -108,30 +78,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode
self.inverted = inverted
}
if let enabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .enabledColor) {
self.enabledColor = enabledColor
}
if let enabledColor_inverted = try typeContainer.decodeIfPresent(Color.self, forKey: .enabledColor_inverted) {
self.enabledColor_inverted = enabledColor_inverted
}
if let disabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor) {
self.disabledColor = disabledColor
}
if let disabledColor_inverted = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor_inverted) {
self.disabledColor_inverted = disabledColor_inverted
}
if let activeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .activeColor) {
self.activeColor = activeColor
}
if let activeColor_inverted = try typeContainer.decodeIfPresent(Color.self, forKey: .activeColor_inverted) {
self.activeColor_inverted = activeColor_inverted
}
if let size = try typeContainer.decodeIfPresent(linkFontSize.self, forKey: .size) {
if let size = try typeContainer.decodeIfPresent(TextLink.Size.self, forKey: .size) {
self.size = size
}
@ -148,13 +95,11 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode
try container.encodeModel(action, forKey: .action)
try container.encode(inverted, forKey: .inverted)
try container.encode(enabled, forKey: .enabled)
try container.encode(enabledColor, forKey: .enabledColor)
try container.encode(enabledColor_inverted, forKey: .enabledColor_inverted)
try container.encode(disabledColor, forKey: .disabledColor)
try container.encode(disabledColor_inverted, forKey: .disabledColor_inverted)
try container.encode(activeColor, forKey: .activeColor)
try container.encode(activeColor_inverted, forKey: .activeColor_inverted)
try container.encodeIfPresent(size, forKey: .size)
try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView)
}
}
extension LinkModel {
public var surface: Surface { inverted ? .dark : .light }
}

View File

@ -18,7 +18,7 @@ open class PillButton: VDS.Button, MVMCoreUIViewConstrainingProtocol, MFButtonPr
// MARK: - Properties
//--------------------------------------------------
public var viewModel: ButtonModel!
open var viewModel: ButtonModel!
public var delegateObject: MVMCoreUIDelegateObject?
public var additionalData: [AnyHashable: Any]?
@ -28,7 +28,7 @@ open class PillButton: VDS.Button, MVMCoreUIViewConstrainingProtocol, MFButtonPr
// MARK: - VDSMoleculeViewProtocol
//--------------------------------------------------
public func viewModelDidUpdate() {
open func viewModelDidUpdate() {
if let accessibilityIdentifier = viewModel.accessibilityIdentifier {
self.accessibilityIdentifier = accessibilityIdentifier
}
@ -37,6 +37,7 @@ open class PillButton: VDS.Button, MVMCoreUIViewConstrainingProtocol, MFButtonPr
isEnabled = viewModel.enabled
size = viewModel.size
use = viewModel.style ?? .primary
surface = viewModel.inverted ? .dark : .light
if let accessibilityText = viewModel.accessibilityText {
accessibilityLabel = accessibilityText
}

View File

@ -75,11 +75,8 @@ extension BaseItemPickerEntryField {
@objc open override func setAccessibilityString(_ accessibilityString: String?) {
var accessibilityString = accessibilityString ?? ""
if let textPickerItem = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item") {
accessibilityString += textPickerItem
}
textField.accessibilityTraits = .staticText
textField.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item")
textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
}
}

View File

@ -379,7 +379,7 @@ import UIKit
if let text = model.text, !text.isEmpty {
regexTextFieldOutputIfAvailable()
}
setAccessibilityString(model.title ?? "")
}
}
@ -399,10 +399,6 @@ extension TextEntryField {
var accessibilityString = accessibilityString ?? ""
if let txtRegular = MVMCoreUIUtility.hardcodedString(withKey: "textfield_regular") {
accessibilityString += txtRegular
}
textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
}
}

View File

@ -372,7 +372,7 @@ public typealias ActionBlockConfirmation = () -> (Bool)
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
guard let model = model as? ToggleModel else { return }
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)

View File

@ -77,9 +77,10 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol {
// MARK: - Initializer
//--------------------------------------------------
public init(_ state: Bool) {
public init(_ state: Bool, id: String = UUID().uuidString) {
self.selected = state
baseValue = state
self.id = id
}
//--------------------------------------------------

View File

@ -0,0 +1,60 @@
//
// Badge.swift
// MVMCoreUI
//
// Created by Rebecca Antonelli on 4/6/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import MVMCore
import VDS
import Combine
open class Badge: VDS.Badge, VDSMoleculeViewProtocol {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var viewModel: BadgeModel!
public var delegateObject: MVMCoreUIDelegateObject?
public var additionalData: [AnyHashable : Any]?
//--------------------------------------------------
// MARK: - Public Methods
//--------------------------------------------------
public func viewModelDidUpdate() {
text = viewModel.text
maxWidth = viewModel.maxWidth
numberOfLines = viewModel.numberOfLines
fillColor = viewModel.fillColor
surface = viewModel.surface
}
public func updateView(_ size: CGFloat) {}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func updateAccessibility() {
super.updateAccessibility()
if let viewModel {
if let accessibilityText = viewModel.accessibilityText {
self.accessibilityLabel = accessibilityText
}
}
}
}
//to deal with how it's parent constrains this control
extension Badge: MVMCoreUIViewConstrainingProtocol {
public func needsToBeConstrained() -> Bool { true }
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
}

View File

@ -0,0 +1,56 @@
//
// BadgeModel.swift
// MVMCoreUI
//
// Created by Rebecca Antonelli on 4/6/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import VDS
open class BadgeModel: MoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "badge"
public var id: String = UUID().uuidString
public var backgroundColor: Color?
//--------------------------------------------------
// MARK: - VDS Properties
//--------------------------------------------------
public var text: String = ""
public var accessibilityText: String?
public var maxWidth: CGFloat?
public var numberOfLines: Int = 1
public var fillColor = Badge.FillColor.red
public var surface: Surface = .light
private enum CodingKeys: String, CodingKey {
case id, text, accessibilityText, fillColor, surface, numberOfLines, maxWidth
}
required public convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
text = try container.decode(String.self, forKey: .text)
accessibilityText = try container.decodeIfPresent(String.self, forKey: .accessibilityText)
fillColor = try container.decodeIfPresent(Badge.FillColor.self, forKey: .fillColor) ?? .red
surface = try container.decodeIfPresent(Surface.self, forKey: .surface) ?? .light
numberOfLines = try container.decodeIfPresent(Int.self, forKey: .numberOfLines) ?? 1
maxWidth = try container.decodeIfPresent(CGFloat.self, forKey: .maxWidth)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(text, forKey: .text)
try container.encode(accessibilityText, forKey: .accessibilityText)
try container.encode(fillColor, forKey: .fillColor)
try container.encode(surface, forKey: .surface)
try container.encode(numberOfLines, forKey: .numberOfLines)
try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
}
}

View File

@ -14,6 +14,7 @@
public let checkbox = Checkbox()
public let label = Label(fontStyle: .RegularBodySmall)
private var observation: NSKeyValueObservation? = nil
//--------------------------------------------------
// MARK: - Properties
@ -63,6 +64,12 @@
bottomLabelConstraint.isActive = true
alignCheckbox(.center)
isAccessibilityElement = true
accessibilityHint = checkbox.accessibilityHint
accessibilityTraits = checkbox.accessibilityTraits
observation = observe(\.checkbox.isSelected, options: [.new]) { [weak self] _, _ in
self?.updateAccessibilityLabel()
}
}
@objc override open func updateView(_ size: CGFloat) {
@ -111,6 +118,7 @@
checkbox.set(with: checkBoxWithLabelModel.checkbox, delegateObject, additionalData)
label.set(with: checkBoxWithLabelModel.label, delegateObject, additionalData)
updateAccessibilityLabel()
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
@ -124,4 +132,13 @@
checkbox.reset()
alignCheckbox(.center)
}
override open func accessibilityActivate() -> Bool {
checkbox.accessibilityActivate()
}
open func updateAccessibilityLabel() {
checkbox.updateAccessibilityLabel()
accessibilityLabel = [checkbox.accessibilityLabel, label.text].compactMap { $0 }.joined(separator: ",")
}
}

View File

@ -15,7 +15,7 @@ public enum CheckboxPosition: String, Codable {
case bottom
}
@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol {
@objcMembers open class CheckboxLabelModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
open class var identifier: String { "checkboxLabel" }
public var moleculeName: String = CheckboxLabelModel.identifier
@DecodableDefault.UUIDString public var id: String
@ -25,6 +25,7 @@ public enum CheckboxPosition: String, Codable {
public var checkbox: CheckboxModel
public var label: LabelModel
public var children: [MoleculeModelProtocol] { [checkbox, label] }
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -44,6 +44,7 @@ public typealias ActionBlock = () -> ()
public var shouldMaskWhileRecording: Bool = false
public var model: MoleculeModelProtocol?
//------------------------------------------------------
// MARK: - Multi-Action Text
//------------------------------------------------------
@ -408,6 +409,7 @@ public typealias ActionBlock = () -> ()
attributedText = attributedString
originalAttributedString = attributedText
}
self.model = labelModel
}
@objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {

View File

@ -7,116 +7,112 @@
//
import UIKit
import VDS
@objcMembers open class Line: View {
@objcMembers open class Line: VDS.Line, VDSMoleculeViewProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var viewModel: LineModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
var lineModel: LineModel? {
get { return model as? LineModel }
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
public var heightConstraint: NSLayoutConstraint?
public var widthConstraint: NSLayoutConstraint?
open func updateLineConstraints(constant: CGFloat) {
if let useVerticalLine = lineModel?.useVerticalLine, useVerticalLine {
heightConstraint?.isActive = false
widthConstraint?.isActive = true
widthConstraint?.constant = constant
} else {
widthConstraint?.isActive = false
heightConstraint?.isActive = true
heightConstraint?.constant = constant
open override var orientation: Line.Orientation {
didSet {
if orientation == .horizontal {
setContentHuggingPriority(.defaultLow, for: .horizontal)
setContentHuggingPriority(.required, for: .vertical)
setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
setContentCompressionResistancePriority(.required, for: .vertical)
} else {
setContentHuggingPriority(.required, for: .horizontal)
setContentHuggingPriority(.defaultLow, for: .vertical)
setContentCompressionResistancePriority(.required, for: .horizontal)
setContentCompressionResistancePriority(.defaultLow, for: .vertical)
}
}
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
//--------------------------------------------------
public convenience init(pinTo view: UIView, edge: UIRectEdge, useMargin: Bool) {
self.init(frame: .zero)
addLine(to: view, edge: edge, useMargin: useMargin)
}
public init() {
super.init(frame: .zero)
model = LineModel(type: .secondary)
setStyle(.secondary)
public required init() {
super.init()
viewModel = LineModel(type: .secondary)
}
public override init(frame: CGRect) {
super.init(frame: frame)
model = LineModel(type: .secondary)
setStyle(.secondary)
viewModel = LineModel(type: .secondary)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
model = LineModel(type: .secondary)
setStyle(.secondary)
viewModel = LineModel(type: .secondary)
}
public required init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
super.init(model: model, delegateObject, additionalData)
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
open func setStyle(_ style: LineModel.Style) {
lineModel?.type = style
backgroundColor = lineModel?.backgroundColor?.uiColor
updateLineConstraints(constant: lineModel?.thickness ?? 1)
}
open func shouldBeVisible() -> Bool {
guard let type = lineModel?.type else { return false }
guard let type = viewModel?.type else { return false }
return type != .none
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open func setStyle(_ style: LineModel.Style) {
viewModel.type = style
update(viewModel: viewModel)
}
open func addLine(to view: UIView, edge: UIRectEdge, useMargin: Bool) {
view.addSubview(self)
NSLayoutConstraint.activate(Array(NSLayoutConstraint.pinView(toSuperview: self, useMargins: useMargin, pinTop: edge != .bottom, pinBottom: edge != .top, pinLeft: edge != .right, pinRight: edge != .left).values))
NSLayoutConstraint.activate(
Array(
NSLayoutConstraint.pinView(toSuperview: self,
useMargins: useMargin,
pinTop: edge != .bottom,
pinBottom: edge != .top,
pinLeft: edge != .right,
pinRight: edge != .left).values
)
)
}
open override func setupView() {
super.setupView()
heightConstraint = heightAnchor.constraint(equalToConstant: 1)
heightConstraint?.isActive = true
widthConstraint = widthAnchor.constraint(equalToConstant: 1)
widthConstraint?.isActive = false
open override func draw(_ rect: CGRect) {
guard viewModel.type != .none else { return }
super.draw(rect)
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
//--------------------------------------------------
// MARK: - VDSMoleculeViewProtocol
//--------------------------------------------------
open func viewModelDidUpdate() {
surface = viewModel.surface
style = VDS.Line.Style(rawValue: viewModel.type.rawValue) ?? .primary
orientation = viewModel.orientation
}
if let lineModel = model as? LineModel {
setStyle(lineModel.type)
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
public func updateView(_ size: CGFloat) {
setNeedsDisplay()
}
open override func reset() {
setStyle(.secondary)
}
public override static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
return (model as? LineModel)?.thickness ?? 1
public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
return 1
}
}
//--------------------------------------------------
// MARK: - MVMCoreUIViewConstrainingProtocol
//--------------------------------------------------
extension Line: MVMCoreUIViewConstrainingProtocol {
open func needsToBeConstrained() -> Bool {

View File

@ -8,8 +8,9 @@
import UIKit
import VDSColorTokens
import VDS
@objcMembers public class LineModel: MoleculeModelProtocol {
public class LineModel: MoleculeModelProtocol, Invertable {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
@ -30,12 +31,12 @@ import VDSColorTokens
/**
The style of the line:
- secondary (1 height, silver)
- primary (1 height, black)
- standard (1 height, silver) - deprecated
- thin (1 height, black) - deprecated
- medium (2 height, black)
- heavy (4 height, black)
- secondary (VDS Secondary)
- primary (VDS Primary)
- standard (VDS Secondary) - deprecated
- thin (VDS Primar) - deprecated
- medium (VDS Primar)
- heavy (VDS Primar)
- none (hidden)
*/
public enum Style: String, Codable {
@ -54,53 +55,14 @@ import VDSColorTokens
public static var identifier: String = "line"
public var id: String = UUID().uuidString
public var backgroundColor: Color?
public var type: Style = .secondary
public var frequency: Frequency? = .allExceptTop
//TODO: use color insted of backgroundColor. Needs server changes
// public var color: Color?
private var _backgroundColor: Color?
public var backgroundColor: Color? {
get {
if let backgroundColor = _backgroundColor { return backgroundColor }
if inverted {
if type == .secondary || type == .standard { return Color(uiColor: VDSColor.paletteGray20) }
return Color(uiColor: VDSColor.elementsPrimaryOndark)
}
if type == .secondary || type == .standard { return Color(uiColor: VDSColor.paletteGray85) }
return Color(uiColor: VDSColor.elementsPrimaryOnlight)
}
set {
_backgroundColor = newValue
}
}
private var _thickness: CGFloat?
public var thickness: CGFloat {
get {
if let thickness = _thickness { return thickness }
switch type {
case .heavy:
return 4
case .medium:
return 2
case .none:
return 0
default:
return 1
}
}
set {
_thickness = newValue
}
}
public var inverted: Bool = false
// Use this to show vertical line
// Default is false
public var useVerticalLine: Bool?
public var orientation: VDS.Line.Orientation = .horizontal
//--------------------------------------------------
// MARK: - Initializer
@ -108,13 +70,11 @@ import VDSColorTokens
public init(type: Style) {
self.type = type
self.useVerticalLine = false
}
public init(verticalLineOf type: Style, backgroundColor: Color? = nil) {
public init(verticalLineOf type: Style) {
self.type = type
self.backgroundColor = backgroundColor
self.useVerticalLine = true
orientation = .vertical
}
//--------------------------------------------------
@ -125,13 +85,10 @@ import VDSColorTokens
case id
case moleculeName
case type
case backgroundColor
case backgroundColor_inverted
case color
case frequency
case inverted
case useVerticalLine
case thickness
case orientation
}
//--------------------------------------------------
@ -155,9 +112,12 @@ import VDSColorTokens
self.inverted = inverted
}
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
useVerticalLine = try typeContainer.decodeIfPresent(Bool.self, forKey: .useVerticalLine)
_thickness = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .thickness)
/// adding code to look for the old useVerticalLine or the new orientation
if let useVerticalLine = try typeContainer.decodeIfPresent(Bool.self, forKey: .useVerticalLine) {
orientation = useVerticalLine ? .vertical : .horizontal
} else if let orientation = try typeContainer.decodeIfPresent(VDS.Line.Orientation.self, forKey: .orientation) {
self.orientation = orientation
}
}
public func encode(to encoder: Encoder) throws {
@ -167,8 +127,6 @@ import VDSColorTokens
try container.encode(type, forKey: .type)
try container.encode(inverted, forKey: .inverted)
try container.encodeIfPresent(frequency, forKey: .frequency)
try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(useVerticalLine, forKey: .useVerticalLine)
try container.encodeIfPresent(_thickness, forKey: .thickness)
try container.encode(orientation == .vertical, forKey: .useVerticalLine)
}
}

View File

@ -13,8 +13,11 @@ import Foundation
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
var progressBarModel: ProgressBarModel?
public var model: MoleculeModelProtocol?
public var progressBarModel: ProgressBarModel? {
get { model as? ProgressBarModel }
set { model = newValue }
}
var thickness: CGFloat = 8.0 {
willSet(newValue) {

View File

@ -18,7 +18,13 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var viewModel: TileletModel!
public var model: MoleculeModelProtocol?
public var viewModel: TileletModel! {
get { model as? TileletModel }
set { model = newValue }
}
public var delegateObject: MVMCoreUIDelegateObject?
public var additionalData: [AnyHashable: Any]?

View File

@ -16,8 +16,11 @@ extension Icon.Size: Codable {}
extension TileContainer.BackgroundColor: Codable {}
extension TileContainer.Padding: Codable {}
extension TileContainer.AspectRatio: Codable {}
extension TextLink.Size: Codable {}
extension VDS.Line.Style: Codable {}
extension VDS.Line.Orientation: Codable {}
extension Use: Codable {}
extension VDS.Button.Size: RawRepresentableCodable {
public static var mapping: [String : VDS.Button.Size] { ["standard": .large, "tiny": .small] }
public static var defaultValue: VDS.Button.Size? { nil }
}
}

View File

@ -42,6 +42,13 @@ import Foundation
return Stack<StackModel>.createStack(with: [headline, horizontalStack], spacing: 8)
}()
//-------------------------------------------------------
// MARK: - Constraints
//-------------------------------------------------------
public var verticalLine1HeightConstraint: NSLayoutConstraint?
public var verticalLine2HeightConstraint: NSLayoutConstraint?
//-------------------------------------------------------
// MARK: - Lifecycle
//-------------------------------------------------------
@ -51,11 +58,10 @@ import Foundation
body.lineBreakMode = .byTruncatingTail
body2.lineBreakMode = .byTruncatingTail
body3.lineBreakMode = .byTruncatingTail
verticalLine1.widthConstraint?.isActive = true
verticalLine1.backgroundColor = .mvmBlack
verticalLine2.widthConstraint?.isActive = true
verticalLine2.backgroundColor = .mvmBlack
// setup lines
verticalLine1.orientation = .vertical
verticalLine2.orientation = .vertical
addMolecule(stack)
stack.restack()
@ -80,13 +86,13 @@ import Foundation
}
open func setLineHeight() {
verticalLine1.heightConstraint?.isActive = false
verticalLine1.heightConstraint = verticalLine1.heightAnchor.constraint(equalTo: body2.heightAnchor, multiplier: 1)
verticalLine1.heightConstraint?.isActive = true
verticalLine1HeightConstraint?.isActive = false
verticalLine1HeightConstraint = verticalLine1.heightAnchor.constraint(equalTo: body2.heightAnchor, multiplier: 1)
verticalLine1HeightConstraint?.isActive = true
verticalLine2.heightConstraint?.isActive = false
verticalLine2.heightConstraint = verticalLine2.heightAnchor.constraint(equalTo: body3.heightAnchor, multiplier: 1)
verticalLine2.heightConstraint?.isActive = true
verticalLine2HeightConstraint?.isActive = false
verticalLine2HeightConstraint = verticalLine2.heightAnchor.constraint(equalTo: body3.heightAnchor, multiplier: 1)
verticalLine2HeightConstraint?.isActive = true
}
//----------------------------------------------------

View File

@ -20,7 +20,7 @@
//--------------------------------------------------
public var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
private var observation: NSKeyValueObservation? = nil
//--------------------------------------------------
@ -68,6 +68,7 @@
checkbox.set(with: model.checkbox, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -93,7 +94,7 @@
message += label
}
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableCheckboxAllTextAndLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
if !linkShowing {
// Make whole cell focusable if no link.

View File

@ -15,6 +15,7 @@
public let leftImage = LoadImageView(pinnedEdges: .all)
public let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink()
public var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
//--------------------------------------------------
// MARK: - Initializers
@ -53,6 +54,7 @@
leftImage.set(with: model.image, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -75,7 +77,7 @@
func updateAccessibilityLabel() {
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableIconAllTextLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none

View File

@ -14,7 +14,7 @@
public let leftImage = LoadImageView(pinnedEdges: .all)
public let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink(spacing: 2.0)
public let rightLabel = Label(fontStyle: .RegularBodySmall)
public var model: MoleculeModelProtocol?
public lazy var rightLabelStackItem: StackItem = {
return StackItem(andContain: rightLabel)
}()
@ -66,6 +66,7 @@
leftImage.set(with: model.image, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
rightLabel.set(with: model.rightLabel, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -97,7 +98,7 @@
}
func updateAccessibilityLabel() {
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableIconWithRightCaretAllTextLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none

View File

@ -15,6 +15,7 @@
public let leftLabel = Label(fontStyle: .Title2XLarge)
public let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink()
public var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
//--------------------------------------------------
// MARK: - Initializers
@ -54,6 +55,7 @@
leftLabel.text = String(model.number)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -85,7 +87,7 @@
func updateAccessibilityLabel() {
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableNumberedListAllTextAndLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none

View File

@ -15,7 +15,7 @@
let radioButton = RadioButton()
let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink()
var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
private var observation: NSKeyValueObservation? = nil
//-----------------------------------------------------
@ -62,6 +62,7 @@
radioButton.set(with: model.radioButton, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -91,7 +92,7 @@
message += label
}
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableRadioButtonAllTextAndLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
if !linkShowing {
// Make whole cell focusable if no link.

View File

@ -18,7 +18,7 @@ import UIKit
let leftImage = LoadImageView(pinnedEdges: .all)
let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink()
var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
private var observation: NSKeyValueObservation? = nil
//-----------------------------------------------------
@ -79,6 +79,7 @@ import UIKit
radioButton.set(with: model.radioButton, delegateObject, additionalData)
leftImage.set(with: model.image, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -112,7 +113,7 @@ import UIKit
message += label
}
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListLeftVariableRadioButtonAndPaymentMethodModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
if !linkShowing {
// Make whole cell focusable if no link.

View File

@ -7,7 +7,7 @@
//
@objcMembers open class ListProgressBarThin: TableViewCell {
open class ListProgressBarThin: TableViewCell {
//--------------------------------------------------
// MARK: - Outlets
//--------------------------------------------------
@ -15,7 +15,11 @@
public let progressBar = ProgressBar()
public let leftHeadline = Label(fontStyle: .BoldBodySmall)
public let leftBody = Label(fontStyle: .BoldBodySmall)
public let rightBar = Line()
public let rightBar: DataLine = {
var line = DataLine()
line.heightConstraint.constant = 2
return line
}()
public let rightLabel = Label(fontStyle: .BoldBodySmall)
private let barStackItem: StackItem
private let rightLabelStackItem: StackItem
@ -102,7 +106,6 @@
leftHeadline.styleB1(true)
leftBody.styleB2(true)
rightLabel.styleB2(true)
rightBar.setStyle(.medium)
}
//------------------------------------------------------

View File

@ -16,14 +16,14 @@ public class ListProgressBarThinModel: ListItemModel, MoleculeModelProtocol {
public var progressBar: ProgressBarModel
public var leftHeadline: LabelModel
public var leftBody: LabelModel?
public var rightBar: LineModel
public var rightBar: DataLineModel
public var rightLabel: LabelModel
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(progressBar: ProgressBarModel, leftHeadline: LabelModel, leftBody: LabelModel? = nil, rightBar: LineModel, rightLabel: LabelModel) {
public init(progressBar: ProgressBarModel, leftHeadline: LabelModel, leftBody: LabelModel? = nil, rightBar: DataLineModel, rightLabel: LabelModel) {
self.progressBar = progressBar
self.leftHeadline = leftHeadline
self.leftBody = leftBody
@ -38,9 +38,7 @@ public class ListProgressBarThinModel: ListItemModel, MoleculeModelProtocol {
override public func setDefaults() {
super.setDefaults()
rightBar.type = .medium
if rightBar.backgroundColor == nil {
rightBar.backgroundColor = Color(uiColor: .gray)
}
@ -74,7 +72,7 @@ public class ListProgressBarThinModel: ListItemModel, MoleculeModelProtocol {
progressBar = try typeContainer.decode(ProgressBarModel.self, forKey:.progressBar)
leftHeadline = try typeContainer.decode(LabelModel.self, forKey: .leftHeadline)
leftBody = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .leftBody)
rightBar = try typeContainer.decode(LineModel.self, forKey: .rightBar)
rightBar = try typeContainer.decode(DataLineModel.self, forKey: .rightBar)
rightLabel = try typeContainer.decode(LabelModel.self, forKey: .rightLabel)
try super.init(from: decoder)
}

View File

@ -11,149 +11,77 @@
// MARK: - Outlets
//--------------------------------------------------
public let heart = Heart()
public let leftHeadline = Label(fontStyle: .BoldBodySmall)
public let leftBody = Label(fontStyle: .RegularBodySmall)
public let badge = Badge()
public let leftHeadline = Label(fontStyle: .BoldTitleSmall)
public let leftBody = Label(fontStyle: .RegularMicro)
public let leftSubBody = Label(fontStyle: .RegularBodySmall)
public let rightLabel = Label(fontStyle: .RegularBodySmall)
private lazy var rightLabelStackItem: StackItem = {
StackItem(andContain: rightLabel)
}()
public var model: ListStoreLocatorModel?
public lazy var horizontalStack: Stack<StackModel> = {
return Stack<StackModel>(with: StackModel(molecules: [StackItemModel(horizontalAlignment: .fill),
StackItemModel(horizontalAlignment: .fill),
StackItemModel(horizontalAlignment: .trailing)],
axis: .horizontal, spacing: Padding.Two), stackItems: [StackItem(andContain: leftHeadline), StackItem(andContain: heart), rightLabelStackItem])
StackItemModel(horizontalAlignment: .trailing, verticalAlignment: .center)],
axis: .horizontal, spacing: Padding.Two), stackItems: [StackItem(andContain: stack), StackItem(andContain: rightLabel)])
}()
public lazy var stack: Stack<StackModel> = {
return Stack<StackModel>.createStack(with: [horizontalStack, leftBody, leftSubBody], axis: .vertical, spacing: 0)
}()
public var sizeObject: MFSizeObject? = MFSizeObject(standardSize: 12, standardiPadPortraitSize: 18)
return Stack<StackModel>(with: .init(molecules: [StackItemModel(horizontalAlignment: .leading),
StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill), StackItemModel(horizontalAlignment: .fill)],
axis: .vertical, spacing: Padding.One), stackItems: [StackItem(andContain: badge), StackItem(andContain: leftHeadline), StackItem(andContain: leftBody), StackItem(andContain: leftSubBody)])
}()
//-------------------------------------------------------
// MARK: - Lifecycle
//-------------------------------------------------------
open override func setupView() {
super.setupView()
rightLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 900), for: .horizontal)
rightLabel.setContentHuggingPriority(UILayoutPriority(rawValue: 900), for: .horizontal)
addMolecule(stack)
addMolecule(horizontalStack)
stack.restack()
horizontalStack.restack()
leftSubBody.textColor = UIColor.mfTextLightGray()
}
public override func updateView(_ size: CGFloat) {
super.updateView(size)
if let dimension = sizeObject?.getValueBased(onSize: size) {
heart.widthConstraint?.constant = dimension
}
}
//------------------------------------------------------
// MARK: - Molecule
//------------------------------------------------------
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? ListStoreLocatorModel else { return }
horizontalStack.updateContainedMolecules(with: [model.leftHeadline, model.heart, model.rightLabel], delegateObject, additionalData)
leftBody.set(with: model.leftBody, delegateObject, additionalData)
leftSubBody.set(with: model.leftSubBody, delegateObject, additionalData)
stack.updateContainedMolecules(with: [model.badge, model.leftHeadline, model.leftBody, model.leftSubBody], delegateObject, additionalData)
rightLabel.set(with: model.rightLabel, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
open override func alignAccessoryToHero() -> CGPoint? {
let heroCenter = super.alignAccessoryToHero()
if let heroCenter = heroCenter {
let convertedPoint = horizontalStack.convert(heroCenter, from: self)
rightLabelStackItem.containerHelper.alignCenterVerticalConstraint?.constant = convertedPoint.y - horizontalStack.bounds.midY
}
return heroCenter
}
public override func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) {
if listItemModel?.action != nil {
super.didSelectCell(at: index, delegateObject: delegateObject, additionalData: additionalData)
} else {
heart.tapAction()
updateAccessibilityLabel()
}
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 120 }
open override func reset() {
super.reset()
leftHeadline.setFontStyle(.BoldBodySmall)
leftBody.setFontStyle(.RegularBodySmall)
leftHeadline.setFontStyle(.BoldTitleSmall)
leftBody.setFontStyle(.RegularMicro)
leftSubBody.setFontStyle(.RegularBodySmall)
rightLabel.setFontStyle(.RegularBodySmall)
leftSubBody.textColor = UIColor.mfTextLightGray()
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
func getAccessibilityMessage() -> String? {
var message = ""
heart.updateAccessibilityLabel()
if let leftHeadlineText = leftHeadline.text, !leftHeadlineText.isEmpty {
message += leftHeadlineText + ", "
}
if let leftBodyText = leftBody.text, !leftBodyText.isEmpty {
message += leftBodyText + ", "
}
if let leftSubBodyText = leftSubBody.text, !leftSubBodyText.isEmpty {
message += leftSubBodyText + ", "
}
if let rightLabelText = rightLabel.text, !rightLabelText.isEmpty {
message += rightLabelText
}
return message.count > 0 ? message : nil
}
func updateAccessibilityLabel() {
let hasHeart = !(horizontalStack.stackModel?.molecules[1].gone ?? true)
if let accessoryView = accessoryView,
hasHeart {
// Both accessory and heart actions.
isAccessibilityElement = false
accessoryView.accessibilityLabel = getAccessibilityMessage()
accessibilityElements = [accessoryView, heart]
} else {
// Make whole cell focusable if no action.
isAccessibilityElement = true
var message = getAccessibilityMessage()
if hasHeart {
accessibilityHint = heart.accessibilityHint
if let heartLabel = heart.accessibilityLabel {
message = (message ?? "") + ", " + heartLabel
}
} else {
accessibilityHint = nil
}
accessibilityLabel = message
}
}
// Ensures voice over does not read "selected" after user triggers action on cell.
override public var accessibilityTraits: UIAccessibilityTraits {
get {
if (accessoryView != nil) {
return .button
} else if (!(horizontalStack.stackModel?.molecules[1].gone ?? true)) {
return heart.accessibilityTraits
} else {
return .none
}
}
set { }
}
func updateAccessibilityLabel() {
isAccessibilityElement = true
let message = [model?.badge?.text, model?.leftHeadline.text, model?.leftBody.text, model?.leftSubBody.text, model?.rightLabel.text].compactMap { $0 }.joined(separator: ", ")
accessibilityLabel = message
}
}

View File

@ -12,7 +12,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol {
//--------------------------------------------------
public static var identifier = "listStoreLocator"
public var heart: HeartModel?
public var badge: BadgeModel?
public var leftHeadline: LabelModel
public var leftBody: LabelModel
public var leftSubBody: LabelModel
@ -22,8 +22,8 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol {
// MARK: - Initializer
//--------------------------------------------------
public init(heart: HeartModel?, leftHeadline: LabelModel, leftBody: LabelModel, leftSubBody: LabelModel, rightLabel: LabelModel) {
self.heart = heart
public init(badge: BadgeModel?, leftHeadline: LabelModel, leftBody: LabelModel, leftSubBody: LabelModel, rightLabel: LabelModel) {
self.badge = badge
self.leftHeadline = leftHeadline
self.leftBody = leftBody
self.leftSubBody = leftSubBody
@ -49,7 +49,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol {
private enum CodingKeys: String, CodingKey {
case moleculeName
case heart
case badge
case leftHeadline
case leftBody
case leftSubBody
@ -62,7 +62,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol {
public required init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
heart = try typeContainer.decodeIfPresent(HeartModel.self, forKey:.heart)
badge = try typeContainer.decodeIfPresent(BadgeModel.self, forKey:.badge)
leftHeadline = try typeContainer.decode(LabelModel.self, forKey: .leftHeadline)
leftBody = try typeContainer.decode(LabelModel.self, forKey: .leftBody)
leftSubBody = try typeContainer.decode(LabelModel.self, forKey: .leftSubBody)
@ -74,7 +74,7 @@ public class ListStoreLocatorModel: ListItemModel, MoleculeModelProtocol {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(heart, forKey: .heart)
try container.encodeIfPresent(badge, forKey: .badge)
try container.encode(leftHeadline, forKey: .leftHeadline)
try container.encode(leftBody, forKey: .leftBody)
try container.encode(leftSubBody, forKey: .leftSubBody)

View File

@ -20,6 +20,7 @@ import Foundation
let subHeadline = Label(fontStyle: .BoldBodySmall)
let body = Label(fontStyle: .RegularBodySmall)
let link = Link()
public var model: MoleculeModelProtocol?
//-----------------------------------------------------
// MARK: - Initializers
@ -52,6 +53,7 @@ import Foundation
stack.updateContainedMolecules(with: [model.eyebrow, model.headline, model.subHeadline, model.body, model.link],
delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -102,7 +104,7 @@ import Foundation
func updateAccessibilityLabel() {
let linkShowing = link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListOneColumnFullWidthTextAllTextAndLinksModel)?.link?.title != nil
isAccessibilityElement = !linkShowing
if !linkShowing {
// Make whole cell focusable if no link.

View File

@ -55,11 +55,11 @@ import Foundation
isAccessibilityElement = true
var message = ""
if let headlineLabel = headlineBody.headlineLabel.text {
if let headlineLabel = headlineBody.headlineLabel.accessibilityLabel ?? headlineBody.headlineLabel.text {
message += headlineLabel + ", "
}
if let messageLabel = headlineBody.messageLabel.text {
if let messageLabel = headlineBody.messageLabel.accessibilityLabel ?? headlineBody.messageLabel.text {
message += messageLabel
}

View File

@ -18,6 +18,7 @@
private let stack: Stack<StackModel>
private let arrowStackItem: StackItem
private let rightLabelStackItem: StackItem
public var model: MoleculeModelProtocol?
//-----------------------------------------------------
// MARK: - Initializers
@ -73,6 +74,7 @@
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
rightLabel.set(with: model.rightLabel, delegateObject, additionalData)
arrow.set(with: model.arrow, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -98,7 +100,7 @@
func updateAccessibilityLabel() {
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListRightVariablePriceChangeAllTextAndLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none

View File

@ -15,6 +15,7 @@
public let rightLabel = Label(fontStyle: .RegularBodySmall)
public let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink()
public var stack: Stack<StackModel>
public var model: MoleculeModelProtocol?
//--------------------------------------------------
// MARK: - Initializers
@ -65,6 +66,7 @@
guard let model = model as? ListRightVariableRightCaretAllTextAndLinksModel else { return }
rightLabel.set(with: model.rightLabel, delegateObject, additionalData)
eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData)
self.model = model
updateAccessibilityLabel()
}
@ -95,7 +97,7 @@
func updateAccessibilityLabel() {
let linkShowing = eyebrowHeadlineBodyLink.link.titleLabel?.text?.count ?? 0 > 0
let linkShowing = (model as? ListRightVariableRightCaretAllTextAndLinksModel)?.eyebrowHeadlineBodyLink.link?.title != nil
isAccessibilityElement = !linkShowing
accessibilityTraits = (isAccessibilityElement && accessoryView != nil) ? .button : .none

View File

@ -6,15 +6,26 @@
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
open class DataLine: View {
lazy var heightConstraint = heightAnchor.constraint(equalToConstant: 4)
lazy var widthConstraint = widthAnchor.constraint(equalToConstant: 20)
open override func setupView() {
super.setupView()
heightConstraint.isActive = true
widthConstraint.isActive = true
}
}
@objcMembers open class ListRightVariableTotalData: TableViewCell {
open class ListRightVariableTotalData: TableViewCell {
//-----------------------------------------------------
// MARK: - Outlets
//-----------------------------------------------------
public let leftLabel = Label(fontStyle: .BoldBodySmall)
public let rightLabel = Label(fontStyle: .RegularBodySmall)
public let bar = Line()
public let bar = DataLine()
//-----------------------------------------------------
// MARK: - Properties
@ -44,8 +55,6 @@
override open func setupView() {
super.setupView()
bar.setStyle(.heavy)
bar.widthAnchor.constraint(equalToConstant: 20).isActive = true
rightLabel.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 900), for: .horizontal)
addMolecule(stack)
stack.restack()
@ -74,7 +83,6 @@
super.reset()
leftLabel.setFontStyle(.BoldBodySmall)
rightLabel.setFontStyle(.RegularBodySmall)
bar.setStyle(.heavy)
}
//--------------------------------------------------

View File

@ -6,8 +6,18 @@
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
public struct DataLineModel: Codable, MoleculeModelProtocol {
public var id: String = UUID().uuidString
public static var identifier: String = "line"
public var backgroundColor: Color?
private enum CodingKeys: String, CodingKey {
case backgroundColor
}
}
public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -15,7 +25,7 @@ public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtoc
public static var identifier: String = "listRVLine"
public var leftLabel: LabelModel
public var rightLabel: LabelModel
public var bar: LineModel
public var bar: DataLineModel
//--------------------------------------------------
// MARK: - Method
@ -24,8 +34,6 @@ public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtoc
override public func setDefaults() {
super.setDefaults()
rightLabel.hero = 0
bar.type = .heavy
if bar.backgroundColor == nil {
bar.backgroundColor = Color(uiColor: .mvmBlue)
}
@ -35,7 +43,7 @@ public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtoc
// MARK: - Initializer
//--------------------------------------------------
public init(leftLabel: LabelModel, rightlabel:LabelModel, bar: LineModel){
public init(leftLabel: LabelModel, rightlabel:LabelModel, bar: DataLineModel) {
self.leftLabel = leftLabel
self.rightLabel = rightlabel
self.bar = bar
@ -46,7 +54,7 @@ public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtoc
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey{
private enum CodingKeys: String, CodingKey {
case moleculeName
case leftLabel
case rightLabel
@ -61,7 +69,7 @@ public class ListRightVariableTotalDataModel: ListItemModel, MoleculeModelProtoc
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
leftLabel = try typeContainer.decode(LabelModel.self, forKey: .leftLabel)
rightLabel = try typeContainer.decode(LabelModel.self, forKey: .rightLabel)
bar = try typeContainer.decode(LineModel.self, forKey: .bar)
bar = try typeContainer.decode(DataLineModel.self, forKey: .bar)
try super.init(from: decoder)
}

View File

@ -24,5 +24,14 @@
if bottomPadding == nil {
bottomPadding = PaddingDefaultVerticalSpacing
}
guard !MVMCoreGetterUtility.isOnIPad(),
horizontalAlignment == nil else { return }
if let _ = molecule as? ButtonModel {
horizontalAlignment = .fill
} else if let model = molecule as? TwoButtonViewModel,
model.primaryButton == nil || model.secondaryButton == nil {
horizontalAlignment = .fill
}
}
}

View File

@ -9,7 +9,13 @@ import VDSColorTokens
@objcMembers open class TabBar: UITabBar, MoleculeViewProtocol, TabBarProtocol, UITabBarDelegate {
public var model: TabBarModel
public var model: MoleculeModelProtocol?
public var tabModel: TabBarModel {
get { model as! TabBarModel }
set { model = newValue }
}
public var delegateObject: MVMCoreUIDelegateObject?
public let line = Line()
@ -50,7 +56,7 @@ import VDSColorTokens
setItems(tabs, animated: false)
selectedItem = tabs[model.selectedTab]
guard let lineModel = line.lineModel else { return }
guard let lineModel = line.viewModel else { return }
lineModel.inverted = model.style == .dark
line.set(with: lineModel, delegateObject, additionalData)
}
@ -68,31 +74,29 @@ import VDSColorTokens
// MARK: - UITabBarDelegate
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
model.selectedTab = item.tag
let action = model.tabs[item.tag].action
tabModel.selectedTab = item.tag
let action = tabModel.tabs[item.tag].action
Task(priority: .userInitiated) {
try await Button.performButtonAction(with: action, button: item, delegateObject: delegateObject, additionalData: nil)
}
}
// MARK: - TabBarProtocol
@MainActor
public func highlightTab(at index: Int) {
MVMCoreDispatchUtility.performBlock(onMainThread: {
guard let newSelectedItem = self.items?[index] else { return }
self.model.selectedTab = index
self.selectedItem = newSelectedItem
})
guard let newSelectedItem = items?[index] else { return }
tabModel.selectedTab = index
selectedItem = newSelectedItem
}
@MainActor
public func selectTab(at index: Int) {
MVMCoreDispatchUtility.performBlock(onMainThread: {
guard let newSelectedItem = self.items?[index] else { return }
self.selectedItem = newSelectedItem
self.tabBar(self, didSelect: newSelectedItem)
})
guard let newSelectedItem = items?[index] else { return }
selectedItem = newSelectedItem
tabBar(self, didSelect: newSelectedItem)
}
public func currentTabIndex() -> Int { model.selectedTab }
public func currentTabIndex() -> Int { tabModel.selectedTab }
}
extension UITabBarItem: MFButtonProtocol { }

View File

@ -174,7 +174,7 @@ import VDSColorTokens
self.additionalData = additionalData
selectedIndex = tabsModel?.selectedIndex ?? 0
selectionLine.backgroundColor = tabsModel?.selectedBarColor.uiColor
let lineModel = bottomLine.lineModel ?? LineModel(type: .secondary)
let lineModel = bottomLine.viewModel ?? LineModel(type: .secondary)
lineModel.inverted = tabsModel?.style == .dark
bottomLine.set(with: lineModel, delegateObject, additionalData)
reloadData()

View File

@ -18,6 +18,7 @@
public var hideArrow: Bool?
public var line: LineModel?
public var style: ListItemStyle?
public var accessibilityText: String?
//--------------------------------------------------
// MARK: - Keys
@ -29,6 +30,7 @@
case hideArrow
case line
case style
case accessibilityText
}
//--------------------------------------------------
@ -102,6 +104,7 @@
hideArrow = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideArrow)
line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line)
style = try typeContainer.decodeIfPresent(ListItemStyle.self, forKey: .style)
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
try super.init(from: decoder)
}
@ -113,5 +116,6 @@
try container.encodeIfPresent(hideArrow, forKey: .hideArrow)
try container.encodeIfPresent(line, forKey: .line)
try container.encodeIfPresent(style, forKey: .style)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
}
}

View File

@ -9,13 +9,15 @@
import Foundation
open class HeadlineBodyToggleModel: MoleculeModelProtocol {
open class HeadlineBodyToggleModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
public static var identifier: String = "headlineBodyToggle"
public var moleculeName: String = HeadlineBodyToggleModel.identifier
@DecodableDefault.UUIDString public var id: String
open var backgroundColor: Color?
open var headlineBody: HeadlineBodyModel
open var toggle: ToggleModel
public var children: [MoleculeModelProtocol] { [headlineBody, toggle] }
public init(_ headlineBody: HeadlineBodyModel, _ toggle: ToggleModel) {
self.headlineBody = headlineBody

View File

@ -93,11 +93,9 @@ open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol {
button?.style = .secondary
switch style {
case .error, .warning:
button?.enabledTextColor = Color(uiColor: .mvmBlack)
button?.enabledBorderColor = Color(uiColor: .mvmBlack)
button?.inverted = false
default:
button?.enabledTextColor = Color(uiColor: .mvmWhite)
button?.enabledBorderColor = Color(uiColor: .mvmWhite)
button?.inverted = true
}
if closeButton?.color == nil {

View File

@ -384,7 +384,7 @@ open class Carousel: View {
extension Carousel: UICollectionViewDelegateFlowLayout {
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemWidth = collectionView.bounds.width * itemWidthPercent
let itemWidth = (collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right) * itemWidthPercent
return CGSize(width: itemWidth, height: collectionView.bounds.height)
}

View File

@ -48,13 +48,6 @@ open class Stack<T>: Container where T: (StackModelProtocol & MoleculeModelProto
}
isAccessibilityElement = false
var accessibleViews: [Any] = []
for (index, view) in stackItems.enumerated() where !stackModel.molecules[index].gone {
accessibleViews.append(view)
}
accessibilityElements = accessibleViews
}
/// Removes all stack items views from the view.

View File

@ -12,6 +12,9 @@ import Foundation
public protocol AccessibilityModelProtocol {
var accessibilityIdentifier: String? { get set }
var accessibilityTraits: UIAccessibilityTraits? { get set }
var accessibilityText: String? { get set }
var accessibilityValue: String? { get set }
}
public extension AccessibilityModelProtocol {
@ -20,4 +23,19 @@ public extension AccessibilityModelProtocol {
get { nil }
set { }
}
var accessibilityTraits: UIAccessibilityTraits? {
get { nil }
set { }
}
var accessibilityText: String? {
get { nil }
set { }
}
var accessibilityValue: String? {
get { nil }
set { }
}
}

View File

@ -37,3 +37,9 @@ public extension MVMCoreUIDelegateObject {
return (moleculeDelegate as? MoleculeListProtocol & NSObjectProtocol)
}
}
public protocol MoleculeCollectionListProtocol {
/// Asks the delegate for the index of molecule.
func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath?
}

View File

@ -12,6 +12,8 @@ import MVMCore.MVMCoreViewProtocol
public protocol MoleculeViewProtocol: UIView, ModelHandlerProtocol {
var model: MoleculeModelProtocol? { get set }
/// Initializes the view with the model
init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?)
@ -33,6 +35,11 @@ public protocol MoleculeViewProtocol: UIView, ModelHandlerProtocol {
extension MoleculeViewProtocol {
public var model: MoleculeModelProtocol? {
get { nil }
set { }
}
/// Calls set with model
public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.init(frame: .zero)

View File

@ -13,10 +13,10 @@ import Foundation
var delegateObject: MVMCoreUIDelegateObject? { get set }
/// Should visually select the given tab index.
@objc func highlightTab(at index: Int)
@MainActor func highlightTab(at index: Int)
/// Should select the tab index. As if the user selected it.
@objc func selectTab(at index: Int)
@MainActor func selectTab(at index: Int)
/// Returns the current tab
@objc func currentTabIndex() -> Int

View File

@ -0,0 +1,18 @@
//
// VDS-Interpreters.swift
// MVMCoreUI
//
// Created by Matt Bruce on 9/20/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import VDS
public protocol Invertable {
var inverted: Bool { get set }
}
extension Invertable {
public var surface: Surface { return inverted ? .dark : .light }
}

View File

@ -28,5 +28,9 @@ extension VDSMoleculeViewProtocol {
viewModel = castedModel
viewModelDidUpdate()
}
public func update(viewModel: ViewModel){
set(with: viewModel, delegateObject, additionalData)
}
}

View File

@ -198,3 +198,11 @@
return modules
}
}
extension CollectionTemplate: MoleculeCollectionListProtocol {
public func getIndexPath(for molecule: CollectionItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
guard let index = (moleculesInfo?.firstIndex { $0.molecule.id == molecule.id }) else { return nil }
return IndexPath(item: index, section: 0)
}
}

View File

@ -35,9 +35,4 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate {
MVMCoreUIActionHandler.performActionUnstructured(with: closeAction, additionalData: nil, delegateObject: self.delegateObject())
})
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
accessibilityElements = [closeButton as Any, tableView as Any]
}
}

View File

@ -288,14 +288,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
/// Checks if the two molecules are equal
private func equal(moleculeA: MoleculeModelProtocol, moleculeB: MoleculeModelProtocol) -> Bool {
// TODO: move this to a better approach, maybe a UUID for each model.
// Do instance check
if let classMoleculeA = moleculeA as? NSObjectProtocol,
let classMoleculeB = moleculeB as? NSObjectProtocol {
return classMoleculeA === classMoleculeB
}
// Do json check
return moleculeA.toJSON() == moleculeB.toJSON()
moleculeA.id == moleculeB.id
}
}

View File

@ -12,11 +12,13 @@
#import "UIColor+MFConvenience.h"
#import "MFStyler.h"
#import "MVMCoreUICommonViewsUtility.h"
#import <MVMCoreUI/MVMCoreUI-Swift.h>
@interface MFLoadingViewController ()
@property (nullable, weak, nonatomic) MFLoadingSpinner *activityIndicator;
@property (nullable, weak, nonatomic) UIView *transparentBackgroundView;
@property (nullable, weak, nonatomic) Label *indicatorText;
@end
@ -27,23 +29,41 @@
view.backgroundColor = [UIColor clearColor];
self.view = view;
UIStackView *loadingStack = [[UIStackView alloc] initWithFrame:CGRectZero];
loadingStack.axis = UILayoutConstraintAxisVertical;
loadingStack.alignment = UIStackViewAlignmentCenter;
loadingStack.spacing = 20;
// Sets up the loading view.
MFLoadingSpinner *activityIndicatorView = [[MFLoadingSpinner alloc] initWithFrame:CGRectMake(0, 0, 36, 36)];
activityIndicatorView.backgroundColor = [UIColor clearColor];
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO;
[view addSubview:activityIndicatorView];
self.activityIndicator = activityIndicatorView;
self.activityIndicator.accessibilityIdentifier = @"Loader";
[activityIndicatorView pinWidthAndHeight];
Label *infoLabel = [Label label];
infoLabel.textAlignment = NSTextAlignmentCenter;
infoLabel.translatesAutoresizingMaskIntoConstraints = NO;
infoLabel.hidden = true;
self.indicatorText = infoLabel;
[loadingStack addArrangedSubview:infoLabel];
[loadingStack addArrangedSubview:activityIndicatorView];
loadingStack.translatesAutoresizingMaskIntoConstraints = NO;
[view addSubview:loadingStack];
// Sets the constraints for the activityIndicatorView
[NSLayoutConstraint constraintPinSubview:activityIndicatorView pinCenterX:YES pinCenterY:YES];
[NSLayoutConstraint constraintWithItem:infoLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:1.0 constant:1.0];
[NSLayoutConstraint constraintPinSubview:loadingStack pinCenterX:YES pinCenterY:YES];
[NSLayoutConstraint constraintPinSubview:loadingStack pinTop:NO topConstant:0 pinBottom:NO bottomConstant:0 pinLeft:YES leftConstant:0 pinRight:YES rightConstant:0];
// Sets up the transparent background view.
UIView *transparentBackground = [MVMCoreUICommonViewsUtility commonView];
transparentBackground.backgroundColor = [UIColor mfBackgroundGray];
transparentBackground.alpha = 0.9;
[view insertSubview:transparentBackground belowSubview:activityIndicatorView];
[view insertSubview:transparentBackground belowSubview:loadingStack];
self.transparentBackgroundView = transparentBackground;
// Sets the constraints of the transparent background view to be the same as the activity indicator view.
@ -61,8 +81,24 @@
[self.activityIndicator resumeSpinner];
}
- (void)startLoadingWith:(nullable NSAttributedString *) text{
if(text != nil){
self.indicatorText.attributedText = text;
self.indicatorText.accessibilityLabel = text.string;
self.indicatorText.hidden = false;
} else {
self.indicatorText.attributedText = nil;
self.indicatorText.accessibilityLabel = @"";
self.indicatorText.hidden = true;
}
[self.activityIndicator resumeSpinner];
}
- (void)stopLoading {
[self.activityIndicator pauseSpinner];
self.indicatorText.hidden = true;
self.indicatorText.attributedText = nil;
self.indicatorText.accessibilityLabel = @"";
}
@end

View File

@ -9,10 +9,14 @@
import Foundation
/// A view controller that has three main layers, a header, collection rows, and a footer. The header is added as a supplement header to the first section, and the footer is added as a supplement footer to the last section. This view controller allows for flexible space between the three layers to fit the screeen.
@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout {
@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout, RotorViewElementsProtocol {
private var topView: UIView?
private var bottomView: UIView?
public var topView: UIView?
public var middleView: UIView? {
set {}
get { collectionView }
}
public var bottomView: UIView?
private var headerView: ContainerCollectionReusableView?
private var footerView: ContainerCollectionReusableView?
private let headerID = "header"

View File

@ -8,15 +8,20 @@
import UIKit
open class ThreeLayerTableViewController: ProgrammaticTableViewController {
open class ThreeLayerTableViewController: ProgrammaticTableViewController, RotorViewElementsProtocol {
//--------------------------------------------------
// MARK: - Main Views
//--------------------------------------------------
private var topView: UIView?
private var bottomView: UIView?
private var headerView: UIView?
private var footerView: UIView?
public var topView: UIView?
public var middleView: UIView? {
get { tableView }
set { }
}
public var bottomView: UIView?
//--------------------------------------------------
// MARK: - Properties

View File

@ -9,12 +9,12 @@
import UIKit
open class ThreeLayerViewController: ProgrammaticScrollViewController {
open class ThreeLayerViewController: ProgrammaticScrollViewController, RotorViewElementsProtocol {
// The three main views
var topView: UIView?
var middleView: UIView?
var bottomView: UIView?
public var topView: UIView?
public var middleView: UIView?
public var bottomView: UIView?
var useMargins: Bool = false
// The top view can be put outside of the scrolling area.

View File

@ -183,8 +183,12 @@
// Returns a gradient lighter color;
+ (nonnull UIColor *)mfGradientColor:(nullable UIColor *)color;
// Returns if the color is dark or not
- (BOOL)isDark;
#pragma mark - Hex String
+ (nullable NSString *)hexStringForColor:(nonnull UIColor*)color;
+ (nonnull UIColor *)mfGetColorForHexWithTransparency:(nonnull NSString *)hexString;
@end

View File

@ -389,6 +389,12 @@
return [UIColor whiteColor];
}
- (BOOL)isDark {
CGFloat greyScale = 0;
[self getWhite:&greyScale alpha:nil];
return greyScale < 0.5;
}
#pragma mark - Hex String
+ (nullable NSString *)hexStringForColor:(nonnull UIColor*)color {

View File

@ -83,6 +83,11 @@ import Combine
}
extension NavigationController: MVMCoreViewManagerProtocol {
public func getAccessibilityElements() -> [Any]? {
nil
}
public func getCurrentViewController() -> UIViewController? {
guard let topViewController = topViewController else { return nil }
return MVMCoreUIUtility.getViewControllerTraversingManagers(topViewController)

View File

@ -12,6 +12,7 @@ import MVMCore
public extension UINavigationController {
/// Convenience function for setting the navigation item.
@MainActor
func setNavigationItem(with model: NavigationItemModelProtocol, for viewController: UIViewController, coordinatingWith pageBehaviorController: PageBehaviorHandlerProtocol? = nil) {
let behaviorHandler = pageBehaviorController ?? viewController as? PageBehaviorHandlerProtocol;
@ -33,6 +34,7 @@ public extension UINavigationController {
}
/// Convenience function for setting the navigation buttons.
@MainActor
func setNavigationButtons(with model: NavigationItemModelProtocol, for viewController: UIViewController) {
let delegate = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject
var leftItems: [UIBarButtonItem] = []
@ -60,6 +62,7 @@ public extension UINavigationController {
}
/// Convenience function for setting the navigation titleView.
@MainActor
func setNavigationTitleView(with model: NavigationItemModelProtocol, for viewController: UIViewController, coordinatingWith pageBehaviorController: PageBehaviorHandlerProtocol? = nil) {
guard let titleViewModel = model.titleView else { return }
@ -79,14 +82,8 @@ public extension UINavigationController {
}
}
/// Returns a ShadowImage based on the line property of NavigationItemModelProtocol
func getNavigationBarShadowImage(for navigationItemModel: NavigationItemModelProtocol) -> UIImage? {
guard let thickness = navigationItemModel.line?.thickness,
let backgroundColor = navigationItemModel.line?.backgroundColor else { return nil }
return backgroundColor.uiColor.image(CGSize(width: thickness, height: thickness))
}
/// Convenience function for setting the navigation bar ui
@MainActor
func setNavigationBarUI(with model: NavigationItemModelProtocol) {
let navigationBar = navigationBar
let font = Styler.Font.BoldTitleSmall.getFont(false)
@ -101,24 +98,30 @@ public extension UINavigationController {
appearance.backgroundColor = backgroundColor
appearance.titleTextAttributes.updateValue(tint, forKey: .foregroundColor)
appearance.titlePositionAdjustment = model.titleOffset ?? .zero
if let type = model.line?.type,
type != .none,
let color = model.line?.backgroundColor {
appearance.shadowColor = color.uiColor
} else {
appearance.shadowColor = .clear
}
appearance.shadowImage = getNavigationBarShadowImage(for: model)?.withRenderingMode(.alwaysTemplate)
appearance.setShadow(for: model.line)
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
setNavigationBarHidden(model.hidden, animated: true)
}
@MainActor
@objc @MainActor
func getViewController() -> UIViewController? {
guard let topViewController = getViewControllers().last,
let viewController = MVMCoreUIUtility.getViewControllerTraversingManagers(topViewController) else { return nil }
return viewController
}
}
public extension UINavigationBarAppearance {
func setShadow(for model: LineModel?) {
let model = model ?? LineModel(type: .secondary)
let line = Line(model: model, nil, nil)
if line.shouldBeVisible() {
shadowColor = line.lineColor
} else {
shadowColor = .clear
}
shadowImage = line.lineColor.image(CGSize(width: line.lineWidth, height: line.lineWidth)).withRenderingMode(.alwaysTemplate)
}
}

View File

@ -249,6 +249,11 @@ public extension MVMCoreUISplitViewController {
}
extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol {
public func getAccessibilityElements() -> [Any]? {
nil
}
public func getCurrentViewController() -> UIViewController? {
navigationController?.getCurrentViewController()
}

View File

@ -125,13 +125,14 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol,
/// Hides/Shows the navigation bar for the page.
open func hideNavigationBarLine(_ isHidden: Bool) {
guard self == navigationController?.topViewController else { return }
var color = UIColor.clear
if !isHidden,
let backgroundColor = (getCurrentViewController() as? PageProtocol)?.pageModel?.navigationBar?.line?.backgroundColor?.uiColor {
color = backgroundColor
var model: LineModel?
if isHidden {
model = LineModel(type: .none)
} else if let lineModel = (getCurrentViewController() as? PageProtocol)?.pageModel?.navigationBar?.line {
model = lineModel
}
navigationController?.navigationBar.standardAppearance.shadowColor = color
navigationController?.navigationBar.scrollEdgeAppearance?.shadowColor = color
navigationController?.navigationBar.standardAppearance.setShadow(for: model)
navigationController?.navigationBar.scrollEdgeAppearance?.setShadow(for: model)
}
open override func updateViews() {
@ -273,7 +274,7 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol,
// Load controller from the cache
needToTrackTabSelect = true
Task(priority: .userInitiated) {
await NavigationHandler.shared().replace(viewController: controller, navigationController: subNavigationController, tryToReplace: false, animated: true)
await NavigationHandler.shared().replace(viewController: controller, navigationController:subNavigationController, delegateObject:delegateObject(), tryToReplace: false, animated: true)
}
} else if let tabsModel = tabs.tabsModel,
let action = tabsModel.tabs[indexPath.row].action {
@ -307,6 +308,10 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol,
}
}
@objc public func getAccessibilityElements() -> [Any]? {
[tabs]
}
open func newDataReceived(in viewController: UIViewController) {
manager?.newDataReceived?(in: viewController)
hideNavigationBarLine(true)

View File

@ -25,16 +25,6 @@ public class NotificationContainerView: UIView {
super.init(coder: coder)
setupView()
}
/// Posts a layout change with taking the arguments from the view following the AccessibilityProtocol.
private func updateAccessibilityForTopAlert(_ view: UIView) {
// Update accessibility with top alert
var accessibilityArgument: Any? = view
if let view = view as? AccessibilityProtocol {
accessibilityArgument = view.getAccessibilityLayoutChangedArgument()
}
UIAccessibility.post(notification: .layoutChanged, argument: accessibilityArgument)
}
}
extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@ -56,7 +46,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
self.superview?.layoutIfNeeded()
} completion: { finished in
self.superview?.layoutIfNeeded()
self.updateAccessibilityForTopAlert(notification)
continuation.resume()
}
}
@ -64,14 +53,11 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol {
@MainActor
public func hide(notification: UIView) async {
// accessibility - below line added to notify VI user through voiceover user when the top alert is closed
UIAccessibility.post(notification: .screenChanged, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed"))
await withCheckedContinuation { continuation in
UIView.animate(withDuration: 0.5) {
self.height.isActive = true
self.superview?.layoutIfNeeded()
} completion: { finished in
UIAccessibility.post(notification: .layoutChanged, argument: nil)
self.currentNotificationView?.removeFromSuperview()
self.currentNotificationView = nil
continuation.resume()

View File

@ -72,6 +72,7 @@ open class CoreUIModelMapping: ModelMapping {
ModelRegistry.register(handler: LoadingSpinner.self, for: LoadingSpinnerModel.self)
ModelRegistry.register(handler: Video.self, for: VideoModel.self)
ModelRegistry.register(handler: Tilelet.self, for: TileletModel.self)
ModelRegistry.register(handler: Badge.self, for: BadgeModel.self)
// MARK:- Horizontal Combination Molecules
ModelRegistry.register(handler: StringAndMoleculeView.self, for: StringAndMoleculeModel.self)

View File

@ -9,19 +9,28 @@
import UIKit
import MVMCore
@objcMembers open class CoreUIObject: MVMCoreObject {
public var alertHandler: AlertHandler?
public var topNotificationHandler: NotificationHandler?
@objcMembers
public class CoreUIObject: NSObject {
private static var singleton = CoreUIObject()
public static func sharedInstance() -> CoreUIObject? { singleton }
private override init() {}
open override func defaultInitialSetup() {
public var alertHandler: AlertHandler?
public var topNotificationHandler: NotificationHandler? {
didSet {
accessibilityHandler?.registerForTopNotificationsChanges()
}
}
public var accessibilityHandler: AccessibilityHandler?
public func defaultInitialSetup() {
MVMCoreObject.sharedInstance()?.defaultInitialSetup()
CoreUIModelMapping.registerObjects()
loadHandler = MVMCoreLoadHandler()
cache = MVMCoreCache()
session = MVMCoreUISession()
sessionHandler = MVMCoreSessionTimeHandler()
actionHandler = MVMCoreUIActionHandler()
viewControllerMapping = MVMCoreUIViewControllerMappingObject()
loggingDelegate = MVMCoreUILoggingHandler()
MVMCoreObject.sharedInstance()?.session = MVMCoreUISession()
MVMCoreObject.sharedInstance()?.actionHandler = MVMCoreUIActionHandler()
MVMCoreObject.sharedInstance()?.viewControllerMapping = MVMCoreUIViewControllerMappingObject()
MVMCoreObject.sharedInstance()?.loggingDelegate = MVMCoreUILoggingHandler()
alertHandler = AlertHandler()
accessibilityHandler = AccessibilityHandler()
}
}

View File

@ -9,7 +9,8 @@
#import "MVMCoreUISession.h"
#import "MFLoadingViewController.h"
#import "NSLayoutConstraint+MFConvenience.h"
@import MVMCore.MVMCoreObject;
@import MVMCore.MVMCoreLoadingOverlayDelegateProtocol;
@import MVMCore.Swift;
@interface MVMCoreUISession () <MVMCoreLoadingOverlayDelegateProtocol>

View File

@ -17,7 +17,7 @@
#import <MVMCoreUI/MVMCoreUI-Swift.h>
CGFloat const PaddingDefault = 24;
CGFloat const PaddingDefaultHorizontalSpacing = 32;
CGFloat const PaddingDefaultHorizontalSpacing = 16;
CGFloat const PaddingDefaultVerticalSpacing = 32;
CGFloat const PaddingDefaultVerticalSpacing3 = 24;
CGFloat const PaddingBetweenFields = 24;

View File

@ -75,7 +75,9 @@
// MARK: Carousel
"MVMCoreUIPageControl_currentpage_index" = "page %@ of %d";
"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d";
"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d selected";
"MVMCoreUIPageControlslides_currentpage_index_accessibilityAnnouncement" = "slide %@ of %d";
"MVMCoreUIPageControlslides_totalslides" = "Carousel containing %d slides,";
// MARK: Styler

View File

@ -60,7 +60,10 @@
// Carousel
"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d seleccionado";
"MVMCoreUIPageControlslides_currentpage_index_accessibilityAnnouncement" = "diapositiva %@ of %d";
"MVMCoreUIPageControlslides_totalslides" = "Carrusel contiene %d diapositivas,";
//Styler
"CountDownDay" = " día";
"CountDownHour" = " hora";

View File

@ -39,7 +39,7 @@ public extension MVMCoreUIUtility {
/// - type: The type you are looking for.
/// - views: The starting array of subviews.
/// - Returns: Will return an array of any view associated with the given type. Will return an empty array of none were found.
static func findViews<T>(by type: T.Type, views: [UIView]) -> [T] {
static func findViews<T>(by type: T.Type, views: [UIView], excludedViews: [UIView] = []) -> [T] {
guard !views.isEmpty else { return [] }
@ -47,6 +47,9 @@ public extension MVMCoreUIUtility {
var matching = [T]()
for view in views {
guard !excludedViews.contains(view) else {
continue
}
if view is T {
matching.append(view as! T)
}
@ -54,12 +57,13 @@ public extension MVMCoreUIUtility {
queue.append(contentsOf: view.subviews)
}
return findViews(by: type, views: queue) + matching
return findViews(by: type, views: queue, excludedViews: excludedViews) + matching
}
@MainActor
static func visibleNavigationBarStlye() -> NavigationItemStyle? {
if let vc = MVMCoreUIUtility.getCurrentVisibleController(),
let navController = NavigationController.navigationController(),
if let navController = NavigationController.navigationController(),
let vc = navController.getViewController(),
let navigationBar = navController.getNavigationModel(from: vc) as? NavigationItemModel {
return navigationBar.style
}

View File

@ -32,9 +32,4 @@ extension RawRepresentableCodable {
throw RawRepresentableCodableError.invalid(value: "\(rawValue)")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self)
}
}