diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index eb5327be..5f9b416e 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -595,6 +595,8 @@ EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; }; EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */; }; + EA985C3E2970938F00F2FF2E /* Tilet.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3D2970938F00F2FF2E /* Tilet.swift */; }; + EA985C402970939A00F2FF2E /* TiletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C3F2970939A00F2FF2E /* TiletModel.swift */; }; EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */; }; EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */; }; EAA0CFB3275E831E00D65EB0 /* DisableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */; }; @@ -1197,6 +1199,8 @@ EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableFormFieldEffectModel.swift; sourceTree = ""; }; EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIUpdatableModelProtocol.swift; sourceTree = ""; }; + EA985C3D2970938F00F2FF2E /* Tilet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilet.swift; sourceTree = ""; }; + EA985C3F2970939A00F2FF2E /* TiletModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiletModel.swift; sourceTree = ""; }; EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldEffectProtocol.swift; sourceTree = ""; }; EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideFormFieldEffectModel.swift; sourceTree = ""; }; EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableFormFieldEffectModel.swift; sourceTree = ""; }; @@ -2255,6 +2259,8 @@ AA37CBD42519072F0027344C /* Stars.swift */, AA07EA902510A442009A2AE3 /* StarModel.swift */, AA07EA922510A451009A2AE3 /* Star.swift */, + EA985C3D2970938F00F2FF2E /* Tilet.swift */, + EA985C3F2970939A00F2FF2E /* TiletModel.swift */, ); path = Views; sourceTree = ""; @@ -2777,6 +2783,7 @@ D2E2A99D23DA3217000B42E6 /* UIStackViewAlignment+Extension.swift in Sources */, 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */, D2A92884241ACB25004E01C6 /* ProgrammaticScrollViewController.swift in Sources */, + EA985C3E2970938F00F2FF2E /* Tilet.swift in Sources */, D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */, 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */, D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */, @@ -3170,6 +3177,7 @@ D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */, 012A88C2238D7BCA00FE3DA1 /* CarouselItemModel.swift in Sources */, 27F6B08C26052AFF008529AA /* ParentMoleculeModelProtocol.swift in Sources */, + EA985C402970939A00F2FF2E /* TiletModel.swift in Sources */, 0AB764D324460FA400E7FE72 /* UIPickerView+Extension.swift in Sources */, D29DF29E21E7AE3B003B2FB9 /* MFStyler.m in Sources */, 94C661D923CCF4B400D9FE5B /* LeftRightLabelModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 059c5637..142bc700 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -741,36 +741,6 @@ public typealias ActionBlock = () -> () clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1)) } - /** - Provides a text container and layout manager of how the text would appear on screen. - They are used in tandem to derive low-level TextKit results of the label. - */ - public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? { - - // Must configure the attributed string to translate what would appear on screen to accurately analyze. - guard let attributedText = attributedText else { return nil } - - let paragraph = NSMutableParagraphStyle() - paragraph.alignment = textAlignment - - let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) - stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) - - let textStorage = NSTextStorage(attributedString: stagedAttributedString) - let layoutManager = NSLayoutManager() - let textContainer = NSTextContainer(size: .zero) - - layoutManager.addTextContainer(textContainer) - textStorage.addLayoutManager(layoutManager) - - textContainer.lineFragmentPadding = 0.0 - textContainer.lineBreakMode = lineBreakMode - textContainer.maximumNumberOfLines = numberOfLines - textContainer.size = bounds.size - - return (textContainer, layoutManager, textStorage) - } - public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect { guard let abstractContainer = label.abstractTextContainer() else { return CGRect() } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilet.swift new file mode 100644 index 00000000..d176e6aa --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilet.swift @@ -0,0 +1,188 @@ +// +// Tilet.swift +// MVMCoreUI +// +// Created by Matt Bruce on 1/12/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore +import VDS + +/** + This class expects its height and width to be equal. + */ +open class Tilet: VDS.Tilet, MVMCoreUIViewConstrainingProtocol, MoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + var delegateObject: MVMCoreUIDelegateObject? + + public var checkboxModel: CheckboxModel? + + /// Disables all selection logic when setting the value of isSelected, reducing it to a stored property. + public var updateSelectionOnly: Bool = false + + /// Action Block called when the switch is selected. + public var actionBlock: ActionBlock? + + /** + The represented state of the Checkbox. + + Setting updateSelectionOnly to true bypasses the animation logic inherent with setting this property. + */ + override open var isSelected: Bool { + didSet { + if !updateSelectionOnly { + checkboxModel?.selected = isSelected + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + updateAccessibilityLabel() + } + } + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + /// There is currently no intention on using xib files. + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + fatalError("xib file is not implemented for Checkbox.") + } + + public convenience required init() { + self.init(frame:.zero) + } + + public convenience init(isChecked: Bool) { + self.init(frame: .zero) + checkAndBypassAnimations(selected: isChecked) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func initialSetup() { + super.initialSetup() + + publisher(for: .touchUpInside) + .sink {[weak self] toggle in + guard let self = self else { return } + self.toggleAndAction() + }.store(in: &subscribers) + + accessibilityHintEnabled = MVMCoreUIUtility.hardcodedString(withKey: "checkbox_action_hint") + updateAccessibilityLabel() + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + + /// This will toggle the state of the Checkbox and execute the actionBlock if provided. + public func toggleAndAction() { + isSelected.toggle() + actionBlock?() + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// Programmatic means to check/uncheck the box. + /// - parameter selected: state of the check box: true = checked OR false = unchecked. + /// - parameter animated: allows the state of the checkbox to change with or without animation. + public func updateSelection(to selected: Bool, animated: Bool) { + DispatchQueue.main.async { + self.checkAndBypassAnimations(selected: selected) + } + } + + /// Adjust accessibility label based on state of Checkbox. + func updateAccessibilityLabel() { + // Attention: This needs to be addressed with the accessibility team. + // NOTE: Currently emptying description part of MVMCoreUICheckBox accessibility label to avoid crashing! + if let state = MVMCoreUIUtility.hardcodedString(withKey: isSelected ? "checkbox_checked_state" : "checkbox_unchecked_state") { + accessibilityLabel = String(format: MVMCoreUIUtility.hardcodedString(withKey: "checkbox_desc_state") ?? "%@%@", "", state) + } + } + + private func checkAndBypassAnimations(selected: Bool) { + updateSelectionOnly = true + isSelected = selected + updateSelectionOnly = false + } + + //-------------------------------------------------- + // MARK: - UITouch + //-------------------------------------------------- + + override open func accessibilityActivate() -> Bool { + guard isEnabled else { return false } + sendActions(for: .touchUpInside) + return true + } + + //-------------------------------------------------- + // MARK: - Molecular + //-------------------------------------------------- + + open func needsToBeConstrained() -> Bool { true } + + open func updateView(_ size: CGFloat) {} + + open override func reset() { + super.reset() + + isEnabled = true + checkAndBypassAnimations(selected: false) + } + + private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: checkboxModel, additionalData: additionalData, delegateObject: delegateObject) + } + + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + self.delegateObject = delegateObject + + guard let model = model as? CheckboxModel else { return } + checkboxModel = model + + FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) + + if model.selected { + checkAndBypassAnimations(selected: model.selected) + } + + model.updateUI = { [weak self] in + MVMCoreDispatchUtility.performBlock(onMainThread: { + guard let self = self else { return } + self.isEnabled = model.enabled && !model.readOnly + }) + } + + isEnabled = model.enabled && !model.readOnly + + if (model.action != nil || model.offAction != nil) { + actionBlock = { [weak self] in + guard let self = self else { return } + + if let offAction = model.offAction, !self.isSelected { + self.performCheckboxAction(with: offAction, delegateObject: delegateObject, additionalData: additionalData) + + } else if let action = model.action { + self.performCheckboxAction(with: action, delegateObject: delegateObject, additionalData: additionalData) + } + } + } + } +} + diff --git a/MVMCoreUI/Atomic/Atoms/Views/TiletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TiletModel.swift new file mode 100644 index 00000000..340ee329 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/TiletModel.swift @@ -0,0 +1,28 @@ +// +// TiletModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 1/12/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class TiletModel: MoleculeModelProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "tilet" + public var backgroundColor: Color? + public var surface: Surface = .light + public var badge: TiletBadgeModel? + public var title: TiletTitleModel? + public var subTitle: TiletSubTitleModel? + public var desriptiveIcon: TiletDescriptiveIcon? + public var directionalIcon: TiletDirectionalIcon? + public var width: Double? + public var textWidth: Double? + public var textPercentage: Double? +}