diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index ad8b8a73..eb5327be 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -593,14 +593,11 @@ EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; - EA797B402909936000DBAFE6 /* VDSTypographyTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA797B3F2909936000DBAFE6 /* VDSTypographyTokens.xcframework */; }; EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; }; EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.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 */; }; - EAA5EEF828F5D079003B3210 /* VDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA5EEF628F5D074003B3210 /* VDS.framework */; }; - EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */; }; EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */; }; EAB14BC327D9378D0012AB2C /* RuleAnyModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */; }; EABFC1412763BB8D00E78B40 /* FormLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFC1402763BB8D00E78B40 /* FormLabel.swift */; }; @@ -1198,14 +1195,11 @@ EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = ""; }; EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = ""; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; - EA797B3F2909936000DBAFE6 /* VDSTypographyTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTypographyTokens.xcframework; path = ../SharedFrameworks/VDSTypographyTokens.xcframework; 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 = ""; }; 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 = ""; }; - EAA5EEF628F5D074003B3210 /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSMoleculeViewProtocol.swift; sourceTree = ""; }; EAB14BC027D935F00012AB2C /* RuleCompareModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleCompareModelProtocol.swift; sourceTree = ""; }; EAB14BC227D9378D0012AB2C /* RuleAnyModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyModelProtocol.swift; sourceTree = ""; }; EABFC1402763BB8D00E78B40 /* FormLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormLabel.swift; sourceTree = ""; }; @@ -1220,9 +1214,7 @@ files = ( D29DF0E621E4F3C7003B2FB9 /* MVMCore.framework in Frameworks */, AFE4A1D127DFB5EE00C458D0 /* VDSColorTokens.xcframework in Frameworks */, - EAA5EEF828F5D079003B3210 /* VDS.framework in Frameworks */, 187FEB2A2844D2A600BF29C2 /* VDSFormControlsTokens.xcframework in Frameworks */, - EA797B402909936000DBAFE6 /* VDSTypographyTokens.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2066,8 +2058,6 @@ D29DF0E421E4F3C7003B2FB9 /* Frameworks */ = { isa = PBXGroup; children = ( - EA797B3F2909936000DBAFE6 /* VDSTypographyTokens.xcframework */, - EAA5EEF628F5D074003B3210 /* VDS.framework */, 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */, AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */, D29DF0E521E4F3C7003B2FB9 /* MVMCore.framework */, @@ -2462,7 +2452,6 @@ D2B9D0E3265EEE9D0084735C /* MoleculeListProtocol.swift */, 011B58EE23A2AA850085F53C /* ModelProtocols */, 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */, - EAA7801F290081320057DFDF /* VDSMoleculeViewProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -2789,7 +2778,6 @@ 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */, D2A92884241ACB25004E01C6 /* ProgrammaticScrollViewController.swift in Sources */, D23A90002612347A007E14CE /* PageBehaviorHandlerModelProtocol.swift in Sources */, - EAA78020290081320057DFDF /* VDSMoleculeViewProtocol.swift in Sources */, 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */, D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */, D2C5001921F8ECDD001DA659 /* MVMCoreUIViewControllerMappingObject.m in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index b220a330..85ad8e9c 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -8,8 +8,8 @@ import MVMCore import UIKit -import VDS -import Combine + +public typealias ActionBlockConfirmation = () -> (Bool) /** A custom implementation of Apple's UISwitch. @@ -19,123 +19,402 @@ import Combine Container: The background of the toggle control. Knob: The circular indicator that slides on the container. */ -open class Toggle: ToggleBase, VDSMoleculeViewProtocol { +@objcMembers open class Toggle: Control, MVMCoreUIViewConstrainingProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public var viewModel: ToggleModel! - public var delegateObject: MVMCoreUIDelegateObject? - public var additionalData: [AnyHashable: Any]? - public var valueChangedSubscription: AnyCancellable? { - willSet{ - if let current = valueChangedSubscription { - current.cancel() - } + + /// Holds the on and off colors for the container. + public var containerTintColor: (on: UIColor, off: UIColor) = (on: .mvmGreen, off: .mvmBlack) + + /// Holds the on and off colors for the knob. + public var knobTintColor: (on: UIColor, off: UIColor) = (on: .mvmWhite, off: .mvmWhite) + + /// Holds the on and off colors for the disabled state.. + public var disabledTintColor: (container: UIColor, knob: UIColor) = (container: .mvmCoolGray3, knob: .mvmWhite) + + /// Set this flag to false if you do not want to animate state changes. + public var isAnimated = true + + public var didToggleAction: ActionBlock? + + /// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute. + public var shouldToggleAction: ActionBlockConfirmation? = { + return { true } + }() + + // Sizes are from InVision design specs. + static let containerSize = CGSize(width: 51, height: 31) + static let knobSize = CGSize(width: 28, height: 28) + + private var knobView: View = { + let view = View() + view.backgroundColor = .white + view.layer.cornerRadius = Toggle.getKnobHeight() / 2.0 + return view + }() + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + open override var isEnabled: Bool { + didSet { + isUserInteractionEnabled = isEnabled + changeStateNoAnimation(isEnabled ? isOn : false) + setToggleAppearanceFromState() + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: isEnabled ? "AccToggleHint" : "AccDisabled") } } + + /// Simple means to prevent user interaction with the toggle. + public var isLocked: Bool = false { + didSet { isUserInteractionEnabled = !isLocked } + } + + /// The state on the toggle. Default value: false. + open var isOn: Bool = false { + didSet { + if isAnimated { + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn, animations: { + if self.isOn { + self.knobView.backgroundColor = self.knobTintColor.on + self.backgroundColor = self.containerTintColor.on + + } else { + self.knobView.backgroundColor = self.knobTintColor.off + self.backgroundColor = self.containerTintColor.off + } + }, completion: nil) + + UIView.animate(withDuration: 0.33, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.2, options: [], animations: { + self.constrainKnob() + self.knobWidthConstraint?.constant = Self.getKnobWidth() + self.layoutIfNeeded() + }, completion: nil) + + } else { + setToggleAppearanceFromState() + self.constrainKnob() + } + + toggleModel?.selected = isOn + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + accessibilityValue = isOn ? MVMCoreUIUtility.hardcodedString(withKey: "AccOn") : MVMCoreUIUtility.hardcodedString(withKey: "AccOff") + setNeedsLayout() + layoutIfNeeded() + } + } + + public var toggleModel: ToggleModel? { + model as? ToggleModel + } + + //-------------------------------------------------- + // MARK: - Delegate + //-------------------------------------------------- + + private var delegateObject: MVMCoreUIDelegateObject? + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + private var knobLeadingConstraint: NSLayoutConstraint? + private var knobTrailingConstraint: NSLayoutConstraint? + private var knobHeightConstraint: NSLayoutConstraint? + private var knobWidthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + private var widthConstraint: NSLayoutConstraint? + + private func constrainKnob() { + + knobLeadingConstraint?.isActive = false + knobTrailingConstraint?.isActive = false + + _ = isOn ? constrainKnobOn() : constrainKnobOff() + + knobTrailingConstraint?.isActive = true + knobLeadingConstraint?.isActive = true + } + + private func constrainKnobOn() { + + knobTrailingConstraint = trailingAnchor.constraint(equalTo: knobView.trailingAnchor, constant: 2) + knobLeadingConstraint = knobView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor) + } + + private func constrainKnobOff() { + + knobTrailingConstraint = trailingAnchor.constraint(greaterThanOrEqualTo: knobView.trailingAnchor) + knobLeadingConstraint = knobView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 2) + } //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public convenience override init() { + self.init(frame: .zero) + } + public convenience init(isOn: Bool) { self.init(frame: .zero) self.isOn = isOn } - open override func initialSetup() { - super.initialSetup() - - publisher(for: .touchUpInside) - .sink {[weak self] toggle in - guard let self = self else { return } - self.toggle() - }.store(in: &subscribers) - - //this is logic that will always need to be run - //and is added into the array of Set - publisher(for: .valueChanged) - .sink {[weak self] _ in - guard let self = self, let viewModel = self.viewModel else { return } - //sync the value on the viewModel - viewModel.selected = self.isOn - - //tell the form you changed - _ = FormValidator.validate(delegate: self.delegateObject?.formHolderDelegate) - - }.store(in: &subscribers) - - //register the defaultActionExecuter - //this can then be overwritten by a subclass - valueChangedSubscription = publisher(for: .valueChanged) - .sink {[weak self] _ in - guard let self = self else { return } - self.executeDefaultAction() - } - - accessibilityLabelEnabled = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") - accessibilityLabelDisabled = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") - accessibilityHintEnabled = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint") - accessibilityHintDisabled = MVMCoreUIUtility.hardcodedString(withKey: "AccDisabled") - accessibilityValueEnabled = MVMCoreUIUtility.hardcodedString(withKey: "AccOn") - accessibilityValueDisabled = MVMCoreUIUtility.hardcodedString(withKey: "AccOff") + /// - parameter isOn: Bool to set the state of the toggle. + /// - parameter didToggleAction: A closure which is executed after the toggle changes states. + public convenience init(isOn: Bool = false, didToggleAction: ActionBlock?) { + self.init(frame: .zero) + changeStateNoAnimation(isOn) + self.didToggleAction = didToggleAction } - open func updateView(_ size: CGFloat) {} - - open override func updateView() { - super.updateView() - //we want to overwrite the VDS color that is set in the ToggleBase - //for surface since the Atomic controls doesn't look at - //surface today for its views. We just want to show whatever - //the current parent's background color. - backgroundColor = .clear + /// - parameter shouldToggleAction: Takes a closure that returns a boolean. + /// - parameter didToggleAction: A closure which is executed after the toggle changes states. + public convenience init(shouldToggleAction: ActionBlockConfirmation?, didToggleAction: ActionBlock?) { + self.init(frame: .zero) + self.didToggleAction = didToggleAction + self.shouldToggleAction = shouldToggleAction } - open func viewModelDidUpdate() { - guard let viewModel else { return } + public required init?(coder: NSCoder) { + super.init(coder: coder) + fatalError("Toggle does not support xib.") + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + public override func updateView(_ size: CGFloat) { + super.updateView(size) - //set properties from the viewModel change that came in - if let accessibilityText = viewModel.accessibilityText { - accessibilityLabelEnabled = accessibilityText - accessibilityLabelDisabled = accessibilityText - } - isAnimated = viewModel.animated + heightConstraint?.constant = Self.getContainerHeight() + widthConstraint?.constant = Self.getContainerWidth() + + knobHeightConstraint?.constant = Self.getKnobHeight() + knobWidthConstraint?.constant = Self.getKnobWidth() + + layer.cornerRadius = Self.getContainerHeight() / 2.0 + knobView.layer.cornerRadius = Self.getKnobHeight() / 2.0 + + changeStateNoAnimation(isOn) + } + + public override func setupView() { + super.setupView() + + isAccessibilityElement = true + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccToggleHint") + accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") + accessibilityTraits = .button - //send toggle.model to the Form - FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + heightConstraint = heightAnchor.constraint(equalToConstant: Self.containerSize.height) + heightConstraint?.isActive = true + + widthConstraint = widthAnchor.constraint(equalToConstant: Self.containerSize.width) + widthConstraint?.isActive = true + + layer.cornerRadius = Self.getContainerHeight() / 2.0 + backgroundColor = containerTintColor.off + + addSubview(knobView) + + knobHeightConstraint = knobView.heightAnchor.constraint(equalToConstant: Self.knobSize.height) + knobHeightConstraint?.isActive = true + knobWidthConstraint = knobView.widthAnchor.constraint(equalToConstant: Self.knobSize.width) + knobWidthConstraint?.isActive = true + knobView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + knobView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor).isActive = true + bottomAnchor.constraint(greaterThanOrEqualTo: knobView.bottomAnchor).isActive = true + + constrainKnobOff() } - public func executeDefaultAction() { - guard let viewModel else { return } - if viewModel.action != nil || viewModel.alternateAction != nil { - var action: ActionModelProtocol? - if isOn { - action = viewModel.action - } else { - action = viewModel.alternateAction ?? viewModel.action - } - if let action { - MVMCoreUIActionHandler.performActionUnstructured(with: action, - sourceModel: viewModel, - additionalData: additionalData, - delegateObject: delegateObject) + public override func reset() { + super.reset() + + backgroundColor = containerTintColor.off + knobView.backgroundColor = knobTintColor.off + accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "Toggle_buttonlabel") + isAnimated = true + didToggleAction = nil + shouldToggleAction = { return true } + } + + class func getContainerWidth() -> CGFloat { + let containerWidth = Self.containerSize.width + return (MFSizeObject(standardSize: containerWidth, standardiPadPortraitSize: CGFloat(Self.containerSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerWidth + } + + class func getContainerHeight() -> CGFloat { + let containerHeight = Self.containerSize.height + return (MFSizeObject(standardSize: containerHeight, standardiPadPortraitSize: CGFloat(Self.containerSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? containerHeight + } + + class func getKnobWidth() -> CGFloat { + let knobWidth = Self.knobSize.width + return (MFSizeObject(standardSize: knobWidth, standardiPadPortraitSize: CGFloat(Self.knobSize.width * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobWidth + } + + class func getKnobHeight() -> CGFloat { + let knobHeight = Self.knobSize.width + return (MFSizeObject(standardSize: knobHeight, standardiPadPortraitSize: CGFloat(Self.knobSize.height * 1.5)))?.getValueBasedOnApplicationWidth() ?? knobHeight + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + + open override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { + super.sendAction(action, to: target, for: event) + toggleAndAction() + } + + open override func sendActions(for controlEvents: UIControl.Event) { + super.sendActions(for: controlEvents) + toggleAndAction() + } + + /// This will toggle the state of the Toggle and execute the actionBlock if provided. + public func toggleAndAction() { + + if let result = shouldToggleAction?(), result { + isOn.toggle() + didToggleAction?() + } + } + + private func changeStateNoAnimation(_ state: Bool) { + + // Hold state in case User wanted isAnimated to remain off. + let isAnimatedState = isAnimated + + isAnimated = false + isOn = state + isAnimated = isAnimatedState + } + + override open func accessibilityActivate() -> Bool { + // Hold state in case User wanted isAnimated to remain off. + guard isUserInteractionEnabled else { return false } + let isAnimatedState = isAnimated + isAnimated = false + sendActions(for: .touchUpInside) + isAnimated = isAnimatedState + return true + } + + //-------------------------------------------------- + // MARK: - UIResponder + //-------------------------------------------------- + + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { + + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint?.constant += PaddingOne + self.layoutIfNeeded() + }) + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent?) { + + knobReformAnimation() + + // Action only occurs of the user lifts up from withing acceptable region of the toggle. + guard let coordinates = touches.first?.location(in: self), + coordinates.x > -20, + coordinates.x < bounds.width + 20, + coordinates.y > -20, + coordinates.y < bounds.height + 20 + else { return } + + sendActions(for: .touchUpInside) + } + + public func touchesCancelled(_ touches: Set, with event: UIEvent) { + + knobReformAnimation() + sendActions(for: .touchCancel) + } + + //-------------------------------------------------- + // MARK: - Animations + //-------------------------------------------------- + + public func setToggleAppearanceFromState() { + + backgroundColor = isEnabled ? isOn ? containerTintColor.on : containerTintColor.off : disabledTintColor.container + knobView.backgroundColor = isEnabled ? isOn ? knobTintColor.on : knobTintColor.off : disabledTintColor.knob + } + + public func knobReformAnimation() { + + if isAnimated { + UIView.animate(withDuration: 0.1, animations: { + self.knobWidthConstraint?.constant = Self.getKnobWidth() + self.layoutIfNeeded() + }, completion: nil) + + } else { + knobWidthConstraint?.constant = Self.getKnobWidth() + layoutIfNeeded() + } + } + + // MARK:- MoleculeViewProtocol + public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + self.delegateObject = delegateObject + + guard let model = model as? ToggleModel else { return } + + FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) + + containerTintColor.on = model.onTintColor.uiColor + containerTintColor.off = model.offTintColor.uiColor + knobTintColor.on = model.onKnobTintColor.uiColor + knobTintColor.off = model.offKnobTintColor.uiColor + isOn = model.selected + changeStateNoAnimation(isOn) + isAnimated = model.animated + isEnabled = model.enabled && !model.readOnly + + if let accessibileString = model.accessibilityText { + accessibilityLabel = accessibileString + } + + if model.action != nil || model.alternateAction != nil { + didToggleAction = { [weak self] in + guard let self = self else { return } + if self.isOn { + if let action = model.action { + MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) + } + } else { + if let action = model.alternateAction ?? model.action { + MVMCoreUIActionHandler.performActionUnstructured(with: action, sourceModel: model, additionalData: additionalData, delegateObject: delegateObject) + } + } } } } - //Return the same height as the internal ToggleBase.toggleContainerSize.height - //since this is a class func, we can't reference it directly - public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return 44 + public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + Self.getContainerHeight() } - } // MARK: - MVMCoreUIViewConstrainingProtocol -extension Toggle: MVMCoreUIViewConstrainingProtocol { +extension Toggle { public func needsToBeConstrained() -> Bool { true } public func horizontalAlignment() -> UIStackView.Alignment { .trailing } } - diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index bcc11d8b..e8d50851 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -13,15 +13,20 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public static var identifier: String = "toggle" - public var backgroundColor: Color? //not used - + public var accessibilityIdentifier: String? + public var backgroundColor: Color? public var selected: Bool = false + public var animated: Bool = true public var enabled: Bool = true public var readOnly: Bool = false - public var animated: Bool = true public var action: ActionModelProtocol? public var alternateAction: ActionModelProtocol? public var accessibilityText: String? + public var onTintColor: Color = Color(uiColor: .mvmGreen) + public var offTintColor: Color = Color(uiColor: .mvmBlack) + public var onKnobTintColor: Color = Color(uiColor: .mvmWhite) + public var offKnobTintColor: Color = Color(uiColor: .mvmWhite) + public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? @@ -29,16 +34,22 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- - + private enum CodingKeys: String, CodingKey { case moleculeName case state + case animated case enabled case readOnly - case animated case action + case backgroundColor + case accessibilityIdentifier case alternateAction case accessibilityText + case onTintColor + case offTintColor + case onKnobTintColor + case offKnobTintColor case fieldKey case groupName } @@ -64,7 +75,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public init(_ state: Bool) { - selected = state + self.selected = state baseValue = state } @@ -76,31 +87,62 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { - selected = state + self.selected = state } + + if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) { + self.animated = animated + } + action = try typeContainer.decodeModelIfPresent(codingKey: .action) alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + + if let onTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onTintColor) { + self.onTintColor = onTintColor + } + + if let offTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offTintColor) { + self.offTintColor = offTintColor + } + + if let onKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onKnobTintColor) { + self.onKnobTintColor = onKnobTintColor + } + + if let offKnobTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .offKnobTintColor) { + self.offKnobTintColor = offKnobTintColor + } + accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) + baseValue = selected fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) ?? FormValidator.defaultGroupName + if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + self.groupName = groupName + } enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) ?? true - } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(alternateAction, forKey: .alternateAction) - try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(selected, forKey: .state) + try container.encode(animated, forKey: .animated) try container.encode(enabled, forKey: .enabled) + try container.encode(onTintColor, forKey: .onTintColor) + try container.encode(onKnobTintColor, forKey: .onKnobTintColor) + try container.encode(onKnobTintColor, forKey: .onKnobTintColor) + try container.encode(offKnobTintColor, forKey: .offKnobTintColor) + try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) try container.encode(readOnly, forKey: .readOnly) - try container.encode(animated, forKey: .animated) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 9e5881a4..9ce55537 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -8,7 +8,6 @@ // import MVMCore -import VDS public typealias ActionBlock = () -> () @@ -738,6 +737,36 @@ 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/Protocols/VDSMoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift deleted file mode 100644 index 41349f2c..00000000 --- a/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// VDSMoleculeViewProtocol.swift -// MVMCoreUI -// -// Created by Matt Bruce on 10/19/22. -// Copyright © 2022 Verizon Wireless. All rights reserved. -// - -import Foundation -import MVMCore - -///----------------------------------------------------------------------------- -///MARK: -- VDSMoleculeViewProtocol (Contract between VDS -> Atomic) -///----------------------------------------------------------------------------- -public protocol VDSMoleculeViewProtocol: MoleculeViewProtocol, MVMCoreViewProtocol { - associatedtype ViewModel: MoleculeModelProtocol - var viewModel: ViewModel! { get set } - var delegateObject: MVMCoreUIDelegateObject? { get set } - var additionalData: [AnyHashable: Any]? { get set } - func viewModelDidUpdate() -} - -extension VDSMoleculeViewProtocol { - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - guard let castedModel = model as? ViewModel else { return } - self.delegateObject = delegateObject - self.additionalData = additionalData - viewModel = castedModel - viewModelDidUpdate() - } -} - diff --git a/Scripts/build_aggregate.sh b/Scripts/build_aggregate.sh index 911a4002..84c7f4fb 100755 --- a/Scripts/build_aggregate.sh +++ b/Scripts/build_aggregate.sh @@ -15,10 +15,6 @@ UNIVERSAL_OUTPUTFOLDER="${BUILD_DIR}/universal" # Update to use .xcframework sed -i '' 's|MVMCore.framework \*\/ = {isa.*};|MVMCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MVMCore.xcframework; path = ../SharedFrameworks/MVMCore.xcframework; sourceTree = ""; };|g' ./MVMCoreUI.xcodeproj/project.pbxproj sed -i '' 's/MVMCore.framework/MVMCore.xcframework/g' ./MVMCoreUI.xcodeproj/project.pbxproj - -sed -i '' 's|VDS.framework \*\/ = {isa.*};|VDS.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDS.xcframework; path = ../SharedFrameworks/VDS.xcframework; sourceTree = ""; };|g' ./MVMCoreUI.xcodeproj/project.pbxproj -sed -i '' 's/VDS.framework/VDS.xcframework/g' ./MVMCoreUI.xcodeproj/project.pbxproj - sed -i '' "s|path = \.\.\/SharedFrameworks|path = ${FRAMEWORKS_DIR}|g" ./MVMCoreUI.xcodeproj/project.pbxproj # Build device archive diff --git a/Scripts/download_dependencies.sh b/Scripts/download_dependencies.sh index 87b29d03..cc6bc209 100755 --- a/Scripts/download_dependencies.sh +++ b/Scripts/download_dependencies.sh @@ -18,13 +18,8 @@ if [ ! -d $FRAMEWORKS_DIR ]; then mkdir -p $FRAMEWORKS_DIR fi -./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/MVMCore.xcframework" BPHV_MobileFirst_IOS/com/vzw/hss/myverizon/MVMCore/3.1/MVMCore-3.1-Debug-SNAPSHOT.zip +./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/MVMCore.xcframework" BPHV_MobileFirst_IOS/com/vzw/hss/myverizon/MVMCore/3.0/MVMCore-3.0-Debug-SNAPSHOT.zip -./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDS.xcframework" BPHV_MobileFirst_IOS/com/vzw/hss/myverizon/VDS/1.0/VDS-1.0-Debug-SNAPSHOT.zip - -./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSColorTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSColorTokens.1.0.6.xcframework.zip +./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSColorTokens.xcframework" GVJV_VDS_Maven/%40vds-tokens/ios/VDSColorTokens.1.0.6.xcframework.zip ./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSFormControlsTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSFormControlsTokens.1.0.7.xcframework.zip - -./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSTypographyTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSTypographyTokens.2.0.0.xcframework.zip - diff --git a/Scripts/download_framework.sh b/Scripts/download_framework.sh index 791e9ead..5d602e0a 100755 --- a/Scripts/download_framework.sh +++ b/Scripts/download_framework.sh @@ -48,7 +48,7 @@ fi echo -e "Getting checksums..." echo -e "URL: ${URL}/api/storage/${REMOTEPATH}" JSON=$(curl --header "X-JFrog-Art-Api: ${ARTIFACTORY_APIKEY}" -X GET "${URL}/api/storage/${REMOTEPATH}") -CHECKSUM=$(echo "$JSON" | python3 -c 'import sys, json; print (json.load(sys.stdin)["checksums"]["sha1"])') +CHECKSUM=$(echo "$JSON" | python -c 'import sys, json; print json.load(sys.stdin)["checksums"]["sha1"]') if [[ -z "$CHECKSUM" ]]; then exit_with_error "No Checksum found in json: ${JSON}" fi