Merge branch 'develop' into bugfix/PRODDEF-24073

This commit is contained in:
Keerthy 2024-03-06 12:05:55 +05:30
commit 5f15d630e1
87 changed files with 1346 additions and 1180 deletions

View File

@ -168,6 +168,11 @@
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 */; };
5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; };
5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */; };
5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; };
5870636F2ACF238E00CA18D5 /* ReadableDecodingErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */; };
58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; };
608211282AC6B57E00C3FC39 /* MVMCoreUILoggingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */; };
7199C8162A4F3A64001568B7 /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */; };
71BE969E2AD96BE6000B5DB7 /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */; };
@ -387,7 +392,7 @@
D23A8FEE26122F7D007E14CE /* VisibleBehaviorForVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FED26122F7D007E14CE /* VisibleBehaviorForVideo.swift */; };
D23A8FF82612308D007E14CE /* PageBehaviorProtocolRequirer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */; };
D23A8FFB26123189007E14CE /* PageBehaviorModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */; };
D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */; };
D23A90002612347A007E14CE /* PageBehaviorContainerModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */; };
D23A9004261234CE007E14CE /* PageBehaviorHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */; };
D23A900926125FFB007E14CE /* GetContactBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A900826125FFB007E14CE /* GetContactBehavior.swift */; };
D23A90682614B0B4007E14CE /* CoreUIModelMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23A90672614B0B4007E14CE /* CoreUIModelMapping.swift */; };
@ -766,6 +771,11 @@
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>"; };
582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = "<group>"; };
5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = "<group>"; };
5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = "<group>"; };
5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadableDecodingErrors.swift; sourceTree = "<group>"; };
58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceableMoleculeBehaviorModel.swift; sourceTree = "<group>"; };
608211262AC6AF8200C3FC39 /* MVMCoreUILoggingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingHandler.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>"; };
@ -985,7 +995,7 @@
D23A8FED26122F7D007E14CE /* VisibleBehaviorForVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleBehaviorForVideo.swift; sourceTree = "<group>"; };
D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorProtocolRequirer.swift; sourceTree = "<group>"; };
D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorModelProtocol.swift; sourceTree = "<group>"; };
D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorHandlerModelProtocol.swift; sourceTree = "<group>"; };
D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorContainerModelProtocol.swift; sourceTree = "<group>"; };
D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageBehaviorHandlerProtocol.swift; sourceTree = "<group>"; };
D23A900826125FFB007E14CE /* GetContactBehavior.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetContactBehavior.swift; sourceTree = "<group>"; };
D23A90672614B0B4007E14CE /* CoreUIModelMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUIModelMapping.swift; sourceTree = "<group>"; };
@ -1224,10 +1234,10 @@
D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */,
014AA72323C501E2006F3E93 /* ContainerModelProtocol.swift */,
D23EA7FA2475F09800D60C34 /* CarouselItemProtocol.swift */,
01EB3683236097C0006832FA /* MoleculeModelProtocol.swift */,
012A88C3238D86E600FE3DA1 /* CarouselItemModelProtocol.swift */,
012A88B0238C880100FE3DA1 /* CarouselPagingModelProtocol.swift */,
EA05EFAA278DE53600828819 /* ClearableModelProtocol.swift */,
01EB3683236097C0006832FA /* MoleculeModelProtocol.swift */,
012A889B23889E8400FE3DA1 /* TemplateModelProtocol.swift */,
D28A837823C7D5BC00DFE4FC /* PageModelProtocol.swift */,
011B58EF23A2AA980085F53C /* ListItemModelProtocol.swift */,
@ -1343,7 +1353,7 @@
children = (
D23A8FF72612308D007E14CE /* PageBehaviorProtocolRequirer.swift */,
D23A8FFA26123189007E14CE /* PageBehaviorModelProtocol.swift */,
D23A8FFF2612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift */,
D23A8FFF2612347A007E14CE /* PageBehaviorContainerModelProtocol.swift */,
D23A9003261234CE007E14CE /* PageBehaviorHandlerProtocol.swift */,
27F973522466074500CAB5C5 /* PageBehaviorProtocol.swift */,
);
@ -1409,6 +1419,8 @@
27F973512466071600CAB5C5 /* Behaviors */ = {
isa = PBXGroup;
children = (
5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */,
58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */,
0A1C30972620F61A00B47F3B /* Protocols */,
27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */,
D23A900826125FFB007E14CE /* GetContactBehavior.swift */,
@ -1475,11 +1487,11 @@
path = OneColumn;
sourceTree = "<group>";
};
7199C8142A4F3A40001568B7 /* Accessibility */ = {
582272082B1FC53E00F75BAE /* Accessibility */ = {
isa = PBXGroup;
children = (
7199C8152A4F3A64001568B7 /* AccessibilityHandler.swift */,
71BE969D2AD96BE6000B5DB7 /* RotorHandler.swift */,
582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */,
5822720A2B1FC55F00F75BAE /* RotorHandler.swift */,
);
path = Accessibility;
sourceTree = "<group>";
@ -1596,6 +1608,7 @@
D202AFE2242A5F1400E5BEDF /* Extensions */ = {
isa = PBXGroup;
children = (
5870636E2ACF238E00CA18D5 /* ReadableDecodingErrors.swift */,
D202AFE3242A5F5E00E5BEDF /* NSTextAlignment+Extension.swift */,
0A209CD223A7E2810068F8B0 /* UIStackViewAlignment+Extension.swift */,
D21EE53B23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift */,
@ -2009,7 +2022,7 @@
D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */ = {
isa = PBXGroup;
children = (
7199C8142A4F3A40001568B7 /* Accessibility */,
582272082B1FC53E00F75BAE /* Accessibility */,
01F2C1FC27C81F9700DC3D36 /* Managers */,
D2ED27D8254B0C1F00A1C293 /* Alerts */,
27F973512466071600CAB5C5 /* Behaviors */,
@ -2742,6 +2755,7 @@
011D95A3240453F8000E3791 /* RuleRegexModel.swift in Sources */,
D2E2A98323D8B32D000B42E6 /* EyebrowHeadlineBodyLinkModel.swift in Sources */,
012A88AD238C418100FE3DA1 /* TemplateProtocol.swift in Sources */,
5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */,
BB6C6AC1242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift in Sources */,
32D2609724C19E2100B56344 /* LockupsPlanSMLXLModel.swift in Sources */,
EA985C872981AB0F00F2FF2E /* VDS-Tilelet+Codable.swift in Sources */,
@ -2762,7 +2776,7 @@
01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */,
D2A92884241ACB25004E01C6 /* ProgrammaticScrollViewController.swift in Sources */,
EA985C3E2970938F00F2FF2E /* Tilelet.swift in Sources */,
D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */,
D23A90002612347A007E14CE /* PageBehaviorContainerModelProtocol.swift in Sources */,
EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */,
0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */,
D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */,
@ -2793,6 +2807,7 @@
D2E2A9A123E095AB000B42E6 /* ButtonModelProtocol.swift in Sources */,
AF8118302AB39B0900FAD1BA /* RawRepresentableCodable.swift in Sources */,
94C2D9AB23872EB50006CF46 /* LabelAttributeActionModel.swift in Sources */,
5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */,
D22D8395241FB41200D3DF69 /* UIStackView+Extension.swift in Sources */,
52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */,
525239C02407BCFF00454969 /* ListTwoColumnPriceDetailsModel.swift in Sources */,
@ -2808,6 +2823,7 @@
0A0FEC7425D42A5E00AF2548 /* BaseItemPickerEntryField.swift in Sources */,
D29DF2A221E7AF4E003B2FB9 /* MVMCoreUIUtility.m in Sources */,
D23A8FF82612308D007E14CE /* PageBehaviorProtocolRequirer.swift in Sources */,
5870636F2ACF238E00CA18D5 /* ReadableDecodingErrors.swift in Sources */,
94C2D9A723872DA90006CF46 /* LabelAttributeColorModel.swift in Sources */,
943820842432382400B43AF3 /* WebView.swift in Sources */,
0103B84E23D7E33A009C315C /* HeadlineBodyToggleModel.swift in Sources */,
@ -2876,7 +2892,6 @@
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 */,
@ -2960,6 +2975,7 @@
EA7D81642B2BABCB00D29F9E /* TooltipModel.swift in Sources */,
AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */,
AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */,
58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */,
D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */,
D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */,
27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */,
@ -3009,6 +3025,7 @@
8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */,
AFA4932229E5EF2E001A9663 /* NotificationHandler.swift in Sources */,
BB2FB3BD247E7EF200DF73CD /* Tags.swift in Sources */,
5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */,
AA104ADC244734EA004D2810 /* HeadersH1LandingPageHeaderModel.swift in Sources */,
BBAA4F03243D8E3B005AAD5F /* RadioBoxes.swift in Sources */,
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
@ -3053,7 +3070,6 @@
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 */,

View File

@ -33,6 +33,7 @@ import Foundation
super.set(with: model, delegateObject, additionalData)
FormValidator.setupValidation(for: castModel, delegate: delegateObject?.formHolderDelegate)
setState()
}
public func setState() {

View File

@ -8,7 +8,7 @@
import Foundation
open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol {
open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol, ParentMoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -29,6 +29,14 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro
public var updateUI: ActionBlock?
public var children: [MoleculeModelProtocol] {
[image].compactMap({$0})
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &image, with: molecule)
}
public init(image: ImageViewModel?, action: ActionModelProtocol) {
self.image = image
self.action = action

View File

@ -19,6 +19,7 @@ import UIKit
//--------------------------------------------------
public private(set) var titleLabel: FormLabel = {
let label = FormLabel()
label.setFontStyle(.RegularMicro)
label.setContentCompressionResistancePriority(.required, for: .vertical)
return label
}()
@ -28,6 +29,7 @@ import UIKit
/// Provides contextual information on the TextField.
public private(set) var feedbackLabel: FormLabel = {
let label = FormLabel()
label.setFontStyle(.RegularMicro)
label.setContentCompressionResistancePriority(.required, for: .vertical)
return label
}()
@ -276,15 +278,13 @@ import UIKit
backgroundColor = .clear
isAccessibilityElement = false
titleLabel.font = Styler.Font.RegularMicro.getFont()
titleLabel.textColor = .mvmBlack
feedbackLabel.font = Styler.Font.RegularMicro.getFont()
feedbackLabel.textColor = .mvmBlack
errorLabel.font = Styler.Font.RegularMicro.getFont()
errorLabel.textColor = .mvmBlack
titleLabel.setFontStyle(.RegularMicro)
feedbackLabel.setFontStyle(.RegularMicro)
errorLabel.setFontStyle(.RegularMicro)
titleLabel.text = nil
feedbackLabel.text = nil
errorLabel.text = nil
entryFieldContainer.disableAllBorders = false
feedbackLabel.text = nil
entryFieldContainer.reset()
entryFieldModel?.updateUI = nil
}

View File

@ -9,11 +9,7 @@
import Foundation
/// Subclass of label that helps with different states
public class FormLabel: Label {
//properties used in setting label
private var delegateObject: MVMCoreUIDelegateObject?
private var additionalData: [AnyHashable: Any]?
public class FormLabel: Label {
//models that drive the label UI
private var formModel: FormLabelModel!

View File

@ -8,15 +8,18 @@
//
import MVMCore
import VDS
public typealias ActionBlock = () -> ()
@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol {
@objcMembers open class Label: VDS.Label, VDSMoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol {
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
open var viewModel: LabelModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
public var makeWholeViewClickable = false
@ -24,58 +27,23 @@ public typealias ActionBlock = () -> ()
public var standardFontSize: CGFloat = 0.0
/// Set this to use a custom sizing object during updateView instead of the standard.
@available(*, deprecated, message: "VDS is maintaining scaleSize")
public var sizeObject: MFSizeObject?
@available(*, deprecated, message: "VDS is maintaining scaleSize")
public var scaleSize: NSNumber?
/// A specific text index to use as a unique marker.
public var hero: Int?
// Used for scaling the font in updateView.
private var originalAttributedString: NSAttributedString?
public var hasText: Bool {
guard let text = text, let attributedText = attributedText else { return false }
return !text.isEmpty || !attributedText.string.isEmpty
}
public var getRange: NSRange {
NSRange(location: 0, length: text?.count ?? 0)
}
public var shouldMaskWhileRecording: Bool = false
public var model: MoleculeModelProtocol?
//------------------------------------------------------
// MARK: - Multi-Action Text
//------------------------------------------------------
/// Data store of the tappable ranges of the text.
public var clauses: [ActionableClause] = [] {
didSet {
isUserInteractionEnabled = !clauses.isEmpty
if clauses.count > 1 {
clauses.sort { first, second in
return first.range.location < second.range.location
}
}
}
}
/// Used for tappable links in the text.
public struct ActionableClause {
public var range: NSRange
public var actionBlock: ActionBlock
public var accessibilityID: Int = 0
public func performAction() {
actionBlock()
}
public init(range: NSRange, actionBlock: @escaping ActionBlock, accessibilityID: Int = 0) {
self.range = range
self.actionBlock = actionBlock
self.accessibilityID = accessibilityID
}
public var hasText: Bool {
guard let text = text, let attributedText = attributedText else { return false }
return !text.isEmpty || !attributedText.string.isEmpty
}
//------------------------------------------------------
@ -84,49 +52,27 @@ public typealias ActionBlock = () -> ()
/// Sets the clauses array to empty.
@objc public func setEmptyClauses() {
clauses = []
}
//------------------------------------------------------
// MARK: - Initialization
//------------------------------------------------------
@objc public func setupView() {
backgroundColor = .clear
numberOfLines = 0
lineBreakMode = .byWordWrapping
translatesAutoresizingMaskIntoConstraints = false
clauses = []
accessibilityCustomActions = []
accessibilityTraits = .staticText
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(textLinkTapped))
tapGesture.numberOfTapsRequired = 1
addGestureRecognizer(tapGesture)
}
@objc public init() {
super.init(frame: .zero)
setupView()
@objc public required init() {
super.init()
}
@objc required public init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
@objc override public init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
public init(fontStyle: Styler.Font, _ scale: Bool = true) {
super.init(frame: .zero)
setupView()
font = fontStyle.getFont(false)
textColor = fontStyle.color()
setScale(scale)
setFontStyle(fontStyle, scale)
}
@objc convenience public init(standardFontSize size: CGFloat) {
@ -145,7 +91,6 @@ public typealias ActionBlock = () -> ()
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.init(frame: .zero)
setupView()
styleB2(true)
set(with: model, delegateObject, additionalData)
}
@ -236,12 +181,6 @@ public typealias ActionBlock = () -> ()
}
}
enum LabelAlignment: String {
case center
case right
case left
}
@objc public func resetAttributeStyle() {
/*
* This is to address a reuse issue with iOS 13 and up.
@ -250,294 +189,121 @@ public typealias ActionBlock = () -> ()
* appropriately called.
* Only other reference found of issue: https://www.thetopsites.net/article/58142205.shtml
*/
if let attributedText = attributedText, let text = text, !text.isEmpty {
let attributedString = NSMutableAttributedString(string: text)
let range = NSRange(location: 0, length: text.count)
for attribute in attributedText.attributes(at: 0, effectiveRange: nil) {
if attribute.key == .underlineStyle {
attributedString.addAttribute(.underlineStyle, value: 0, range: range)
}
if attribute.key == .strikethroughStyle {
attributedString.addAttribute(.strikethroughStyle, value: 0, range: range)
}
if let text = text, !text.isEmpty {
//create the primary string
let mutableText = NSMutableAttributedString.mutableText(for: text,
textStyle: textStyle,
useScaledFont: useScaledFont,
textColor: textColorConfiguration.getColor(self),
alignment: textAlignment,
lineBreakMode: lineBreakMode)
if let attributes = attributes {
mutableText.apply(attributes: attributes)
}
self.attributedText = attributedString
self.attributedText = mutableText
}
}
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject? = nil, _ additionalData: [AnyHashable: Any]? = nil) {
public func viewModelDidUpdate() {
shouldMaskWhileRecording = viewModel.shouldMaskRecordedView ?? false
text = viewModel.text
hero = viewModel.hero
Label.setLabel(self, withHTML: viewModel.html)
textAlignment = viewModel.textAlignment ?? .left
surface = viewModel.surface
clauses = []
text = nil
attributedText = nil
originalAttributedString = nil
shouldMaskWhileRecording = model.shouldMaskRecordedView ?? false
guard let labelModel = model as? LabelModel else { return }
text = labelModel.text
if let accessibilityTraits = labelModel.accessibilityTraits {
self.accessibilityTraits = accessibilityTraits
}
resetAttributeStyle()
hero = labelModel.hero
Label.setLabel(self, withHTML: labelModel.html)
isAccessibilityElement = hasText
switch labelModel.textAlignment {
case .center:
textAlignment = .center
case .right:
textAlignment = .right
default:
textAlignment = .left
}
makeWholeViewClickable = labelModel.makeWholeViewClickable ?? false
if let backgroundColor = labelModel.backgroundColor {
makeWholeViewClickable = viewModel.makeWholeViewClickable ?? false
if let backgroundColor = viewModel.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
if let accessibilityText = labelModel.accessibilityText {
accessibilityLabel = accessibilityText
}
if let fontStyle = labelModel.fontStyle {
fontStyle.styleLabel(self, genericScaling: false)
standardFontSize = font.pointSize
} else {
let fontSize = labelModel.fontSize
if let fontSize = fontSize {
if let style = viewModel.fontStyle?.vdsTextStyle() {
font = style.font
textStyle = style
} else if let fontName = viewModel.fontName {
// there is a TextStyle.defaultStyle
let fontSize = viewModel.fontSize
if let fontSize {
standardFontSize = fontSize
}
if let fontName = labelModel.fontName {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? standardFontSize)
} else if let fontSize = fontSize {
font = font.updateSize(fontSize)
if let customStyle = style(for: fontName, pointSize: fontSize ?? standardFontSize), customStyle != textStyle {
font = customStyle.font
textStyle = customStyle
}
}
if let color = labelModel.textColor {
textColor = color.uiColor
if let color = viewModel.textColor {
textColorConfiguration = SurfaceColorConfiguration(color.uiColor, color.uiColor).eraseToAnyColorable()
}
if let lines = labelModel.numberOfLines {
numberOfLines = lines
if let lines = viewModel.numberOfLines {
numberOfLines = lines
}
if let attributes = labelModel.attributes, let labelText = text {
let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: font.updateSize(standardFontSize), NSAttributedString.Key.foregroundColor: textColor as UIColor])
for attribute in attributes {
guard let range = validateAttribute(range: NSRange(location: attribute.location, length: attribute.length) , in: attributedString, type: attribute.type)
else { continue }
switch attribute {
case let underlineAtt as LabelAttributeUnderlineModel:
attributedString.addAttribute(.underlineStyle, value: underlineAtt.underlineValue.rawValue, range: range)
if let underlineColor = underlineAtt.color?.uiColor {
attributedString.addAttribute(.underlineColor, value: underlineColor, range: range)
}
case _ as LabelAttributeStrikeThroughModel:
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
case let colorAtt as LabelAttributeColorModel:
if let colorHex = colorAtt.textColor {
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.foregroundColor, value: colorHex.uiColor, range: range)
}
case let imageAtt as LabelAttributeImageModel:
var fontSize = font.pointSize
if let attributeSize = imageAtt.size {
fontSize = attributeSize
}
let imageName = imageAtt.name ?? "externalLink"
let imageAttachment: NSTextAttachment
if let url = imageAtt.URL {
imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: self)
} else {
imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
}
// Confirm that the intended image location is within range.
if 0...labelText.count ~= imageAtt.location {
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: imageAtt.location)
}
case let fontAtt as LabelAttributeFontModel:
if let fontStyle = fontAtt.style {
attributedString.removeAttribute(.font, range: range)
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.font, value: fontStyle.getFont(), range: range)
attributedString.addAttribute(.foregroundColor, value: fontStyle.color(), range: range)
} else {
let fontSize = fontAtt.size
var font: UIFont?
if let fontName = fontAtt.name {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? self.font.pointSize)
} else if let fontSize = fontSize {
font = self.font.updateSize(fontSize)
}
if let font = font {
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range)
}
}
case let actionAtt as LabelAttributeActionModel:
addTappableLinkAttribute(range: NSRange(location: range.location, length: range.length)) {
MVMCoreUIActionHandler.performActionUnstructured(with: actionAtt.action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject)
}
addActionAttributes(range: range, string: attributedString)
default:
continue
}
}
attributedText = attributedString
originalAttributedString = attributedText
if let attributeModels = viewModel.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) {
attributes = attributeModels
}
}
/// See if the font that is currently set matches a VDS Font and if so grab the matching TextStyle or create custom TextStyle that
/// that the Label will use moving forward.
private func checkforFontChange() {
guard let customStyle = style(for: font.fontName, pointSize: font.pointSize), customStyle != textStyle
else { return }
textStyle = customStyle
}
private func style(for fontName: String, pointSize: CGFloat) -> TextStyle? {
guard let vdsFont = Font.from(fontName: fontName),
let customStyle = TextStyle.style(from: vdsFont, pointSize: pointSize)
else { return nil }
return customStyle
}
open override func updateView() {
checkforFontChange()
super.updateView()
}
open override func updateAccessibility() {
super.updateAccessibility()
if let accessibilityTraits = viewModel?.accessibilityTraits {
self.accessibilityTraits = accessibilityTraits
}
if let accessibilityText = viewModel?.accessibilityText {
accessibilityLabel = accessibilityText
}
}
@objc open override func reset() {
super.reset()
}
@objc open func updateView(_ size: CGFloat) { }
@objc open func setFont(_ font: UIFont, scale: Bool) {
self.font = font
setScale(scale)
}
@objc open func setScale(_ scale: Bool) {
if scale {
standardFontSize = font.pointSize
} else {
standardFontSize = 0
}
self.model = labelModel
}
@objc public static func setUILabel(_ label: UILabel?, withJSON json: [AnyHashable: Any]?, delegate: DelegateObject?, additionalData: [AnyHashable: Any]?) {
guard let label = label else { return }
// Some properties can only be set on Label.
// Label fonts should not be scaled because it will be scaled in updateView.
let mvmLabel = label as? Label
label.text = json?.optionalStringForKey(KeyText)
setLabel(label, withHTML: json?.optionalStringForKey("html"))
if let alignment = json?.optionalStringForKey("textAlignment") {
switch alignment {
case "center":
label.textAlignment = .center
case "right":
label.textAlignment = .right
default:
label.textAlignment = .left
}
}
mvmLabel?.makeWholeViewClickable = json?.boolForKey("makeWholeViewClickable") ?? false
if let backgroundColorHex = json?.optionalStringForKey(KeyBackgroundColor), !backgroundColorHex.isEmpty {
label.backgroundColor = UIColor.mfGet(forHex: backgroundColorHex)
}
label.accessibilityLabel = json?.optionalStringForKey("accessibilityText")
if let fontStyle = json?.optionalStringForKey("fontStyle") {
MFStyler.style(label: label, styleString: fontStyle, genericScaling: mvmLabel == nil)
mvmLabel?.standardFontSize = label.font.pointSize
} else {
let fontSize = json?["fontSize"] as? CGFloat
if let fontSize = fontSize {
mvmLabel?.standardFontSize = fontSize
}
if let fontName = json?.optionalStringForKey("fontName") {
label.font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
} else if let fontSize = fontSize {
label.font = label.font.updateSize(fontSize)
}
}
if let textColorHex = json?.optionalStringForKey(KeyTextColor), !textColorHex.isEmpty {
label.textColor = UIColor.mfGet(forHex: textColorHex)
}
if let attributes = json?.optionalArrayForKey("attributes"), let labelText = label.text {
let attributedString = NSMutableAttributedString(string: labelText,
attributes: [NSAttributedString.Key.font: mvmLabel?.font.updateSize(mvmLabel!.standardFontSize) ?? label.font as UIFont,
NSAttributedString.Key.foregroundColor: label.textColor as UIColor])
for case let attribute as [String: Any] in attributes {
guard let attributeType = attribute.optionalStringForKey(KeyType),
let location = attribute["location"] as? Int,
let length = attribute["length"] as? Int,
let range = validateAttribute(range: NSRange(location: location, length: length), in: attributedString, type: attributeType)
else { continue }
switch attributeType {
case "underline":
attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
case "strikethrough":
attributedString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.thick.rawValue, range: range)
attributedString.addAttribute(.baselineOffset, value: 0, range: range)
case "color":
if let colorHex = attribute.optionalStringForKey(KeyTextColor), !colorHex.isEmpty {
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.foregroundColor, value: UIColor.mfGet(forHex: colorHex), range: range)
}
case "image":
let fontSize = attribute["size"] as? CGFloat ?? label.font.pointSize
let imageName = attribute["name"] as? String ?? "externalLink"
let imageURL = attribute["URL"] as? String
let imageAttachment: NSTextAttachment
if let url = imageURL, let label = label as? Label {
imageAttachment = Label.getTextAttachmentFrom(url: url, dimension: fontSize, label: label)
} else {
imageAttachment = Label.getTextAttachmentImage(name: imageName, dimension: fontSize)
}
let mutableString = NSMutableAttributedString()
mutableString.append(NSAttributedString(attachment: imageAttachment))
attributedString.insert(mutableString, at: location)
case "font":
if let fontStyle = attribute.optionalStringForKey("style") {
let styles = MFStyler.getAttributedString(for: "0", styleString: fontStyle, genericScaling: mvmLabel == nil)
attributedString.removeAttribute(.font, range: range)
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttributes(styles.attributes(at: 0, effectiveRange: nil), range: range)
} else {
let fontSize = attribute["size"] as? CGFloat
var font: UIFont?
if let fontName = attribute.optionalStringForKey("name") {
font = MFFonts.mfFont(withName: fontName, size: fontSize ?? mvmLabel?.standardFontSize ?? label.font.pointSize)
} else if let fontSize = fontSize {
font = label.font.updateSize(fontSize)
}
if let font = font {
attributedString.removeAttribute(.font, range: range)
attributedString.addAttribute(.font, value: font, range: range)
}
}
case "action":
guard let actionLabel = label as? Label else { continue }
actionLabel.addActionAttributes(range: range, string: attributedString)
if let actionBlock = actionLabel.createActionBlockFor(actionMap: attribute, additionalData: additionalData, delegateObject: delegate) {
actionLabel.appendActionableClause(range: range, actionBlock: actionBlock)
}
default:
continue
}
}
label.attributedText = attributedString
mvmLabel?.originalAttributedString = attributedString
}
guard let label = label as? Label,
let json = json as? [String: Any],
let labelModel = try? LabelModel.decode(jsonDict: json) else { return }
label.set(with: labelModel, delegate as? MVMCoreUIDelegateObject, additionalData)
}
//------------------------------------------------------
@ -545,119 +311,50 @@ public typealias ActionBlock = () -> ()
//------------------------------------------------------
public func setFontStyle(_ fontStyle: Styler.Font, _ scale: Bool = true) {
fontStyle.styleLabel(self, genericScaling: false)
guard let style = fontStyle.vdsTextStyle() else { return }
textStyle = style
setScale(scale)
}
//------------------------------------------------------
// MARK: - 2.0 Styling Methods
//------------------------------------------------------
@objc public func styleH1(_ scale: Bool) {
MFStyler.styleLabelH1(self, genericScaling: false)
setScale(scale)
setFontStyle(.H1, scale)
}
@objc public func styleH2(_ scale: Bool) {
MFStyler.styleLabelH2(self, genericScaling: false)
setScale(scale)
setFontStyle(.H2, scale)
}
@objc public func styleH3(_ scale: Bool) {
MFStyler.styleLabelH3(self, genericScaling: false)
setScale(scale)
setFontStyle(.H3, scale)
}
@objc public func styleH32(_ scale: Bool) {
MFStyler.styleLabelH32(self, genericScaling: false)
setScale(scale)
setFontStyle(.H32, scale)
}
@objc public func styleB1(_ scale: Bool) {
MFStyler.styleLabelB1(self, genericScaling: false)
setScale(scale)
setFontStyle(.B1, scale)
}
@objc public func styleB2(_ scale: Bool) {
MFStyler.styleLabelB2(self, genericScaling: false)
setScale(scale)
setFontStyle(.B2, scale)
}
@objc public func styleB3(_ scale: Bool) {
MFStyler.styleLabelB3(self, genericScaling: false)
setScale(scale)
setFontStyle(.B3, scale)
}
@objc public func styleB20(_ scale: Bool) {
MFStyler.styleLabelB20(self, genericScaling: false)
setScale(scale)
setFontStyle(.B20, scale)
}
/// Will remove the values contained in attributedText.
func clearAttributes() {
guard let labelText = text, !labelText.isEmpty else { return }
guard let attributes = attributedText?.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: labelText.count))
else { return }
let attributedString = NSMutableAttributedString(string: labelText)
for attribute in attributes {
attributedString.removeAttribute(attribute.key, range: NSRange(location: 0, length: labelText.count))
}
attributedText = attributedString
}
}
@objc public func updateView(_ size: CGFloat) {
scaleSize = size as NSNumber
if let originalAttributedString = originalAttributedString {
let attributedString = NSMutableAttributedString(attributedString: originalAttributedString)
attributedString.removeAttribute(.font, range: NSRange(location: 0, length: attributedString.length))
// Loop the original attributed string, resize the fonts.
originalAttributedString.enumerateAttribute(.font, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
if let fontObj = value as? UIFont, let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: fontObj.pointSize)?.getValueBased(onSize: size) {
attributedString.addAttribute(.font, value: fontObj.updateSize(stylerSize) as Any, range: range)
}
}
// Loop the original attributed string, resize the image attachments.
originalAttributedString.enumerateAttribute(.attachment, in: NSRange(location: 0, length: originalAttributedString.length), options: []) { value, range, stop in
if let attachment = value as? NSTextAttachment,
let stylerSize = MFStyler.sizeObjectGeneric(forCurrentDevice: attachment.bounds.width)?.getValueBased(onSize: size) {
let dimension = round(stylerSize)
attachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
}
}
attributedText = attributedString
} else if !MVMCoreGetterUtility.fequal(a: Float(standardFontSize), b: 0.0), let sizeObject = sizeObject ?? MFStyler.sizeObjectGeneric(forCurrentDevice: standardFontSize) {
font = font.updateSize(sizeObject.getValueBased(onSize: size))
}
}
@objc public func setFont(_ font: UIFont, scale: Bool) {
self.font = font
setScale(scale)
}
@objc public func setScale(_ scale: Bool) {
if scale {
standardFontSize = font.pointSize
if let floatScale = scaleSize?.floatValue {
updateView(CGFloat(floatScale))
} else {
updateView(MVMCoreUIUtility.getWidth())
}
} else {
standardFontSize = 0
}
}
// Mark: - Old Helpers
extension Label {
/**
Appends an external link image to the end of the attributed string.
Will provide one whitespace to the left of the icon; adds 2 chars to the end of the string.
@ -689,7 +386,7 @@ public typealias ActionBlock = () -> ()
self.attributedText = mutableString
}
/*
Retrieves an NSTextAttachment for NSAttributedString that is prepped to be inserted with the text.
@ -705,47 +402,6 @@ public typealias ActionBlock = () -> ()
return imageAttachment
}
static func getTextAttachmentFrom(url: String, dimension: CGFloat, label: Label) -> NSTextAttachment {
let imageAttachment = NSTextAttachment()
imageAttachment.bounds = CGRect(x: 0, y: 0, width: dimension, height: dimension)
DispatchQueue.global(qos: .default).async {
MVMCoreCache.shared()?.getImage(url, useWidth: false, widthForS7: 0, useHeight: false, heightForS7: 0, localFallbackImageName: nil) { image, data, _ in
DispatchQueue.main.sync {
imageAttachment.image = image
label.setNeedsDisplay()
}
}
}
return imageAttachment
}
/// Call to detect in the attributedText contains an NSTextAttachment.
func containsTextAttachment() -> Bool {
guard let attributedText = attributedText else { return false }
var containsAttachment = false
attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, stop in
if value is NSTextAttachment {
containsAttachment = true
return
}
}
return containsAttachment
}
func appendActionableClause(range: NSRange, actionBlock: @escaping ActionBlock) {
accessibilityTraits = .button
let accessibleAction = customAccessibilityAction(range: range)
clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1))
}
public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect {
guard let abstractContainer = label.abstractTextContainer() else { return CGRect() }
@ -789,25 +445,11 @@ public typealias ActionBlock = () -> ()
return (textContainer, layoutManager, textStorage)
}
}
// MARK: - Atomization
extension Label {
public func reset() {
text = nil
attributedText = nil
hero = nil
textAlignment = .left
originalAttributedString = nil
styleB2(true)
accessibilityCustomActions = []
clauses = []
accessibilityTraits = .staticText
numberOfLines = 0
}
public func needsToBeConstrained() -> Bool { true }
public func horizontalAlignment() -> UIStackView.Alignment { .leading }
@ -817,19 +459,25 @@ extension Label {
// MARK: - Multi-Link Functionality
extension Label {
/// Applied to existing text. Removes underlines of tappable links and assoated actionable clauses.
@objc public func clearActionableClauses() {
guard let attributedText = attributedText else { return }
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
clauses.forEach { clause in
mutableAttributedString.removeAttribute(NSAttributedString.Key.underlineStyle, range: clause.range)
/// Underlines the tappable region and stores the tap logic for interation.
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
var textLink = ActionLabelAttribute(location: range.location, length: range.length)
textLink.subscriber = textLink
.action
.sink { _ in
actionBlock()
}
if var attributes {
attributes.append(textLink)
setNeedsUpdate()
} else {
attributes = [textLink]
}
self.attributedText = mutableAttributedString
accessibilityElements = []
clauses = []
}
@objc public func clearActionableClauses() {
attributes = attributes?.filter { !($0 is (any ActionLabelAttributeModel)) }
}
public func createActionBlockFor(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock? {
@ -842,24 +490,6 @@ extension Label {
}
}
private func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) {
guard let string = string,
let range = validateAttribute(range: range, in: string)
else { return }
string.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range)
}
fileprivate func setActionAttributes(range: NSRange) {
guard let attributedText = attributedText else { return }
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedText)
addActionAttributes(range: range, string: mutableAttributedString)
self.attributedText = mutableAttributedString
}
/**
Provides an actionable range of text.
@ -898,113 +528,6 @@ extension Label {
setTextLinkState(range: getRange, actionBlock: actionBlock)
}
/// Underlines the tappable region and stores the tap logic for interation.
private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) {
setActionAttributes(range: range)
appendActionableClause(range: range, actionBlock: actionBlock)
}
@objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) {
for clause in clauses {
// This determines if we tapped on the desired range of text.
if gesture.didTapAttributedTextInLabel(self, inRange: clause.range) {
clause.performAction()
return
}
}
}
}
// MARK: -
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(_ label: Label, inRange targetRange: NSRange) -> Bool {
// There would only ever be one clause to act on.
if label.makeWholeViewClickable {
return true
}
guard let abstractContainer = label.abstractTextContainer() else { return false }
let textContainer = abstractContainer.0
let layoutManager = abstractContainer.1
let tapLocation = location(in: label)
let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer)
let intrinsicWidth = label.intrinsicContentSize.width
// Assert that tapped occured within acceptable bounds based on alignment.
switch label.textAlignment {
case .right:
if tapLocation.x < label.bounds.width - intrinsicWidth {
return false
}
case .center:
let halfBounds = label.bounds.width / 2
let halfIntrinsicWidth = intrinsicWidth / 2
if tapLocation.x > halfBounds + halfIntrinsicWidth {
return false
} else if tapLocation.x < halfBounds - halfIntrinsicWidth {
return false
}
default: // Left align
if tapLocation.x > intrinsicWidth {
return false
}
}
// Affirms that the tap occured in the desired rect of provided by the target range.
return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange)
}
}
// MARK: - Accessibility
extension Label {
func customAccessibilityAction(range: NSRange) -> UIAccessibilityCustomAction? {
guard let text = text else { return nil }
if accessibilityHint == nil {
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
}
let actionText = NSString(string: text).substring(with: range)
let accessibleAction = UIAccessibilityCustomAction(name: actionText, target: self, selector: #selector(accessibilityCustomAction(_:)))
accessibilityCustomActions?.append(accessibleAction)
return accessibleAction
}
@objc public func accessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
for clause in clauses {
if action.hash == clause.accessibilityID {
clause.performAction()
return
}
}
}
open override func accessibilityActivate() -> Bool {
guard let accessibleActions = accessibilityCustomActions else { return false }
for clause in clauses {
for action in accessibleActions {
if action.hash == clause.accessibilityID {
clause.performAction()
return true
}
}
}
return false
}
}
//------------------------------------------------------
@ -1025,3 +548,4 @@ func validateAttribute(range: NSRange, in string: NSAttributedString, type: Stri
return range
}

View File

@ -5,7 +5,7 @@
// Created by Suresh, Kamlesh on 10/3/19.
// Copyright © 2019 Suresh, Kamlesh. All rights reserved.
//
import VDS
@objcMembers open class LabelModel: MoleculeModelProtocol {
//--------------------------------------------------
@ -30,6 +30,7 @@
public var numberOfLines: Int?
public var shouldMaskRecordedView: Bool? = false
public var accessibilityTraits: UIAccessibilityTraits?
public var inverted: Bool = false
//--------------------------------------------------
// MARK: - Keys
@ -49,6 +50,7 @@
case attributes
case html
case hero
case inverted
case makeWholeViewClickable
case numberOfLines
case shouldMaskRecordedView
@ -97,6 +99,7 @@
attributes = try typeContainer.decodeModelsIfPresent(codingKey: .attributes)
html = try typeContainer.decodeIfPresent(String.self, forKey: .html)
hero = try typeContainer.decodeIfPresent(Int.self, forKey: .hero)
inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) ?? false
makeWholeViewClickable = try typeContainer.decodeIfPresent(Bool.self, forKey: .makeWholeViewClickable)
numberOfLines = try typeContainer.decodeIfPresent(Int.self, forKey: .numberOfLines)
shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? false
@ -123,9 +126,14 @@
try container.encodeModelsIfPresent(attributes, forKey: .attributes)
try container.encodeIfPresent(html, forKey: .html)
try container.encodeIfPresent(hero, forKey: .hero)
try container.encodeIfPresent(inverted, forKey: .inverted)
try container.encodeIfPresent(makeWholeViewClickable, forKey: .makeWholeViewClickable)
try container.encodeIfPresent(numberOfLines, forKey: .numberOfLines)
try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView)
try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits)
}
}
extension LabelModel {
public var surface: Surface { inverted ? .dark : .light }
}

View File

@ -68,23 +68,35 @@ open class TileletModel: MoleculeModelProtocol {
public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? {
guard let title else { return nil }
let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
let style: TextStyle? = title.fontStyle?.vdsTextStyle()
if let style, let standardStyle = Tilelet.TitleModel.StandardStyle(rawValue: style.toStandardStyle().rawValue) {
return .init(text: title.text, textAttributes: attrs, standardStyle: standardStyle)
} else {
return .init(text: title.text, textAttributes: attrs)
}
}
do {
if let style = title.fontStyle {
return .init(text: title.text,
textAttributes: attrs,
standardStyle: try style.vdsSubsetStyle())
}
} catch MVMCoreError.errorObject(let object) {
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
} catch { }
return .init(text: title.text, textAttributes: attrs)
}
public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.SubTitleModel? {
guard let subTitle else { return nil }
let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
let style: TextStyle? = subTitle.fontStyle?.vdsTextStyle()
if let style, let standardStyle = Tilelet.SubTitleModel.StandardStyle(rawValue: style.toStandardStyle().rawValue) {
return .init(text: subTitle.text, textAttributes: attrs, standardStyle: standardStyle)
} else {
return .init(text: subTitle.text, textAttributes: attrs)
}
do {
if let style = subTitle.fontStyle {
return .init(text: subTitle.text,
otherStandardStyle: try style.vdsSubsetStyle(),
textAttributes: attrs)
}
} catch MVMCoreError.errorObject(let object) {
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
} catch { }
return .init(text: subTitle.text, textAttributes: attrs)
}
public func encode(to encoder: Encoder) throws {

View File

@ -0,0 +1,63 @@
//
// ReadableDecodingErrors.swift
// MVMCore
//
// Created by Kyle Hedden on 10/5/23.
// Copyright © 2023 myverizon. All rights reserved.
//
import Foundation
protocol HumanReadableDecodingErrorProtocol {
var readableDescription: String { get }
}
extension JSONError: HumanReadableDecodingErrorProtocol {
var readableDescription: String {
switch (self) {
case .other(let other):
if let other = other as? HumanReadableDecodingErrorProtocol {
return other.readableDescription
}
return description
default:
return description
}
}
}
extension ModelRegistry.Error: HumanReadableDecodingErrorProtocol {
var readableDescription: String {
switch (self) {
case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil:
return "Model identifier \"\(identifier!)\" is not mapped for \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })"
case .decoderErrorObjectNotPresent(let codingKey, let codingPath):
return "Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })"
default:
return "Registry error: \((self as NSError).localizedFailureReason ?? self.localizedDescription)"
}
}
}
extension DecodingError: HumanReadableDecodingErrorProtocol {
var readableDescription: String {
switch (self) {
case .keyNotFound(let codingKey, let context):
return "Required key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })"
case .valueNotFound(_, let context):
return "Value not found @ \(context.codingPath.map { return $0.stringValue })"
case .typeMismatch(_, let context):
return "Value type mismatch @ \(context.codingPath.map { return $0.stringValue })"
case .dataCorrupted(let context):
return "Data corrupted @ \(context.codingPath.map { return $0.stringValue })"
@unknown default:
return (self as NSError).localizedFailureReason ?? self.localizedDescription
}
}
}

View File

@ -25,9 +25,8 @@ extension VDS.Tabs.Overflow: Codable {}
extension VDS.Tabs.Size: Codable {}
extension VDS.TextLink.Size: Codable {}
extension VDS.TextLinkCaret.IconPosition: Codable {}
extension VDS.TileContainer.BackgroundColor: Codable {}
extension VDS.TileContainer.Padding: Codable {}
extension VDS.TileContainer.AspectRatio: Codable {}
extension VDS.TitleLockup.TextAlignment: Codable {}
extension VDS.Tooltip.FillColor: Codable {}
extension VDS.Tooltip.Size: Codable {}
extension VDS.Line.Style: Codable {}
@ -61,3 +60,75 @@ extension DecodableDefault {
public typealias BlackColor = Wrapper<Sources.BlackColor>
public typealias Surface = Wrapper<Sources.Surface>
}
extension VDS.TileContainer.BackgroundColor: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .custom(let value):
try container.encode(value)
default:
try container.encode(String(reflecting: self))
}
}
// Init from decoder to handle the decoding based on the type
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let type = try container.decode(String.self)
switch type {
case "primary":
self = .primary
case "secondary":
self = .secondary
case "white":
self = .white
case "black":
self = .black
default:
self = .custom(type)
}
}
}
extension VDS.TileContainer.Padding: Codable {
enum PaddingError: Error {
case valueNotFound(type: String)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .custom(let value):
try container.encode(value)
default:
try container.encode(String(reflecting: self))
}
}
// Init from decoder to handle the decoding based on the type
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let type = try container.decode(String.self)
switch type {
case "padding2X":
self = .padding2X
case "padding4X":
self = .padding4X
case "padding6X":
self = .padding6X
case "padding8X":
self = .padding8X
case "padding12X":
self = .padding12X
default:
throw PaddingError.valueNotFound(type: type)
}
} catch PaddingError.valueNotFound(let type) {
throw PaddingError.valueNotFound(type: type)
} catch {
let type = try container.decode(CGFloat.self)
self = .custom(type)
}
}
}

View File

@ -12,20 +12,54 @@ import VDS
extension Styler.Font {
//Converts Legacy Font to a VDS.TextStyle
public func vdsTextStyle() -> VDS.TextStyle? {
let updatedRaw = rawValue.replacingOccurrences(of: "Regular", with: "")
//ensure that the current Styler.Font isn't Legacy Version
//if it is, here is the conversion to the updated version
var actualFont: Styler.Font = self
switch self {
case .Title2XLarge: actualFont = .RegularTitle2XLarge
case .TitleXLarge: actualFont = .RegularTitleXLarge
case .H1: actualFont = .RegularTitle2XLarge
case .H32: actualFont = .RegularTitleXLarge
case .H2: actualFont = .RegularTitleLarge
case .B20: actualFont = .RegularBodyLarge
case .H3: actualFont = .BoldTitleMedium
case .B1: actualFont = .BoldBodySmall
case .B2: actualFont = .RegularBodySmall
case .B3: actualFont = .RegularMicro
default: break
}
let updatedRaw = actualFont.rawValue.replacingOccurrences(of: "Regular", with: "")
let newRaw = updatedRaw.prefix(1).lowercased() + updatedRaw.dropFirst()
guard let style = VDS.TextStyle(rawValue: newRaw) else { return nil }
return style
}
public func vdsSubsetStyle<T: EnumSubset>() -> T? {
guard let style = vdsTextStyle() else { return nil }
guard let rawValue = style.rawValue as? T.RawValue,
let found = T(rawValue: rawValue) else {
print("Style: \(style.rawValue) is not in enum \(T.self)\ronly these cases exist:\r\(T.allCases)")
return nil
public func vdsSubsetStyle<T: EnumSubset>() throws -> T {
guard let style = vdsTextStyle(), let rawValue = style.toStandardStyle().rawValue as? T.RawValue, let standardStyle = T(rawValue: rawValue) else {
let err = "\(rawValue) was not found in the \(T.self), only these cases exist:\r\(T.allCases)"
throw MVMCoreError.errorObject(MVMCoreErrorObject(title: "\(T.self) conversion Issue",
messageToLog: err,
code: 999,
domain: ErrorDomainNative,
location: #file)!)
}
return found
return standardStyle
}
}
extension VDS.Font {
internal static func from(fontName: String) -> Self? {
Self.allCases.filter({$0.fontName == fontName }).first
}
}
extension VDS.TextStyle {
internal static func style(from font: VDS.Font, pointSize: CGFloat) -> TextStyle? {
guard let first = allCases.filter({$0.fontFace == font && $0.pointSize == pointSize}).first else {
return TextStyle(rawValue: "Custom-TextStyle", fontFace: font, pointSize: pointSize)
}
return first
}
}

View File

@ -20,6 +20,11 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMol
[headlineBody, buttons]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &buttons, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -24,6 +24,15 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol
[headline, headline2, subHeadline, body, link, buttons]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &headline2, with: molecule)
|| replaceChildMolecule(at: &subHeadline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
|| replaceChildMolecule(at: &link, with: molecule)
|| replaceChildMolecule(at: &buttons, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -20,6 +20,10 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
[headlineBody]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -22,6 +22,11 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMo
[headlineBody, buttons]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &buttons, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -19,6 +19,11 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, Parent
[headlineBody, caretLink]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &caretLink, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -21,6 +21,11 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMolec
[headlineBody, link]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &link, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -21,6 +21,10 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol
[headlineBody]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -25,6 +25,17 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, P
[headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0})
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
|| replaceChildMolecule(at: &subBody, with: molecule)
|| replaceChildMolecule(at: &body2, with: molecule)
|| replaceChildMolecule(at: &body2, with: molecule)
|| replaceChildMolecule(at: &subBody2, with: molecule)
|| replaceChildMolecule(at: &body3, with: molecule)
|| replaceChildMolecule(at: &subBody3, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -22,6 +22,11 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, Paren
[headlineBody, button]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &button, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -20,6 +20,11 @@ open class ListLeftVariableCheckboxBodyTextModel: ListItemModel, MoleculeModelPr
[checkbox, headlineBody]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &checkbox, with: molecule)
|| replaceChildMolecule(at: &headlineBody, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -20,6 +20,11 @@ public class ListLeftVariableIconAllTextLinksModel: ListItemModel, MoleculeModel
return [image, eyebrowHeadlineBodyLink]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &image, with: molecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule)
}
//--------------------------------------------------
// MARK: - Method
//--------------------------------------------------

View File

@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretAllTextLinksModel: ListItemModel,
return [image, eyebrowHeadlineBodyLink, rightLabel]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &image, with: molecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule)
}
//-----------------------------------------------------
// MARK: - Methods
//-----------------------------------------------------

View File

@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretBodyTextModel: ListItemModel, Par
[image, headlineBody, rightLabel]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &image, with: molecule)
|| replaceChildMolecule(at: &headlineBody, with: molecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule)
}
//-----------------------------------------------------
// MARK: - Methods
//-----------------------------------------------------

View File

@ -21,6 +21,12 @@ public class ListLeftVariableIconWithRightCaretModel: ListItemModel, ParentMolec
return [image, leftLabel, rightLabel]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &image, with: molecule)
|| replaceChildMolecule(at: &leftLabel, with: molecule)
|| replaceChildMolecule(at: &rightLabel, with: molecule)
}
//-----------------------------------------------------
// MARK: - Methods
//-----------------------------------------------------

View File

@ -7,7 +7,7 @@
//
open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, MoleculeModelProtocol {
open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, ParentMoleculeModelProtocol {
//-----------------------------------------------------
// MARK: - Properties
//-----------------------------------------------------
@ -15,7 +15,16 @@ open class ListLeftVariableRadioButtonBodyTextModel: ListItemModel, MoleculeMode
public static var identifier: String = "listLVRBBdy"
public var radioButton: RadioButtonModel
public var headlineBody: HeadlineBodyModel
public var children: [MoleculeModelProtocol] {
[radioButton, headlineBody]
}
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &radioButton, with: replacementMolecule)
|| replaceChildMolecule(at: &headlineBody, with: replacementMolecule)
}
//-----------------------------------------------------
// MARK: - Initializer
//-----------------------------------------------------

View File

@ -39,6 +39,10 @@ public class ListOneColumnFullWidthTextBodyTextModel: ListItemModel, MoleculeMod
return [headlineBody]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headlineBody, with: molecule)
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------

View File

@ -40,6 +40,11 @@ public class ListRightVariableButtonAllTextAndLinksModel: ListItemModel, Molecul
return [button, eyebrowHeadlineBodyLink]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &button, with: molecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule)
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------

View File

@ -20,6 +20,11 @@ public class ListRightVariableRightCaretAllTextAndLinksModel: ListItemModel, Par
[rightLabel, eyebrowHeadlineBodyLink]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &rightLabel, with: molecule)
|| replaceChildMolecule(at: &eyebrowHeadlineBodyLink, with: molecule)
}
//-----------------------------------------------------
// MARK: - Methods
//-----------------------------------------------------

View File

@ -12,7 +12,7 @@ import Foundation
//--------------------------------------------------
// MARK: - Outlets
//--------------------------------------------------
public let planLabel = Label()
public let planLabel = Label(fontStyle: .BoldFeatureXLarge)
public let headline = Label(fontStyle: .BoldTitleLarge)
public let subHeadline = Label(fontStyle: .RegularTitleLarge)
public let body = Label(fontStyle: .RegularBodySmall)
@ -33,8 +33,6 @@ import Foundation
//-------------------------------------------------------
open override func setupView() {
super.setupView()
planLabel.font = MFStyler.getMVA3FontSize(96, bold: true)
planLabel.standardFontSize = 96
addSubview(stack)
planLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
planLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
@ -94,8 +92,7 @@ import Foundation
open override func reset() {
super.reset()
stack.reset()
planLabel.font = MFStyler.getMVA3FontSize(96, bold: true)
planLabel.standardFontSize = 96
planLabel.setFontStyle(.BoldFeatureXLarge)
headline.setFontStyle(.BoldTitleLarge)
subHeadline.setFontStyle(.RegularTitleLarge)
body.setFontStyle(.RegularBodySmall)

View File

@ -5,119 +5,43 @@
// Created by Nadigadda, Sumanth on 04/05/22.
// Copyright © 2022 Verizon Wireless. All rights reserved.
//
import VDS
@objcMembers open class TitleLockup: VDS.TitleLockup, VDSMoleculeViewProtocol {
@objcMembers open class TitleLockup: View {
//--------------------------------------------------
// MARK: - Outlets
// MARK: - Public Properties
//--------------------------------------------------
open var viewModel: TitleLockupModel!
open var delegateObject: MVMCoreUIDelegateObject?
open var additionalData: [AnyHashable : Any]?
public let eyebrow = Label(fontStyle: .RegularBodySmall)
public let title = Label(fontStyle: .RegularBodySmall)
public let subTitle = Label(fontStyle: .RegularBodySmall)
public lazy var stack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [eyebrow, title, subTitle])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
return stack
}()
var castModel: TitleLockupModel? {
get { return model as? TitleLockupModel }
//--------------------------------------------------
// MARK: - Public Functions
//--------------------------------------------------
open func viewModelDidUpdate() {
surface = viewModel.surface
textAlignment = viewModel.textAlignment
eyebrowModel = viewModel.eyebrowModel(delegateObject: delegateObject, additionalData: additionalData)
titleModel = viewModel.titleModel(delegateObject: delegateObject, additionalData: additionalData)
subTitleModel = viewModel.subTitleModel(delegateObject: delegateObject, additionalData: additionalData)
}
//--------------------------------------------------
// MARK: - Initialization
//--------------------------------------------------
public convenience init() {
public convenience required init() {
self.init(frame: .zero)
}
//--------------------------------------------------
// MARK: - MFViewProtocol
//--------------------------------------------------
open override func setupView() {
super.setupView()
addSubview(stack)
NSLayoutConstraint.constraintPinSubview(toSuperview: stack)
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
stack.updateView(size)
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open override func reset() {
super.reset()
stack.reset()
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? TitleLockupModel else { return }
stack.setCustomSpacing(model.defaultEyebrowTitleSpacing(), after: eyebrow)
stack.setCustomSpacing(model.defaultTitleSubTitleSpacing(), after: title)
stack.updateContainedMolecules(with: [model.eyebrow,
model.title,
model.subTitle],
delegateObject, additionalData)
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
return 65
}
//--------------------------------------------------
// MARK: - Accessibility Helpers
//--------------------------------------------------
/// Returns the labels text in one message.
func getAccessibilityMessage() -> String? {
var message = ""
if let eyebrowLabel = eyebrow.text {
message += eyebrowLabel + ", "
}
if let headlineLabel = title.text {
message += headlineLabel + ", "
}
if let bodyLabel = subTitle.text {
message += bodyLabel
}
return message.count > 0 ? message : nil
}
/// Returns an array of the appropriate accessibility elements.
func getAccessibilityElements() -> [Any]? {
var elements: [UIView] = []
if eyebrow.hasText {
elements.append(eyebrow)
}
if title.hasText {
elements.append(title)
}
if subTitle.hasText {
elements.append(subTitle)
}
return elements.count > 0 ? elements : nil
}
open func updateView(_ size: CGFloat) {}
}

View File

@ -7,6 +7,7 @@
//
import VDSColorTokens
import VDS
public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtocol {
@ -18,52 +19,27 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco
public var moleculeName: String = TitleLockupModel.identifier
public var id: String = UUID().uuidString
public var textAlignment: TitleLockup.TextAlignment = .left
public var eyebrow: LabelModel?
public var title: LabelModel
public var subTitle: LabelModel?
public var subTitleColor: Use = .primary
public var alignment: Alignment = .left {
didSet {
///Updating the text alignment for all labels
if let textAlignment = NSTextAlignment(rawValue: alignment.rawValue) {
eyebrow?.textAlignment = textAlignment
title.textAlignment = textAlignment
subTitle?.textAlignment = textAlignment
}
}
}
public var alignment: VDS.TitleLockup.TextAlignment = .left
public var inverted: Bool = false
public var inverted: Bool = false {
didSet {
///Updating the text color
eyebrow?.textColor = titleColor
title.textColor = titleColor
subTitle?.textColor = subTitleColor
}
}
private var _backgroundColor: Color?
public var backgroundColor: Color? {
get {
return inverted ? Color(uiColor: VDSColor.backgroundPrimaryDark) : Color(uiColor: VDSColor.backgroundPrimaryLight)
}
set {
_backgroundColor = newValue
}
}
public var titleColor: Color? {
return inverted ? Color(uiColor: VDSColor.elementsPrimaryOndark) : Color(uiColor: VDSColor.elementsPrimaryOnlight)
}
public var subTitleColor: Color? {
return inverted ? Color(uiColor: VDSColor.elementsSecondaryOndark) : Color(uiColor: VDSColor.elementsSecondaryOnlight)
}
public var backgroundColor: Color?
public var children: [MoleculeModelProtocol] {
[eyebrow, title, subTitle].compactMap { (molecule: MoleculeModelProtocol?) in molecule }
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &eyebrow, with: molecule)
|| replaceChildMolecule(at: &title, with: molecule)
|| replaceChildMolecule(at: &subTitle, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
@ -72,58 +48,6 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco
self.eyebrow = eyebrow
self.title = title
self.subTitle = subTitle
updateLabelAttributes()
}
//--------------------------------------------------
// MARK: - Enum
//--------------------------------------------------
public enum Alignment: String, Codable {
case left
case center
}
//--------------------------------------------------
// MARK: - Styling
//--------------------------------------------------
/// Returns the default fontStyle for the subtitle, based on the title fontStyle.
func defaultSubtitleFontStyle() -> Styler.Font {
switch title.fontStyle {
case .RegularTitleXLarge, .RegularTitle2XLarge, .RegularFeatureXSmall:
return .RegularBodyLarge
case .RegularFeatureSmall, .RegularFeatureMedium:
return .RegularTitleLarge
default:
return .RegularBodySmall
}
}
/// Returns the default spacing between the eyebrow and title, based on the title fontStyle.
func defaultEyebrowTitleSpacing() -> CGFloat {
switch title.fontStyle {
case .RegularTitleXLarge, .RegularTitle2XLarge, .RegularFeatureXSmall, .RegularFeatureSmall:
return Padding.Three
case .RegularFeatureMedium:
return subTitle?.fontStyle == .RegularBodyLarge ? Padding.Three : Padding.Four
default:
return Padding.Two
}
}
/// Returns the default spacing between the title and subTitle, based on the title fontStyle.
func defaultTitleSubTitleSpacing() -> CGFloat {
switch title.fontStyle {
case .RegularTitleXLarge:
return Padding.Three
case .RegularTitle2XLarge, .RegularFeatureXSmall, .RegularFeatureSmall:
return Padding.Four
case .RegularFeatureMedium:
return Padding.Five
default:
return Padding.Two
}
}
//--------------------------------------------------
@ -133,10 +57,11 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco
private enum CodingKeys: String, CodingKey {
case id
case moleculeName
case backgroundColor
case textAlignment
case eyebrow
case title
case subTitle
case subTitleColor
case inverted
case alignment
}
@ -148,72 +73,122 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
textAlignment = try typeContainer.decodeIfPresent(TitleLockup.TextAlignment.self, forKey: .textAlignment) ?? .left
title = try typeContainer.decodeMolecule(codingKey: .title)
eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow)
subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle)
/// look for color hex code
if let color = try? typeContainer.decodeIfPresent(Color.self, forKey: .subTitleColor) {
self.subTitleColor = color.uiColor.isDark() ? .primary : .secondary
if let newAlignment = try typeContainer.decodeIfPresent(Alignment.self, forKey: .alignment) {
} else if let subTitleColor = try? typeContainer.decodeIfPresent(Use.self, forKey: .subTitleColor) {
self.subTitleColor = subTitleColor
} else {
subTitleColor = .primary
}
if let newAlignment = try typeContainer.decodeIfPresent(VDS.TitleLockup.TextAlignment.self, forKey: .alignment) {
alignment = newAlignment
}
if let invertedStatus = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
inverted = invertedStatus
if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) {
self.inverted = inverted
} else {
try setInverted(deprecatedFrom: decoder)
}
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
updateLabelAttributes()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(textAlignment, forKey: .textAlignment)
try container.encodeIfPresent(eyebrow, forKey: .eyebrow)
try container.encodeModel(title, forKey: .title)
try container.encodeIfPresent(subTitle, forKey: .subTitle)
try container.encode(subTitleColor, forKey: .subTitleColor)
try container.encode(alignment, forKey: .alignment)
try container.encode(inverted, forKey: .inverted)
try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor)
}
//--------------------------------------------------
// MARK: - Model updates
//--------------------------------------------------
public func eyebrowModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.EyebrowModel? {
guard let eyebrow else { return nil }
let attrs = eyebrow.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
do {
if let style = eyebrow.fontStyle {
return .init(text: eyebrow.text,
isBold: style.isBold(),
standardStyle: try style.vdsSubsetStyle(),
textAttributes: attrs,
numberOfLines: eyebrow.numberOfLines ?? 0)
}
} catch MVMCoreError.errorObject(let object) {
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
} catch { }
return .init(text: eyebrow.text, textAttributes: attrs, numberOfLines: eyebrow.numberOfLines ?? 0)
}
public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.TitleModel {
let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
do {
if let style = title.fontStyle {
return .init(text: title.text,
textAttributes: attrs,
isBold: style.isBold(),
standardStyle: try style.vdsSubsetStyle(),
numberOfLines: title.numberOfLines ?? 0)
}
private func updateLabelAttributes() {
// If subtitle style is not available, will set font style based on the component
if subTitle?.fontStyle == nil {
subTitle?.fontStyle = defaultSubtitleFontStyle()
}
} catch MVMCoreError.errorObject(let object) {
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
} catch { }
// If eyebrow style is not available, will set font style based on the component. Eyebrow and subtitle share the same font size
if eyebrow?.fontStyle == nil {
eyebrow?.fontStyle = subTitle?.fontStyle
}
return .init(text: title.text, textAttributes: attrs, numberOfLines: title.numberOfLines ?? 0)
}
public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> VDS.TitleLockup.SubTitleModel? {
guard let subTitle else { return nil }
let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData)
// Updating the text color
if eyebrow?.textColor == nil {
eyebrow?.textColor = subTitleColor
}
if title.textColor == nil {
title.textColor = titleColor
}
if subTitle?.textColor == nil {
subTitle?.textColor = subTitleColor
}
// Updating the text alignment for all labels
if let textAlignment = NSTextAlignment(rawValue: alignment.rawValue) {
if eyebrow?.textAlignment == nil {
eyebrow?.textAlignment = textAlignment
}
if title.textAlignment == nil {
title.textAlignment = textAlignment
}
if subTitle?.textAlignment == nil {
subTitle?.textAlignment = textAlignment
do {
if let style = subTitle.fontStyle {
return .init(text: subTitle.text,
otherStandardStyle: try style.vdsSubsetStyle(),
textColor: subTitleColor,
textAttributes: attrs,
numberOfLines: subTitle.numberOfLines ?? 0)
}
} catch MVMCoreError.errorObject(let object) {
MVMCoreLoggingHandler.shared()?.addError(toLog: object)
} catch { }
return .init(text: subTitle.text, textColor: subTitleColor, textAttributes: attrs, numberOfLines: subTitle.numberOfLines ?? 0)
}
private enum DeprecatedCodingKeys: String, CodingKey {
case titleColor
case backgroundColor
}
private func setInverted(deprecatedFrom decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: DeprecatedCodingKeys.self)
if let titleColor = try typeContainer.decodeIfPresent(Color.self, forKey: .titleColor) {
inverted = !titleColor.uiColor.isDark()
}
if let backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
inverted = !backgroundColor.uiColor.isDark()
}
}
}
extension TitleLockupModel {
public var surface: Surface { inverted ? .dark : .light }
}

View File

@ -22,6 +22,11 @@ public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, Mo
[headline, body].compactMap({$0})
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -22,6 +22,11 @@ public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, Mo
[headline, body].compactMap({$0})
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -22,6 +22,11 @@ public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, Mol
[headline, body].compactMap({$0})
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -30,6 +30,7 @@ import VDS
if let delegate {
onTabDidSelect = { [weak self] index in
guard let self else { return }
viewModel.selectedIndex = index
delegate.didSelectItem(.init(row: index, section: 0), tabs: self)
}

View File

@ -23,6 +23,11 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol {
public var children: [MoleculeModelProtocol] { [primaryButton, secondaryButton].compactMap { $0 } }
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &primaryButton, with: molecule)
|| replaceChildMolecule(at: &secondaryButton, with: molecule)
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------

View File

@ -6,7 +6,7 @@
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
// A base class that has common list item boilerplate model stuffs.
import MVMCore
@objcMembers open class ListItemModel: ContainerModel, ListItemModelProtocol {
//--------------------------------------------------
@ -18,6 +18,7 @@
public var hideArrow: Bool?
public var line: LineModel?
public var style: ListItemStyle?
public var gone: Bool = false
public var accessibilityTraits: UIAccessibilityTraits?
public var accessibilityValue: String?
public var accessibilityText: String?
@ -31,6 +32,7 @@
case hideArrow
case line
case style
case gone
case accessibilityTraits
case accessibilityValue
case accessibilityText
@ -107,6 +109,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)
gone = try typeContainer.decodeIfPresent(Bool.self, forKey: .gone) ?? false
accessibilityTraits = try typeContainer.decodeIfPresent(UIAccessibilityTraits.self, forKey: .accessibilityTraits)
accessibilityValue = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityValue)
try super.init(from: decoder)
@ -120,6 +123,7 @@
try container.encodeIfPresent(hideArrow, forKey: .hideArrow)
try container.encodeIfPresent(line, forKey: .line)
try container.encodeIfPresent(style, forKey: .style)
try container.encode(gone, forKey: .gone)
try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits)
try container.encodeIfPresent(accessibilityValue, forKey: .accessibilityValue)
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)

View File

@ -9,7 +9,7 @@
import UIKit
public class TabsListItemModel: ListItemModel, MoleculeModelProtocol {
public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
@ -19,6 +19,23 @@ public class TabsListItemModel: ListItemModel, MoleculeModelProtocol {
var molecules: [[ListItemModelProtocol & MoleculeModelProtocol]]
private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]?
public var children: [MoleculeModelProtocol] {
return molecules.flatMap { $0 }
}
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
guard let replacementMolecule = replacementMolecule as? ListItemModelProtocol & MoleculeModelProtocol else { return false }
for (tabIndex, _) in molecules.enumerated() {
for (elementIndex, _) in molecules[tabIndex].enumerated() {
if molecules[tabIndex][elementIndex].id == replacementMolecule.id {
molecules[tabIndex][elementIndex] = replacementMolecule
return true
}
}
}
return false
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------

View File

@ -76,8 +76,8 @@ import UIKit
private func setDefaultState() {
headlineBodyButton.headlineBody.headlineLabel.font = MFStyler.fontBoldTitleMedium()
headlineBodyButton.headlineBody.messageLabel.font = MFStyler.fontRegularMicro()
headlineBodyButton.headlineBody.headlineLabel.setFontStyle(.BoldTitleMedium)
headlineBodyButton.headlineBody.messageLabel.setFontStyle(.RegularMicro)
imageLoader.imageView.contentMode = .scaleAspectFit
imageLoader.addSizeConstraintsForAspectRatio = true
buttonHeaderPadding = PaddingTwo

View File

@ -18,10 +18,19 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol {
public var bottomLeftLabel: LabelModel?
public var bottomRightLabel: LabelModel?
public var molecule: MoleculeModelProtocol?
public var children: [MoleculeModelProtocol] {
[molecule, topLeftLabel, topRightLabel, bottomLeftLabel, bottomRightLabel].compactMap { $0 }
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &self.molecule, with: molecule)
|| replaceChildMolecule(at: &topLeftLabel, with: molecule)
|| replaceChildMolecule(at: &topRightLabel, with: molecule)
|| replaceChildMolecule(at: &bottomLeftLabel, with: molecule)
|| replaceChildMolecule(at: &bottomRightLabel, with: molecule)
}
public init(with molecule: MoleculeModelProtocol?) {
self.molecule = molecule
}

View File

@ -14,7 +14,7 @@ import UIKit
// MARK: - Properties
//--------------------------------------------------
public let label = Label(fontStyle: .BoldBodySmall)
public let label = Label().with { $0.font = Styler.Font.BoldFeatureXLarge.getFont() }
public let toggle = Toggle()
//--------------------------------------------------

View File

@ -23,7 +23,7 @@ open class ModuleMolecule: Container {
super.set(with: model, delegateObject, additionalData)
guard let moduleMoleculeModel = model as? ModuleMoleculeModel,
let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMoleculeModel.moduleName) else {
let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMoleculeModel.moduleName) else {
// Critical error
return
}
@ -49,7 +49,7 @@ open class ModuleMolecule: Container {
public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
guard let moduleMolecule = model as? ModuleMoleculeModel,
let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName),
let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName),
let classType = ModelRegistry.getMoleculeClass(moduleModel),
let height = classType.estimatedHeight(with: moduleModel, delegateObject) else {
// Critical error
@ -60,7 +60,7 @@ open class ModuleMolecule: Container {
public override class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
guard let moduleMolecule = model as? ModuleMoleculeModel,
let moduleModel = delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName),
let moduleModel = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleMolecule.moduleName),
let classType = ModelRegistry.getMoleculeClass(moduleModel),
let name = classType.nameForReuse(with: moduleModel, delegateObject) else {
// Critical error
@ -72,7 +72,7 @@ open class ModuleMolecule: Container {
public override class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
guard let moduleName = (model as? ModuleMoleculeModel)?.moduleName,
let _ = delegateObject?.moleculeDelegate?.getModuleWithName(moduleName) else {
let _ = try? delegateObject?.moleculeDelegate?.getModuleWithName(moduleName) else {
if let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess), code: CoreUIErrorCode.ErrorCodeModuleMolecule.rawValue, domain: ErrorDomainNative, location: String(describing: self)) {
error?.pointee = errorObject
MVMCoreUILoggingHandler.shared()?.addError(toLog: errorObject)

View File

@ -20,6 +20,10 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco
return [molecule]
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &self.molecule, with: molecule)
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------

View File

@ -12,9 +12,14 @@ public protocol MoleculeContainerModelProtocol: ContainerModelProtocol, ParentMo
}
public extension MoleculeContainerModelProtocol {
var children: [MoleculeModelProtocol] {
return [molecule]
}
}
public extension MoleculeContainerModelProtocol where Self: AnyObject {
mutating func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &molecule, with: replacementMolecule)
}
}

View File

@ -25,6 +25,13 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule
[eyebrow, headline, body, link].compactMap { (molecule: MoleculeModelProtocol?) in molecule }
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &eyebrow, with: molecule)
|| replaceChildMolecule(at: &headline, with: molecule)
|| replaceChildMolecule(at: &body, with: molecule)
|| replaceChildMolecule(at: &link, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -62,8 +62,8 @@
private func defaultState() {
headlineBody.headlineLabel.font = Styler.Font.BoldTitleMedium.getFont()
headlineBody.messageLabel.font = Styler.Font.RegularMicro.getFont()
headlineBody.headlineLabel.setFontStyle(.BoldTitleMedium)
headlineBody.messageLabel.setFontStyle(.RegularMicro)
button.use = .secondary
button.isHidden = false
buttonHeadlinePadding = PaddingTwo

View File

@ -24,13 +24,9 @@
[headline, body].compactMap { $0 }
}
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool {
return [
\HeadlineBodyModel.headline,
\HeadlineBodyModel.body,
].contains {
replaceChildMolecule(on: self, keyPath: $0, replacementMolecule: replacementMolecule)
}
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at:&headline, with: replacementMolecule)
|| replaceChildMolecule(at:&body, with: replacementMolecule)
}
//--------------------------------------------------

View File

@ -181,6 +181,10 @@ open class Carousel: View {
}
}
open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
return (model as? CarouselModel)?.height ?? 80
}
//--------------------------------------------------
// MARK: - JSON Setters
//--------------------------------------------------

View File

@ -179,4 +179,8 @@ extension CarouselModel {
return molecules
}
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(in: &molecules, with: molecule)
}
}

View File

@ -21,19 +21,6 @@
public var spacing: CGFloat = Padding.Four
public var useStackSpacingBeforeFirstItem = false
public var children: [MoleculeModelProtocol] {
return molecules
}
public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool {
guard let replacementMolecule = replacementMolecule as? StackItemModelProtocol & MoleculeModelProtocol else { return false }
guard let matchingIndex = molecules.firstIndex(where: { molecule in
molecule.id == replacementMolecule.id
}) else { return false }
molecules[matchingIndex] = replacementMolecule
return true
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------

View File

@ -16,7 +16,12 @@ public protocol StackModelProtocol: ParentMoleculeModelProtocol {
}
extension StackModelProtocol {
public var children: [MoleculeModelProtocol] { return molecules }
}
extension StackModelProtocol where Self: AnyObject {
public mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(in: &molecules, with: molecule)
}
}

View File

@ -20,6 +20,7 @@ public protocol ListItemModelProtocol: ContainerModelProtocol, AccessibilityMode
var action: ActionModelProtocol? { get set }
var hideArrow: Bool? { get set }
var style: ListItemStyle? { get set }
var gone: Bool { get set }
}
// Not a strict requirement.

View File

@ -68,8 +68,14 @@ public extension Array where Element == MoleculeModelProtocol {
}
func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool) -> Void) {
forEach { (molecule) in
molecule.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit)
var shouldStop = false
let stopIntercept = { depth, molecule, stop in
onVisit(depth, molecule, &shouldStop)
stop = shouldStop
}
for molecule in self {
molecule.depthFirstTraverse(options: options, depth: depth, onVisit: stopIntercept)
if shouldStop { break }
}
}
}

View File

@ -14,4 +14,6 @@ public protocol PageModelProtocol {
var backgroundColor: Color? { get set }
var navigationBar: (NavigationItemModelProtocol & MoleculeModelProtocol)? { get set }
var shouldMaskScreenWhileRecording: Bool? { get }
var hideLeftPanel: Bool? { get }
var hideRightPanel: Bool? { get }
}

View File

@ -8,11 +8,48 @@
import Foundation
public protocol ParentMoleculeModelProtocol: MoleculeModelProtocol, AnyObject {
public protocol ParentModelProtocol: MoleculeTreeTraversalProtocol {
/// Returns the direct children of this component. (Does not recurse.)
var children: [MoleculeModelProtocol] { get }
func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool
/// Method for replacing surface level children. (Does not recurse.)
mutating func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool
}
public extension ParentModelProtocol where Self: AnyObject {
/// Top level test to replace child molecules. Each parent molecule should attempt to replace.
func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool { return false }
/// Helper function for replacing a single molecules with type and optionality checks.
func replaceChildMolecule<T>(at childMolecule: inout T, with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
guard let childIdMolecule = childMolecule as? MoleculeModelProtocol else { return false }
if childIdMolecule.id == replacementMolecule.id {
guard let replacementMolecule = replacementMolecule as? T else {
throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))")
}
childMolecule = replacementMolecule
return true
}
return false
}
/// Helper for replacing a molecule in place within an array. Note the "in".
func replaceChildMolecule<T>(in molecules: inout [T], with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
guard let moleculeIdModels = molecules as? [MoleculeModelProtocol], let matchingIndex = moleculeIdModels.firstIndex(where: {
$0.id == replacementMolecule.id
}) else { return false }
guard let replacementMolecule = replacementMolecule as? T else {
throw MolecularError.error("Molecular replacement '\(replacementMolecule.id)' does not type match \(type(of: T.self)) of \(type(of: self))")
}
molecules[matchingIndex] = replacementMolecule
return true
}
}
public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelProtocol {
}
public extension ParentMoleculeModelProtocol {
@ -37,43 +74,27 @@ public extension ParentMoleculeModelProtocol {
}
func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool)->Void) {
var stop = false
var shouldStop = false
if (options == .parentFirst) {
onVisit(depth, self, &stop)
guard !stop else { return }
onVisit(depth, self, &shouldStop)
guard !shouldStop else { return }
}
let stopIntercept: (Int, MoleculeModelProtocol, inout Bool)->Void = { depth, model, stop in
onVisit(depth, model, &shouldStop)
stop = shouldStop
}
for child in children {
if let additionalParent = child as? ParentMoleculeModelProtocol {
// Safety net to make sure the ParentMoleculeModelProtocol's method extension is called over the base MoleculeModelProtocol.
additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit)
additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept)
} else {
child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit)
child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: stopIntercept)
}
guard !stop else { return }
guard !shouldStop else { return }
}
if (options == .childFirst) {
onVisit(depth, self, &stop)
onVisit(depth, self, &shouldStop)
}
// if options == .leafOnly don't call on self.
}
/// Top level test to replace child molecules. Each parent molecule should attempt to replace.
func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool { return false }
/// Helper function for replacing molecules on a path.
func replaceChildMolecule<P: ParentMoleculeModelProtocol, T: MoleculeModelProtocol>(on target: P, keyPath: ReferenceWritableKeyPath<P, T?>, replacementMolecule: MoleculeModelProtocol) -> Bool {
if let currentMolecule = target[keyPath: keyPath], currentMolecule.id == replacementMolecule.id, let newHeadline = replacementMolecule as? T {
target[keyPath: keyPath] = newHeadline
return true
}
return false
}
func replaceChildMolecule<P: ParentMoleculeModelProtocol, T: MoleculeModelProtocol>(on target: P, keyPath: ReferenceWritableKeyPath<P, T>, replacementMolecule: MoleculeModelProtocol) -> Bool {
if target[keyPath: keyPath].id == replacementMolecule.id, let newHeadline = replacementMolecule as? T {
target[keyPath: keyPath] = newHeadline
return true
}
return false
}
}

View File

@ -7,13 +7,17 @@
//
public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol {
public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol {
var template: String { get }
var rootMolecules: [MoleculeModelProtocol] { get }
}
public extension TemplateModelProtocol {
var children: [MoleculeModelProtocol] {
return rootMolecules
}
var template: String {
get { return Self.identifier }
}
@ -34,3 +38,31 @@ public extension TemplateModelProtocol {
return rootMolecules.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit)
}
}
extension TemplateModelProtocol {
/// Recursively finds and replaces the first child matching the replacement molecule id property.
mutating func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> Bool {
// Attempt root level replacement on the template model first.
if try replaceChildMolecule(with: replacementMolecule) {
return true
}
var didReplaceMolecule = false
var possibleError: Error?
// Dive into each root thereafter.
depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in
guard var parentMolecule = molecule as? ParentMoleculeModelProtocol else { return }
do {
didReplaceMolecule = try parentMolecule.replaceChildMolecule(with: replacementMolecule)
} catch {
possibleError = error
}
stop = didReplaceMolecule || possibleError != nil
}
if let error = possibleError {
throw error
}
return didReplaceMolecule
}
}

View File

@ -9,39 +9,49 @@
public protocol MoleculeDelegateProtocol: AnyObject {
func getTemplateModel() -> TemplateModelProtocol?
func getRootMolecules() -> [MoleculeModelProtocol]
/// returns a module for the corresponding module name.
/// Returns a raw module map for the corresponding module name if the key name exists.
func getModuleWithName(_ name: String?) -> [AnyHashable: Any]?
func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol?
/// Returns the decoded module for the corresponding module name if the key name exists. Throws if there is a decoding error.
func getModuleWithName(_ moleculeName: String) throws -> MoleculeModelProtocol?
/// Notifies the delegate that the molecule layout update. Should be called when the layout may change due to an async method. Mainly used for list or collections.
func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) //optional
func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol])
}
extension MoleculeDelegateProtocol {
public func getRootMolecules() -> [MoleculeModelProtocol] {
getTemplateModel()?.rootMolecules ?? []
}
public func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
public func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? {
public func getModuleWithName(_ moleculeName: String) throws -> MoleculeModelProtocol? {
let moduleJSON: [AnyHashable: Any]? = getModuleWithName(moleculeName)
guard let moduleJSON = moduleJSON as? [String: Any],
let moleculeName = moduleJSON.optionalStringForKey("moleculeName"),
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self)
else { return nil }
guard let moduleJSON = moduleJSON as? [String: Any] else { return nil }
do {
return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as? MoleculeModelProtocol
} catch {
MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "error: \(error)")
guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName),
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else {
throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName))
}
return nil
return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as? MoleculeModelProtocol
}
}
extension MoleculeDelegateProtocol where Self: TemplateProtocol {
public func getTemplateModel() -> TemplateModelProtocol? {
return templateModel
}
public func getRootMolecules() -> [MoleculeModelProtocol] {
templateModel?.rootMolecules ?? []
}

View File

@ -38,7 +38,7 @@ public extension MoleculeTreeTraversalProtocol {
func printMolecules(options: TreeTraversalOptions = .parentFirst) {
depthFirstTraverse(options: options, depth: 1) { depth, molecule, stop in
print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule)]")
print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule): \(molecule.id)]")
}
}
@ -51,10 +51,23 @@ public extension MoleculeTreeTraversalProtocol {
}
}
func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) {
depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in
guard let parentMolecule = molecule as? ParentMoleculeModelProtocol else { return }
stop = parentMolecule.replaceChildMolecule(with: replacementMolecule)
func filterMoleculeTree(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> [MoleculeModelProtocol] {
return reduceDepthFirstTraverse(options: options, depth: 0, initialResult: []) { (accumulator, molecule, depth) in
if condition(molecule) {
return accumulator + [molecule]
}
return accumulator
}
}
func findFirstMolecule(options: TreeTraversalOptions = .parentFirst, by condition: (MoleculeModelProtocol)->Bool) -> MoleculeModelProtocol? {
var foundMolecule: MoleculeModelProtocol?
depthFirstTraverse(options: options, depth: 0) { depth, molecule, isDone in
isDone = condition(molecule)
if isDone {
foundMolecule = molecule
}
}
return foundMolecule
}
}

View File

@ -28,25 +28,26 @@ public extension TemplateProtocol {
}
}
func decodeTemplate(using decoder: JSONDecoder, from data: Data) throws -> TemplateModel {
try decoder.decode(TemplateModel.self, from: data)
}
}
public extension TemplateProtocol where Self: PageBehaviorHandlerProtocol, Self: MVMCoreViewControllerProtocol {
/// Helper function to do common parsing logic.
func parseTemplate(json: [AnyHashable: Any]?) throws {
guard let pageJSON = json else { return }
let delegateObject = (self as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject
let delegateObject = delegateObject?() as? MVMCoreUIDelegateObject
let data = try JSONSerialization.data(withJSONObject: pageJSON)
let decoder = JSONDecoder.create(with: delegateObject)
templateModel = try decodeTemplate(using: decoder, from: data)
// Add additional required behaviors if applicable.
guard var behaviorHandlerModel = templateModel as? TemplateModelProtocol & PageBehaviorHandlerModelProtocol,
var behaviorHandler = self as? PageBehaviorHandlerProtocol else { return }
behaviorHandlerModel.traverseAndAddRequiredBehaviors()
behaviorHandler.createBehaviors(for: behaviorHandlerModel, delegateObject: delegateObject)
if let viewController = self as? UIViewController {
MVMCoreUISession.sharedGlobal()?.applyGlobalBehaviors(to: viewController)
}
}
func decodeTemplate(using decoder: JSONDecoder, from data: Data) throws -> TemplateModel {
try decoder.decode(TemplateModel.self, from: data)
guard var pageBehaviorsModel = templateModel as? TemplateModelProtocol & PageBehaviorContainerModelProtocol else { return }
pageBehaviorsModel.traverseAndAddRequiredBehaviors()
var behaviorHandler = self
behaviorHandler.applyBehaviors(pageBehaviorModel: pageBehaviorsModel, delegateObject: delegateObject)
}
}

View File

@ -32,7 +32,13 @@ import Foundation
public var tabBarIndex: Int?
public var shouldMaskScreenWhileRecording: Bool?
public var hideLeftPanel: Bool?
public var hideRightPanel: Bool?
public func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try replaceChildMolecule(at: &navigationBar, with: molecule)
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
@ -56,6 +62,8 @@ import Foundation
case tabBarHidden
case tabBarIndex
case shouldMaskScreenWhileRecording
case hideLeftPanel
case hideRightPanel
}
//--------------------------------------------------
@ -79,6 +87,12 @@ import Foundation
if let tabBarHidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .tabBarHidden) {
self.tabBarHidden = tabBarHidden
}
if let hideLeftPanel = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideLeftPanel) {
self.hideLeftPanel = hideLeftPanel
}
if let hideRightPanel = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideRightPanel) {
self.hideRightPanel = hideRightPanel
}
tabBarIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .tabBarIndex)
shouldMaskScreenWhileRecording = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskScreenWhileRecording)
}
@ -92,6 +106,8 @@ import Foundation
try container.encodeIfPresent(formRules, forKey: .formRules)
try container.encodeModelIfPresent(navigationBar, forKey: .navigationBar)
try container.encode(tabBarHidden, forKey: .tabBarHidden)
try container.encode(hideLeftPanel, forKey: .hideLeftPanel)
try container.encode(hideRightPanel, forKey: .hideRightPanel)
try container.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex)
try container.encode(shouldMaskScreenWhileRecording, forKey: .shouldMaskScreenWhileRecording)
}

View File

@ -81,11 +81,15 @@
open override func handleNewData() {
setup()
registerCells()
super.handleNewData()
}
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
setup()
registerCells()
super.handleNewData()
super.updateUI(for: molecules)
}
//--------------------------------------------------

View File

@ -22,6 +22,11 @@
}
return super.rootMolecules
}
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try super.replaceChildMolecule(with: molecule)
|| (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule))
}
//--------------------------------------------------
// MARK: - Initializer

View File

@ -26,6 +26,12 @@
return super.rootMolecules
}
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try super.replaceChildMolecule(with: molecule)
|| (molecules != nil && replaceChildMolecule(in: &(molecules!), with: molecule))
|| replaceChildMolecule(at: &line, with: molecule)
}
/// This template requires content.
func validateModelHasContent() throws {
if header == nil,

View File

@ -21,8 +21,8 @@ open class ModalSectionListTemplate: SectionListTemplate {
// MARK: - Lifecycle
//--------------------------------------------------
override open func handleNewData() {
super.handleNewData()
override open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
_ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in
guard let self = self else { return }
let closeAction = (self.templateModel as? ModalSectionListTemplateModel)?.closeAction ??

View File

@ -82,11 +82,22 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
}
open override func handleNewData() {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
setup()
registerWithTable()
super.handleNewData()
super.handleNewData() // Currently stuck as MoleculeListProtocol being called from AddRemoveMoleculesBehaviorModel.
}
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScrollArea = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScrollArea = templateModel?.anchorFooter ?? false
super.updateUI(for: molecules)
molecules?.forEach({ molecule in
if let index = moleculesInfo?.firstIndex(where: { $0.molecule.id == molecule.id }) {
moleculesInfo?[index].molecule = molecule
}
newData(for: molecule)
})
}
open override func viewDidAppear(_ animated: Bool) {
@ -124,6 +135,10 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
}
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return (getMoleculeInfo(for: indexPath)?.molecule as? ListItemModelProtocol)?.gone == true ? 0 : UITableView.automaticDimension
}
open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject)
@ -141,7 +156,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
guard let moleculeInfo = getMoleculeInfo(for: indexPath),
let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier)
else { return UITableViewCell() }
cell.isHidden = (getMoleculeInfo(for: indexPath)?.molecule as? ListItemModelProtocol)?.gone == true
(cell as? MoleculeViewProtocol)?.reset()
(cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath)
if let moleculeView = cell as? MoleculeViewProtocol {
@ -191,35 +206,43 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
}
open func newData(for molecule: MoleculeModelProtocol) {
//TODO: expand for header, navigation, etc
guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
if equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule) {
return true
} else if let parent = moleculeInfo.molecule as? ParentMoleculeModelProtocol {
// Get all molecules of the same type for faster check.
let molecules: [MoleculeModelProtocol] = parent.reduceDepthFirstTraverse(options: .childFirst, depth: 0, initialResult: []) { (accumulator, currentMolecule, depth) in
if currentMolecule.moleculeName == molecule.moleculeName {
return accumulator + [currentMolecule]
}
return accumulator
}
for moleculeB in molecules {
if equal(moleculeA: molecule, moleculeB: moleculeB) {
return true
}
}
}
return false
}) else { return }
//Check header and footer if replace happens then return.
if updateHeaderFooterView(topView, with: molecule) ||
updateHeaderFooterView(bottomView, with: molecule, isHeader: false) {
return
}
guard let moleculesInfo = moleculesInfo else { return }
let indicies = moleculesInfo.indices.filter({ index -> Bool in
return moleculesInfo[index].molecule.findFirstMolecule(by: {
$0.moleculeName == molecule.moleculeName && equal(moleculeA: molecule, moleculeB: $0)
}) != nil
})
// Refresh the cell. (reload loses cell selection)
let selectedIndex = tableView.indexPathForSelectedRow
let indexPath = IndexPath(row: index, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
let indexPaths = indicies.map {
return IndexPath(row: $0, section: 0)
}
tableView.reloadRows(at: indexPaths, with: .automatic)
if let selectedIndex = selectedIndex {
tableView.selectRow(at: selectedIndex, animated: false, scrollPosition: .none)
}
}
///Helper functions to update header/footer view
private func updateHeaderFooterView(_ view: UIView?, with molecule: MoleculeModelProtocol, isHeader: Bool = true) -> Bool {
if let updateView = view,
let moleculeView = MVMCoreUIUtility.findViews(by: MoleculeViewProtocol.self, views: [updateView]).first(where: { $0.model?.moleculeName == molecule.moleculeName && $0.model?.id == molecule.id }) {
updateMoleculeView(moleculeView, from: molecule)
//Redraw the header/footer for content update
isHeader ? showHeader(nil) : showFooter(nil)
return true
}
return false
}
//--------------------------------------------------
// MARK: - Convenience
@ -293,7 +316,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
}
extension MoleculeListTemplate: MoleculeListProtocol {
open func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
public func removeMolecules(at indexPaths: [IndexPath], animation: UITableView.RowAnimation?) {
for (index, indexPath) in indexPaths.sorted().enumerated() {
let removeIndex = indexPath.row - index
moleculesInfo?.remove(at: removeIndex)
@ -306,7 +329,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
view.layoutIfNeeded()
}
open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
public func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation?) {
var indexPaths: [IndexPath] = []
for molecule in molecules {
@ -325,7 +348,7 @@ extension MoleculeListTemplate: MoleculeListProtocol {
self.view.layoutIfNeeded()
}
open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
public func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? {
guard let index = moleculesInfo?.firstIndex(where: { (moleculeInfo) -> Bool in
return equal(moleculeA: molecule, moleculeB: moleculeInfo.molecule)
}) else { return nil }

View File

@ -16,7 +16,13 @@
public var moleculeStack: StackModel
public override var rootMolecules: [MoleculeModelProtocol] {
[navigationBar, header, moleculeStack, footer].compactMap { $0 }
super.rootMolecules + [moleculeStack]
}
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try super.replaceChildMolecule(with: molecule)
|| replaceChildMolecule(at: &navigationBar, with: molecule)
|| replaceChildMolecule(at: &moleculeStack, with: molecule)
}
//--------------------------------------------------

View File

@ -26,8 +26,8 @@
}
}
open override func handleNewData() {
super.handleNewData()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
heightConstraint?.isActive = true
}
}

View File

@ -21,6 +21,12 @@
[navigationBar, header, footer].compactMap { $0 }
}
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try super.replaceChildMolecule(with: molecule)
|| replaceChildMolecule(at: &header, with: molecule)
|| replaceChildMolecule(at: &footer, with: molecule)
}
//--------------------------------------------------
// MARK: - Init
//--------------------------------------------------

View File

@ -22,6 +22,11 @@
return super.rootMolecules
}
public override func replaceChildMolecule(with molecule: MoleculeModelProtocol) throws -> Bool {
return try super.replaceChildMolecule(with: molecule)
|| replaceChildMolecule(at: &middle, with: molecule)
}
//--------------------------------------------------
// MARK: - Init
//--------------------------------------------------

View File

@ -19,10 +19,10 @@ import UIKit
try super.parsePageJSON()
}
open override func handleNewData() {
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
topViewOutsideOfScroll = templateModel?.anchorHeader ?? false
bottomViewOutsideOfScroll = templateModel?.anchorFooter ?? false
super.handleNewData()
super.updateUI(for: molecules)
}
open override func viewForTop() -> UIView? {

View File

@ -9,6 +9,6 @@
import Foundation
public protocol MVMControllerModelProtocol: TemplateModelProtocol, FormHolderModelProtocol, PageBehaviorHandlerModelProtocol {
public protocol MVMControllerModelProtocol: TemplateModelProtocol, FormHolderModelProtocol, PageBehaviorContainerModelProtocol {
}

View File

@ -63,8 +63,8 @@ open class ScrollingViewController: ViewController {
registerForKeyboardNotifications()
}
open override func handleNewData() {
super.handleNewData()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
// will change scrollView indicatorStyle automatically on the basis of backgroundColor
var greyScale: CGFloat = 0
if view.backgroundColor?.getWhite(&greyScale, alpha: nil) ?? false {

View File

@ -112,8 +112,8 @@ import Foundation
}
}
open override func handleNewData() {
super.handleNewData()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
topView?.removeFromSuperview()
bottomView?.removeFromSuperview()
topView = viewForTop()

View File

@ -55,8 +55,11 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController, Rotor
tableView.reloadData()
}
open override func handleNewData() {
super.handleNewData()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
guard molecules == nil else { return }
createViewForTableHeader()
createViewForTableFooter()
tableView?.reloadData()

View File

@ -49,8 +49,8 @@ open class ThreeLayerViewController: ProgrammaticScrollViewController, RotorView
}
}
open override func handleNewData() {
super.handleNewData()
open override func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
super.updateUI(for: molecules)
// Removes the views
topView?.removeFromSuperview()

View File

@ -38,12 +38,19 @@ import MVMCore
public var behaviors: [PageBehaviorProtocol]?
public var needsUpdateUI = false
private var observingForResponses = false
private var observingForResponses: NSObjectProtocol?
private var initialLoadFinished = false
public var previousScreenSize = CGSize.zero
public var selectedField: UIView?
public var pageUpdateQueue: OperationQueue = {
let pageUpdateQueue = OperationQueue()
pageUpdateQueue.maxConcurrentOperationCount = 1
pageUpdateQueue.qualityOfService = .userInteractive
return pageUpdateQueue
}()
/// Checks if the screen width has changed
open func screenSizeChanged() -> Bool {
!MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1)
@ -54,18 +61,17 @@ import MVMCore
//--------------------------------------------------
open func observeForResponseJSONUpdates() {
guard !observingForResponses,
guard observingForResponses == nil,
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
else { return }
observingForResponses = true
NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:))
}
open func stopObservingForResponseJSONUpdates() {
guard observingForResponses else { return }
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil)
observingForResponses = false
guard let observingForResponses = observingForResponses else { return }
NotificationCenter.default.removeObserver(observingForResponses)
self.observingForResponses = nil
}
open func pagesToListenFor() -> [String]? {
@ -74,7 +80,9 @@ import MVMCore
}
open func modulesToListenFor() -> [String]? {
loadObject?.requestParameters?.allModules()
let requestModules = loadObject?.requestParameters?.allModules() ?? []
let behaviorModules = behaviors?.flatMap { $0.modulesToListenFor() } ?? []
return requestModules + behaviorModules
}
@objc open func responseJSONUpdated(notification: Notification) {
@ -109,9 +117,12 @@ import MVMCore
guard newData else { return }
do {
// TODO: Parse parsePageJSON modifies the page model on a different thread than
// the UI update which could cause discrepancies. Parse should return the resulting
// object and assignment should be synchronized on handleNewData(model: ).
try parsePageJSON()
MVMCoreDispatchUtility.performBlock(onMainThread: {
self.handleNewDataAndUpdateUI()
self.handleNewData()
})
} catch {
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") {
@ -158,40 +169,13 @@ import MVMCore
}
return false
}
return true
}
func describe(parsingError: Error) -> String {
if let registryError = parsingError as? ModelRegistry.Error {
switch (registryError) {
case .decoderErrorModelNotMapped(let identifier, let codingKey, let codingPath) where identifier != nil && codingKey != nil && codingPath != nil:
return "Error parsing template. Model identifier \"\(identifier!)\" is not mapped for \"\(codingKey!.stringValue)\" @ \(codingPath!.map { return $0.stringValue })"
case .decoderErrorObjectNotPresent(let codingKey, let codingPath):
return "Error parsing template. Required model \"\(codingKey.stringValue)\" was not found @ \(codingPath.map { return $0.stringValue })"
default:
return "Error parsing template. Registry error: \((registryError as NSError).localizedFailureReason ?? registryError.localizedDescription)"
}
}
if let decodingError = parsingError as? DecodingError {
switch (decodingError) {
case .keyNotFound(let codingKey, let context):
return "Error parsing template. Required key \(codingKey.stringValue) was not found @ \(context.codingPath.map { return $0.stringValue })"
case .valueNotFound(_, let context):
return "Error parsing template. Value not found @ \(context.codingPath.map { return $0.stringValue })"
case .typeMismatch(_, let context):
return "Error parsing template. Value type mismatch @ \(context.codingPath.map { return $0.stringValue })"
case .dataCorrupted(let context):
return "Error parsing template. Data corrupted @ \(context.codingPath.map { return $0.stringValue })"
@unknown default:
return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)"
}
if let error = parsingError as? HumanReadableDecodingErrorProtocol {
return "Error parsing template. \(error.readableDescription)"
}
return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)"
}
@ -227,13 +211,6 @@ import MVMCore
return true
}
/// Calls processNewData and then sets the ui to update with updateView
open func handleNewDataAndUpdateUI() {
handleNewData()
needsUpdateUI = true
view.setNeedsLayout()
}
/// Creates a legacy navigation model.
open func createDefaultLegacyNavigationModel() -> NavigationItemModel {
let navigationModel = NavigationItemModel()
@ -241,7 +218,8 @@ import MVMCore
return navigationModel
}
/// Processes any new data. Called after the page is loaded the first time and on response updates for this page,
/// Processes any new data. Called after the page is loaded the first time and on response updates for this page, Triggers a render refresh.
@MainActor
open func handleNewData() {
if model?.navigationBar == nil {
let navigationItem = createDefaultLegacyNavigationModel()
@ -257,12 +235,27 @@ import MVMCore
formValidator = FormValidator(rules)
}
updateUI()
// Notify the manager of new data.
// Warning: Some flows cause table reloads. Until the UI update is decoupled, should be after the updateUI.
manager?.newDataReceived?(in: self)
}
/// Applies the latest model to the UI.
open func updateUI(for molecules: [MoleculeModelProtocol]? = nil) {
guard molecules == nil else { return }
executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in
behavior.willRender(rootMolecules: getRootMolecules(), delegateObjectIVar)
}
if let backgroundColor = model?.backgroundColor {
view.backgroundColor = backgroundColor.uiColor
}
// Notify the manager of new data
manager?.newDataReceived?(in: self)
needsUpdateUI = true
view.setNeedsLayout()
}
public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? {
@ -331,7 +324,7 @@ import MVMCore
initialLoad()
}
handleNewDataAndUpdateUI()
handleNewData()
}
open override func viewDidLayoutSubviews() {
@ -511,13 +504,47 @@ import MVMCore
// MARK: - MoleculeDelegateProtocol
//--------------------------------------------------
open func getRootMolecules() -> [MoleculeModelProtocol] {
model?.rootMolecules ?? []
}
open func getTemplateModel() -> TemplateModelProtocol? { model }
open func getRootMolecules() -> [MoleculeModelProtocol] { model?.rootMolecules ?? [] }
// Needed otherwise when subclassed, the extension gets called.
open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { }
public func replaceMoleculeData(_ moleculeModels: [MoleculeModelProtocol]) {
pageUpdateQueue.addOperation {
let replacedModels:[MoleculeModelProtocol] = moleculeModels.compactMap { model in
guard self.attemptToReplace(with: model) else {
return nil
}
return model
}
if replacedModels.count > 0 {
DispatchQueue.main.sync {
self.updateUI(for: replacedModels)
}
}
}
}
open func attemptToReplace(with replacementModel: MoleculeModelProtocol) -> Bool {
guard var templateModel = getTemplateModel() else { return false }
var didReplace = false
do {
didReplace = try templateModel.replaceMolecule(with: replacementModel)
if !didReplace {
MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Failed to find '\(replacementModel.id)' in the current screen.", code: ErrorCode.viewControllerProcessingJSON.rawValue, domain: ErrorDomainSystem, location: String(describing: type(of: self)))!)
}
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error replacing molecule \"\(replacementModel.id)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
}
return didReplace
}
//--------------------------------------------------
// MARK: - MVMCoreUIDetailViewProtocol
//--------------------------------------------------
@ -532,7 +559,7 @@ import MVMCore
public func isRightPanelAccessible() -> Bool {
// TODO: Remove when FAB is 100%.
if loadObject?.pageJSON?.boolForKey(KeyHideMainMenu) ?? false {
if let hideRightPanel = model?.hideRightPanel, hideRightPanel == true {
return false
}
return (MVMCoreUISession.sharedGlobal()?.launchAppLoadedSuccessfully ?? false) || showRightPanelForScreenBeforeLaunchApp()

View File

@ -76,7 +76,7 @@ public class AddRemoveMoleculesBehavior: PageCustomActionHandlerBehavior, PageMo
self.delegate = delegateObject
}
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
public func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
guard let list = delegate?.moleculeListDelegate else { return }
for case let model as (MoleculeModelProtocol & ListItemModelProtocol & AddMolecules) in rootMolecules {
if let moleculesToAdd = model.getRecursiveMoleculesToAdd(),

View File

@ -43,7 +43,7 @@ public class PageGetContactBehavior: PageVisibilityBehavior {
MVMCoreDispatchUtility.performBlock(onMainThread: {
// TODO: move to protocol function instead
guard let controller = self?.delegate?.moleculeDelegate as? ViewController else { return }
controller.handleNewDataAndUpdateUI()
controller.handleNewData()
})
}
}

View File

@ -45,11 +45,12 @@ public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior {
for consumer in consumers {
consumer.consume(notificationStatus: settings.authorizationStatus)
}
// Tell template to update
MVMCoreDispatchUtility.performBlock(onMainThread: {
Task {
// Tell template to update
guard let controller = self.delegate?.moleculeDelegate as? ViewController else { return }
controller.handleNewDataAndUpdateUI()
})
await controller.handleNewData()
}
}
}

View File

@ -0,0 +1,104 @@
//
// RefreshableMoleculeBehavior.swift
// MVMCoreUI
//
// Created by Kyle Hedden on 9/12/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import MVMCore
public class PollingBehaviorModel: PageBehaviorModelProtocol {
public class var identifier: String { "pollingBehavior" }
public var shouldAllowMultipleInstances: Bool { true }
public let refreshInterval: TimeInterval
public let refreshAction: ActionModelProtocol
public var runWhileHidden: Bool
public var refreshOnFirstLoad: Bool
public var refreshOnShown: Bool
private enum CodingKeys: String, CodingKey {
case refreshInterval
case refreshAction
case runWhileHidden
case refreshOnFirstLoad
case refreshOnShown
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
refreshInterval = try typeContainer.decode(TimeInterval.self, forKey: .refreshInterval)
refreshAction = try typeContainer.decodeModel(codingKey: .refreshAction)
runWhileHidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .runWhileHidden) ?? false
refreshOnFirstLoad = try typeContainer.decodeIfPresent(Bool.self, forKey: .refreshOnFirstLoad) ?? false
refreshOnShown = try typeContainer.decodeIfPresent(Bool.self, forKey: .refreshOnShown) ?? false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(refreshInterval, forKey: .refreshInterval)
try container.encode(refreshAction, forKey: .refreshAction)
try container.encode(runWhileHidden, forKey: .runWhileHidden)
try container.encode(refreshOnFirstLoad, forKey: .refreshOnFirstLoad)
try container.encode(refreshOnShown, forKey: .refreshOnShown)
}
}
public class PollingBehavior: NSObject, PageVisibilityBehavior {
var model: PollingBehaviorModel
var delegateObject: MVMCoreUIDelegateObject?
var lastRefresh = Date.distantPast
var pollTimer: DispatchSourceTimer?
var remainingTimeToRefresh: TimeInterval {
lastRefresh.timeIntervalSinceNow - model.refreshInterval
}
var firstTimeLoad = true
var refreshOnShown: Bool {
if model.refreshOnFirstLoad && firstTimeLoad {
return true
}
firstTimeLoad = false
return model.refreshOnShown
}
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
self.model = model as! PollingBehaviorModel
self.delegateObject = delegateObject
}
public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {
resumePollingTimer(withRemainingTime: refreshOnShown ? 0 : remainingTimeToRefresh, refreshAction: model.refreshAction, interval: model.refreshInterval)
}
public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) {
pollTimer?.cancel()
}
func resumePollingTimer(withRemainingTime timeRemaining: TimeInterval, refreshAction: ActionModelProtocol, interval: TimeInterval) {
let delegateObject = delegateObject
pollTimer?.cancel()
pollTimer = DispatchSource.makeTimerSource()
pollTimer?.schedule(deadline: .now() + timeRemaining, repeating: interval)
pollTimer?.setEventHandler(qos:.utility) {
Task {
if let delegateActionHandler = delegateObject?.actionDelegate as? ActionDelegateProtocol {
try? await delegateActionHandler.performAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject)
} else {
try? await MVMCoreActionHandler.shared()?.handleAction(with: refreshAction, additionalData: nil, delegateObject: delegateObject)
}
}
}
pollTimer?.resume()
}
deinit {
pollTimer?.cancel()
}
}

View File

@ -1,16 +1,17 @@
//
// PageBehaviorHandlerModelProtocol.swift
// PageBehaviorConatinerModelProtocol.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 3/29/21.
// Copyright © 2021 Verizon Wireless. All rights reserved.
//
public protocol PageBehaviorHandlerModelProtocol {
/// Protocol applied to a model that contains a list of behavior models.
public protocol PageBehaviorContainerModelProtocol {
var behaviors: [PageBehaviorModelProtocol]? { get set }
}
public extension PageBehaviorHandlerModelProtocol {
public extension PageBehaviorContainerModelProtocol {
/// Adds the behavior model to the behaviors if possible.
mutating func add(behavior: PageBehaviorModelProtocol) {
@ -24,7 +25,7 @@ public extension PageBehaviorHandlerModelProtocol {
}
}
public extension PageBehaviorHandlerModelProtocol where Self: MoleculeTreeTraversalProtocol {
public extension PageBehaviorContainerModelProtocol where Self: MoleculeTreeTraversalProtocol {
/// Traverses all models and adds any required behavior models.
mutating func traverseAndAddRequiredBehaviors() {

View File

@ -12,20 +12,10 @@ public protocol PageBehaviorHandlerProtocol {
}
public extension PageBehaviorHandlerProtocol {
/// Creates the behaviors and sets the variable.
mutating func createBehaviors(for model: PageBehaviorHandlerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
behaviors = behaviors?.filter { $0.transcendsPageUpdates }
if behaviors?.isEmpty ?? false {
behaviors = nil
}
guard let behaviorModels = model.behaviors else {
return
}
var behaviors: [PageBehaviorProtocol] = behaviors ?? []
func createBehaviors(for behaviorModels: [PageBehaviorModelProtocol], delegateObject: MVMCoreUIDelegateObject?) -> [PageBehaviorProtocol] {
var behaviors = [PageBehaviorProtocol]()
for behaviorModel in behaviorModels {
do {
let handlerType = try ModelRegistry.getHandler(behaviorModel) as! PageBehaviorProtocol.Type
@ -37,7 +27,23 @@ public extension PageBehaviorHandlerProtocol {
}
}
}
return behaviors
}
mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
// Pull the existing behaviors.
var behaviors = (behaviors ?? []).filter { $0.transcendsPageUpdates }
// Create and append any new behaviors based on the incoming models.
let newBehaviors = createBehaviors(for: pageBehaviorModel.behaviors ?? [], delegateObject: delegateObject)
behaviors.append(contentsOf: newBehaviors)
// Apply them to the page.
self.behaviors = behaviors.count > 0 ? behaviors : nil
// Ask the session to apply any more. (Curently inverted contol due to Swift <--> Obj-C conflict.
if let viewController = self as? UIViewController {
MVMCoreUISession.sharedGlobal()?.applyGlobalBehaviors(to: viewController)
}
}
/// Executes all behaviors of type.
@ -49,3 +55,11 @@ public extension PageBehaviorHandlerProtocol {
return try behaviors?.compactMap({$0 as? T}).allSatisfy({ return try behaviourBlock($0) }) ?? true
}
}
public extension PageBehaviorHandlerProtocol where Self: MVMCoreViewControllerProtocol {
mutating func applyBehaviors(pageBehaviorModel: PageBehaviorContainerModelProtocol) {
applyBehaviors(pageBehaviorModel: pageBehaviorModel, delegateObject: delegateObject?() as? MVMCoreUIDelegateObject)
}
}

View File

@ -14,12 +14,15 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol {
/// Should the behavior persist regardless of page behavior model updates.
var transcendsPageUpdates: Bool { get }
func modulesToListenFor() -> [String]
/// Initializes the behavior with the model
init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?)
}
public extension PageBehaviorProtocol {
var transcendsPageUpdates: Bool { return false }
func modulesToListenFor() -> [String] { return [] }
}
/**
@ -32,6 +35,7 @@ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol {
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol)
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar)
func didSetupNavigationBar(view: UINavigationBar, with model: NavigationItemModelProtocol)
func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?)
func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject) throws -> Bool
}
@ -42,11 +46,11 @@ public extension PageMoleculeTransformationBehavior {
func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {}
func willSetupNavigationBar(with model: NavigationItemModelProtocol, updating view: UINavigationBar) {}
func didSetupNavigationBar(view: UINavigationBar, with model: NavigationItemModelProtocol) {}
func willRender(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {}
func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject) throws -> Bool { return true }
}
public protocol PageVisibilityBehavior: PageBehaviorProtocol {
func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?)
func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?)
func willHidePage(_ delegateObject: MVMCoreUIDelegateObject?)
@ -84,8 +88,8 @@ public protocol PageCustomActionHandlerBehavior: PageBehaviorProtocol {
}
public extension MVMCoreUIDelegateObject {
var behaviorModelDelegate: PageBehaviorHandlerModelProtocol? {
(moleculeDelegate as? PageProtocol)?.pageModel as? PageBehaviorHandlerModelProtocol
var behaviorModelDelegate: PageBehaviorContainerModelProtocol? {
(moleculeDelegate as? PageProtocol)?.pageModel as? PageBehaviorContainerModelProtocol
}
weak var behaviorTemplateDelegate: (PageBehaviorHandlerProtocol & NSObjectProtocol)? {

View File

@ -0,0 +1,110 @@
//
// ReplacementMoleculeBehavior.swift
// MVMCoreUI
//
// Created by Kyle Hedden on 9/12/23.
// Copyright © 2023 Verizon Wireless. All rights reserved.
//
import Foundation
import MVMCore
public class ReplaceableMoleculeBehaviorModel: PageBehaviorModelProtocol {
public class var identifier: String { "replaceMoleculeBehavior" }
public var shouldAllowMultipleInstances: Bool { true }
public var moleculeIds: [String]
}
public class ReplaceableMoleculeBehavior: PageMoleculeTransformationBehavior {
var moleculeIds: [String]
var modulesToListenFor: [String]
private var observingForResponses: NSObjectProtocol?
private var delegateObject: MVMCoreUIDelegateObject?
public var transcendsPageUpdates: Bool { true }
public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) {
moleculeIds = (model as! ReplaceableMoleculeBehaviorModel).moleculeIds
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
if shouldListenForListUpdates {
modulesToListenFor = []
listenForModuleUpdates()
} else {
modulesToListenFor = moleculeIds
stopListeningForModuleUpdates()
}
self.delegateObject = delegateObject
guard let pageType = delegateObject?.moleculeDelegate?.getTemplateModel()?.pageType else { return }
MVMCoreViewControllerMappingObject.shared()?.addOptionalModules(toMapping: moleculeIds, forPageType: pageType)
}
public func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {
self.delegateObject = delegateObject
let shouldListenForListUpdates = delegateObject?.moleculeListDelegate != nil
if shouldListenForListUpdates {
modulesToListenFor = []
listenForModuleUpdates()
} else {
modulesToListenFor = moleculeIds
stopListeningForModuleUpdates()
}
let moleculeModels = moleculeIds.compactMap { moleculeId in
do {
return try delegateObject?.moleculeDelegate?.getModuleWithName(moleculeId)
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
return nil
}
}
if moleculeModels.count > 0 {
delegateObject?.moleculeDelegate?.replaceMoleculeData(moleculeModels)
}
}
private func listenForModuleUpdates() {
guard observingForResponses == nil else { return }
let pageUpdateQueue = OperationQueue()
pageUpdateQueue.maxConcurrentOperationCount = 1
pageUpdateQueue.qualityOfService = .userInteractive
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue, using: responseJSONUpdated(notification:))
}
private func stopListeningForModuleUpdates() {
guard let observingForResponses = observingForResponses else { return }
NotificationCenter.default.removeObserver(observingForResponses)
self.observingForResponses = nil
}
@objc func responseJSONUpdated(notification: Notification) {
guard let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) else { return }
let modules: [MoleculeModelProtocol] = moleculeIds.compactMap { moleculeId in
guard let json = modulesLoaded.optionalDictionaryForKey(moleculeId) else { return nil }
do {
return try convertToModel(moduleJSON: json)
} catch {
let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: String(describing: type(of: self)))!
if let error = error as? HumanReadableDecodingErrorProtocol {
coreError.messageToLog = "Error decoding replacement \"\(moleculeId)\": \(error.readableDescription)"
}
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
return nil
}
}
if modules.count > 0 {
delegateObject?.moleculeDelegate?.replaceMoleculeData(modules)
}
}
private func convertToModel(moduleJSON: [String: Any]) throws -> MoleculeModelProtocol {
guard let moleculeName = moduleJSON.optionalStringForKey(KeyMoleculeName),
let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) else {
throw ModelRegistry.Error.decoderErrorModelNotMapped(identifer: moduleJSON.optionalStringForKey(KeyMoleculeName))
}
return try modelType.decode(jsonDict: moduleJSON as [String : Any]) as! MoleculeModelProtocol
}
}

View File

@ -231,6 +231,8 @@ open class CoreUIModelMapping: ModelMapping {
ModelRegistry.register(handler: PageGetContactBehavior.self, for: PageGetContactBehaviorModel.self)
ModelRegistry.register(handler: AddRemoveMoleculesBehavior.self, for: AddRemoveMoleculesBehaviorModel.self)
ModelRegistry.register(handler: GetNotificationAuthStatusBehavior.self, for: GetNotificationAuthStatusBehaviorModel.self)
ModelRegistry.register(handler: ReplaceableMoleculeBehavior.self, for: ReplaceableMoleculeBehaviorModel.self)
ModelRegistry.register(handler: PollingBehavior.self, for: PollingBehaviorModel.self)
}
open override class func registerActions() {

View File

@ -57,59 +57,6 @@ open class Styler {
case B2 // Maps to RegularBodySmall
case B3 // Maps to RegularMicro
/// Returns the font size of the current enum case.
public func pointSize() -> CGFloat {
switch self {
case .RegularFeatureXLarge,
.BoldFeatureXLarge:
return 96
case .RegularFeatureLarge,
.BoldFeatureLarge:
return 80
case .RegularFeatureMedium,
.BoldFeatureMedium:
return 64
case .RegularFeatureSmall,
.BoldFeatureSmall:
return 48
case .RegularFeatureXSmall,
.BoldFeatureXSmall,
.RegularTitle2XLarge,
.BoldTitle2XLarge,
.Title2XLarge,
.H1:
return 40
case .RegularTitleXLarge,
.BoldTitleXLarge,
.TitleXLarge,
.H32:
return 32
case .BoldTitleLarge,
.RegularTitleLarge,
.H2:
return 24
case .BoldTitleMedium,
.RegularTitleMedium,
.H3:
return 20
case .RegularTitleSmall,
.BoldTitleSmall,
.BoldBodyLarge,
.RegularBodyLarge,
.B20:
return 16
case .RegularBodyMedium,
.BoldBodyMedium:
return 14
case .BoldBodySmall, .B1,
.RegularBodySmall, .B2:
return 12
case .BoldMicro,
.RegularMicro, .B3:
return 11
}
}
public func color() -> UIColor {
switch self {
case .B3:
@ -148,8 +95,8 @@ open class Styler {
/// Returns the font based on the declared enum case.
public func getFont(_ genericScaling: Bool = true) -> UIFont {
let size = genericScaling ? sizeFontGeneric(forCurrentDevice: pointSize()) : pointSize()
return MFStyler.getFontFor(size: size, isBold: isBold())
let vdsStyle = vdsTextStyle() ?? .defaultStyle
return vdsStyle.font
}
/// Styles the provided label to the declared enum Font case.
@ -237,7 +184,7 @@ open class Styler {
/// Creates the appropriate VZW font for a VDS style, scaling based on the scaleValue threshold passed in.
@objc static func getFontFor(styleString: String, scaleValue: CGFloat) -> UIFont? {
guard let font = Styler.Font(rawValue: styleString),
let size = Styler.Font(rawValue: styleString)?.pointSize(),
let size = font.vdsTextStyle()?.pointSize,
let newSize = Styler.sizeObjectGeneric(forCurrentDevice: size)?.getValueBased(onSize: scaleValue) else { return nil }
return getFontFor(size: newSize, isBold: font.isBold())
}