diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index e90dff6f..a0e87b6f 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -129,6 +129,8 @@ 187FEB2A2844D2A600BF29C2 /* VDSFormControlsTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */; }; 1D6D258826899B0C00DEBB08 /* ImageButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6D258626899B0B00DEBB08 /* ImageButtonModel.swift */; }; 1D6D258926899B0C00DEBB08 /* ImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6D258726899B0B00DEBB08 /* ImageButton.swift */; }; + 27559EFC27D691D3000836C1 /* ViewMaskingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */; }; + 27577DCD286CA959001EC47E /* MoleculeMaskingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */; }; 279B1569242BBC2F00921D6C /* ActionModelAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B1568242BBC2F00921D6C /* ActionModelAdapter.swift */; }; 27F6B08826051831008529AA /* MoleculeTreeTraversalProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F6B08726051831008529AA /* MoleculeTreeTraversalProtocol.swift */; }; 27F6B08C26052AFF008529AA /* ParentMoleculeModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */; }; @@ -727,6 +729,8 @@ 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = ""; }; 1D6D258626899B0B00DEBB08 /* ImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButtonModel.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift; sourceTree = SOURCE_ROOT; }; 1D6D258726899B0B00DEBB08 /* ImageButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButton.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift; sourceTree = SOURCE_ROOT; }; + 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewMaskingProtocol.swift; sourceTree = ""; }; + 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeMaskingProtocol.swift; sourceTree = ""; }; 279B1568242BBC2F00921D6C /* ActionModelAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModelAdapter.swift; sourceTree = ""; }; 27F6B08726051831008529AA /* MoleculeTreeTraversalProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeTreeTraversalProtocol.swift; sourceTree = ""; }; 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentMoleculeModelProtocol.swift; sourceTree = ""; }; @@ -1234,6 +1238,7 @@ D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */, D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */, 27F6B08B26052AFF008529AA /* ParentMoleculeModelProtocol.swift */, + 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */, ); path = ModelProtocols; sourceTree = ""; @@ -2443,6 +2448,7 @@ D28BA7442481652D00B75CB8 /* TabBarProtocol.swift */, D2B9D0E3265EEE9D0084735C /* MoleculeListProtocol.swift */, 011B58EE23A2AA850085F53C /* ModelProtocols */, + 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -2915,6 +2921,7 @@ D23EA800247EBD6C00D60C34 /* LabelBarButtonItem.swift in Sources */, 01EB368F23609801006832FA /* LabelModel.swift in Sources */, 0A6682AC243531C300AD3CA1 /* Padding.swift in Sources */, + 27559EFC27D691D3000836C1 /* ViewMaskingProtocol.swift in Sources */, AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */, AA37CBD52519072F0027344C /* Stars.swift in Sources */, 942C378E2412F5B60066E45E /* ModalMoleculeStackTemplate.swift in Sources */, @@ -3095,6 +3102,7 @@ D29DF2AA21E7B2F9003B2FB9 /* MVMCoreUIConstants.m in Sources */, EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */, 011D95892404249B000E3791 /* FormHolderModelProtocol.swift in Sources */, + 27577DCD286CA959001EC47E /* MoleculeMaskingProtocol.swift in Sources */, BB54C5202434D92F0038326C /* ListRightVariableButtonAllTextAndLinks.swift in Sources */, 948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */, 013F801923FB4A8E00AD8013 /* UIContentMode+Extension.swift in Sources */, diff --git a/MVMCoreUI/Alerts/MVMCoreAlertObject.h b/MVMCoreUI/Alerts/MVMCoreAlertObject.h index 03748c98..a3a019b3 100644 --- a/MVMCoreUI/Alerts/MVMCoreAlertObject.h +++ b/MVMCoreUI/Alerts/MVMCoreAlertObject.h @@ -62,11 +62,4 @@ typedef void (^TextFieldErrorHandler)(NSArray * _Nonnull fieldErrors); // Will show this alert in it's appropriate type style. - (void)showAlert; -#pragma mark - Deprecated - -// Creates an alert object for an error with the passed in load object response info -+ (nullable instancetype)alertObjectForLoadObject:(nullable MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)error actionDelegate:(nullable NSObject *)actionDelegate __deprecated; -+ (nullable instancetype)alertObjectForPageType:(nullable NSString *)pageType responseInfo:(nullable NSDictionary *)responseInfo additionalData:(nullable NSDictionary *)additionalData actionDelegate:(nullable NSObject *)actionDelegate __deprecated; -+ (nullable instancetype)alertObjectWithPage:(nullable NSDictionary *)page isGreedy:(BOOL)isGreedy additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate error:(MVMCoreErrorObject *_Nullable *_Nullable)error __deprecated; - @end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertObject.m b/MVMCoreUI/Alerts/MVMCoreAlertObject.m index 8b1bc248..4dfa560c 100644 --- a/MVMCoreUI/Alerts/MVMCoreAlertObject.m +++ b/MVMCoreUI/Alerts/MVMCoreAlertObject.m @@ -195,113 +195,4 @@ } } -#pragma mark - Deprecated - -+ (nullable instancetype)alertObjectForLoadObject:(nullable MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)error actionDelegate:(nullable NSObject *)actionDelegate { - - MVMCoreAlertObject *alert = nil; - if (!error || [ErrorDomainServer isEqualToString:error.domain]) { - alert = [MVMCoreAlertObject alertObjectForPageType:loadObject.pageType responseInfo:loadObject.responseInfoMap additionalData:loadObject.dataForPage actionDelegate:actionDelegate]; - } else { - alert = [[MVMCoreAlertObject alloc] initPopupAlertWithError:error isGreedy:NO]; - } - - // only if actions are empty, then go inside and set OK as default action - if (alert.type == MFAlertTypePopup && alert.actions.count == 0) { - alert.defaultAction = YES; - alert.actions = @[[UIAlertAction actionWithTitle:[MVMCoreGetterUtility hardcodedStringWithKey:HardcodedOK] style:UIAlertActionStyleDefault handler:nil]]; - } - return alert; -} - -+ (nullable instancetype)alertObjectForPageType:(nullable NSString *)pageType responseInfo:(nullable NSDictionary *)responseInfo additionalData:(nullable NSDictionary *)additionalData actionDelegate:(nullable NSObject *)actionDelegate { - - __block MVMCoreAlertObject *alert = [[MVMCoreAlertObject alloc] init]; - alert.title = [responseInfo string:KeyErrorHeading] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorTitle]; - alert.message = [responseInfo string:KeyUserMessage] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorUnableToProcess]; - NSString *messageStyle = [responseInfo stringForKey:KeyMessageStyle]; - if ([ValueTypeFieldErrors isEqualToString:[responseInfo string:KeyType]]) { - - // field errors. - alert.type = MFAlertTypeField; - alert.fieldErrors = [responseInfo array:ValueTypeFieldErrors]; - } else { - - // Check for top alert (persistent or regular). - if ([messageStyle isEqualToString:ValueMessageStyleTopPersistent] || [messageStyle isEqualToString:ValueMessageStyleTop]) { - - alert.topAlertObject = [[MVMCoreTopAlertObject alloc] initWithResponseInfo:responseInfo]; - if ([actionDelegate conformsToProtocol:@protocol(MVMCoreTopAlertDelegateProtocol)]) { - alert.topAlertObject.delegate = (NSObject *)actionDelegate; - } - alert.topAlertObject.pageType = pageType; - alert.type = MFAlertTypeTop; - } else if ([messageStyle isEqualToString:ValueMessageStylePopup]) { - - // Perform a popup. - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - - // Check if we have a popup driven by page object (otherwise by default it will just use response info title message with an OK button). - NSString *pageTypeForPopup = [responseInfo stringForKey:@"popupPageType"]; - [[MVMCoreCache sharedCache] fetchJSONForPageType:pageTypeForPopup queue:nil waitUntilFinished:YES completionHandler:^(NSDictionary * _Nullable jsonDictionary) { - - MVMCoreErrorObject *error = nil; - MVMCoreAlertObject *popupAlert = [MVMCoreAlertObject alertObjectWithPage:jsonDictionary isGreedy:NO additionalData:additionalData delegate:actionDelegate error:&error]; - if (error) { - - // Error, popup page not found for page type. - popupAlert = [[MVMCoreAlertObject alloc] initPopupAlertWithError:error isGreedy:NO]; - } - - if (popupAlert) { - alert = popupAlert; - } - }]; - } else if (messageStyle.length == 0 && pageType) { - - // No message style! - alert.type = MFAlertTypeNone; - } else { - - // Default to popup - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - } - } - if ([actionDelegate conformsToProtocol:@protocol(MVMCoreAlertDelegateProtocol)]) { - alert.alertDelegate = (NSObject *)actionDelegate; - } - return alert; -} - -+ (nullable instancetype)alertObjectWithPage:(nullable NSDictionary *)page isGreedy:(BOOL)isGreedy additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate error:(MVMCoreErrorObject *_Nullable *_Nullable)error { - - MVMCoreAlertObject *alert = [[MVMCoreAlertObject alloc] init]; - alert.title = [page string:KeyTitle] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorTitle]; - alert.pageJson = page; - alert.message = [page string:KeyMessage] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorUnableToProcess]; - alert.isGreedy = isGreedy; - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - - NSArray *actions = [page array:KeyLinks]; - NSMutableArray *actionsForAlert = [NSMutableArray array]; - for (NSDictionary *actionMap in actions) { - [actionsForAlert addObject:[UIAlertAction actionWithTitle:[actionMap stringForKey:KeyTitle] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [[MVMCoreActionHandler sharedActionHandler] handleActionWithDictionary:actionMap additionalData:additionalData delegate:delegate]; - }]]; - } - alert.actions = actionsForAlert; - - if ((alert.title.length > 0 || alert.message.length > 0) && alert.actions.count > 0) { - return alert; - } else { - if (error) { - *error = [[MVMCoreErrorObject alloc] initWithTitle:nil messageToLog:[MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorUnableToProcess] code:ErrorCodePopupFailed domain:ErrorDomainNative location:[NSString stringWithFormat:@"%@_Popup_pageType:%@",NSStringFromClass([delegate class]),[page stringForKey:KeyPageType]]]; - } - return nil; - } -} - @end diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift index af507860..38bcb491 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -32,6 +32,8 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode public var inverted = false public var size:linkFontSize = linkFontSize.small + public var shouldMaskRecordedView: Bool? = false + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -61,6 +63,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode case activeColor_inverted case inverted case size + case shouldMaskRecordedView } public enum linkFontSize: String, Codable { @@ -117,7 +120,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode if let disabledColor_inverted = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor_inverted) { self.disabledColor_inverted = disabledColor_inverted } - + if let activeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .activeColor) { self.activeColor = activeColor } @@ -128,6 +131,8 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode if let size = try typeContainer.decodeIfPresent(linkFontSize.self, forKey: .size) { self.size = size } + + shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? shouldMaskRecordedView } public func encode(to encoder: Encoder) throws { @@ -146,5 +151,6 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode try container.encode(activeColor, forKey: .activeColor) try container.encode(activeColor_inverted, forKey: .activeColor_inverted) try container.encodeIfPresent(size, forKey: .size) + try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index 962580a6..c3134e19 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -42,7 +42,8 @@ import Foundation public var wasInitiallySelected: Bool = false public var title: String? public var feedback: String? - + public var shouldMaskRecordedView: Bool? = true + //used to drive the EntryFieldView UI public var titleStateLabel: FormLabelModel public var feedbackStateLabel: FormLabelModel @@ -79,6 +80,7 @@ import Foundation case fieldKey case groupName case required + case shouldMaskRecordedView } //-------------------------------------------------- @@ -143,6 +145,7 @@ import Foundation hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) ?? false baseValue = text fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) + shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? shouldMaskRecordedView if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { self.groupName = groupName } @@ -172,5 +175,6 @@ import Foundation try container.encode(enabled, forKey: .enabled) try container.encode(required, forKey: .required) try container.encode(hideBorders, forKey: .hideBorders) + try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index d081b717..07260ac1 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -337,12 +337,16 @@ import UIKit text = model.text placeholder = model.placeholder + textField.shouldMaskWhileRecording = model.shouldMaskRecordedView ?? true + switch model.type { case .password, .secure: textField.isSecureTextEntry = true + textField.shouldMaskWhileRecording = true case .numberSecure: textField.isSecureTextEntry = true + textField.shouldMaskWhileRecording = true textField.keyboardType = .numberPad case .number: diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift index 852cd5c1..b9cac95c 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift @@ -23,6 +23,7 @@ import MVMCore public var baseValue: AnyHashable? public var enabled: Bool = true public var readOnly: Bool = false + //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift index 65bc978c..03c18491 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift @@ -26,6 +26,7 @@ public var localBundle: Bundle? public var cornerRadius: CGFloat? public var clipsImage: Bool? + public var shouldMaskRecordedView: Bool? = false //-------------------------------------------------- // MARK: - Initializer @@ -54,5 +55,6 @@ case contentMode case cornerRadius case clipsImage + case shouldMaskRecordedView } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 7641b8f6..91bf44b8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -12,7 +12,8 @@ import MVMCore public typealias ActionBlock = () -> () -@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol { +@objcMembers open class Label: UILabel, MVMCoreViewProtocol, MoleculeViewProtocol, MVMCoreUIViewConstrainingProtocol, MFButtonProtocol, ViewMaskingProtocol { + //------------------------------------------------------ // MARK: - Properties //------------------------------------------------------ @@ -41,6 +42,8 @@ public typealias ActionBlock = () -> () NSRange(location: 0, length: text?.count ?? 0) } + public var shouldMaskWhileRecording: Bool = false + //------------------------------------------------------ // MARK: - Multi-Action Text //------------------------------------------------------ @@ -244,6 +247,7 @@ public typealias ActionBlock = () -> () text = nil attributedText = nil originalAttributedString = nil + shouldMaskWhileRecording = model.shouldMaskRecordedView ?? false guard let labelModel = model as? LabelModel else { return } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index 2eec132f..40740194 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -26,6 +26,7 @@ public var hero: Int? public var makeWholeViewClickable: Bool? public var numberOfLines: Int? + public var shouldMaskRecordedView: Bool? = false //-------------------------------------------------- // MARK: - Keys @@ -46,6 +47,7 @@ case hero case makeWholeViewClickable case numberOfLines + case shouldMaskRecordedView } enum AttributeTypeKey: String, CodingKey { @@ -79,6 +81,7 @@ hero = try typeContainer.decodeIfPresent(Int.self, forKey: .hero) 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 } open func encode(to encoder: Encoder) throws { @@ -97,5 +100,6 @@ try container.encodeIfPresent(hero, forKey: .hero) try container.encodeIfPresent(makeWholeViewClickable, forKey: .makeWholeViewClickable) try container.encodeIfPresent(numberOfLines, forKey: .numberOfLines) + try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index 8d8ef49e..f3d381bc 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -22,7 +22,7 @@ public var addSizeConstraintsForAspectRatio = true public var shouldNotifyDelegateOnUpdate = true public var shouldNotifyDelegateOnDefaultSizeChange = false - + // Allows for a view to hardcode which height to use if there is none in the json. var imageWidth: CGFloat? var imageHeight: CGFloat? diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift index d4edfd38..ce27740b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift @@ -41,7 +41,7 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol { } super.setDefaults() button.style = .secondary - button.size = .tiny + button.size = .small } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift index 9d061f76..d1c7cf48 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift @@ -51,9 +51,7 @@ open class HeaderView: Container { open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) guard let headerModel = headerModel else { return } - if let lineModel = headerModel.line { - line.set(with: lineModel, delegateObject, additionalData) - } + line.setOptional(with: headerModel.line, delegateObject, additionalData) } open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/HeaderModel.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/HeaderModel.swift index 08c7343f..df1045c2 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/HeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/HeaderModel.swift @@ -30,9 +30,6 @@ if bottomPadding == nil { bottomPadding = PaddingDefaultVerticalSpacing } - if line == nil { - line = LineModel(type: .heavy) - } } public override init() { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeMaskingProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeMaskingProtocol.swift new file mode 100644 index 00000000..b9e9997e --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeMaskingProtocol.swift @@ -0,0 +1,17 @@ +// +// MaskedMoleculeProtocol.swift +// MVMCoreUI +// +// Created by Kyle on 6/29/22. +// Copyright © 2022 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol MoleculeMaskingProtocol { + var shouldMaskRecordedView: Bool? { get } +} + +public extension MoleculeMaskingProtocol { + var shouldMaskRecordedView: Bool? { return false } +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index df3b0dca..1f1fdc1e 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -5,7 +5,7 @@ public enum MolecularError: Swift.Error { } -public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol { +public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol { var moleculeName: String { get } var backgroundColor: Color? { get set } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/PageModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/PageModelProtocol.swift index cfad22df..122adcee 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/PageModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/PageModelProtocol.swift @@ -13,4 +13,5 @@ public protocol PageModelProtocol { var screenHeading: String? { get set } var backgroundColor: Color? { get set } var navigationBar: (NavigationItemModelProtocol & MoleculeModelProtocol)? { get set } + var shouldMaskScreenWhileRecording: Bool? { get } } diff --git a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift index 082f2da3..144001bc 100644 --- a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift @@ -41,6 +41,9 @@ public extension TemplateProtocol { 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 { diff --git a/MVMCoreUI/Atomic/Protocols/ViewMaskingProtocol.swift b/MVMCoreUI/Atomic/Protocols/ViewMaskingProtocol.swift new file mode 100644 index 00000000..e8004790 --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ViewMaskingProtocol.swift @@ -0,0 +1,15 @@ +// +// ViewMaskingProtocol.swift +// MVMCoreUI +// +// Created by Kyle on 3/7/22. +// Copyright © 2022 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol ViewMaskingProtocol: UIView { + + var shouldMaskWhileRecording: Bool { get } + +} diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index 21024474..daa3aed1 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -30,6 +30,8 @@ import Foundation public var tabBarHidden: Bool = false public var tabBarIndex: Int? + + public var shouldMaskScreenWhileRecording: Bool? //-------------------------------------------------- // MARK: - Initializer @@ -53,6 +55,7 @@ import Foundation case navigationBar case tabBarHidden case tabBarIndex + case shouldMaskScreenWhileRecording } //-------------------------------------------------- @@ -77,6 +80,7 @@ import Foundation self.tabBarHidden = tabBarHidden } tabBarIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .tabBarIndex) + shouldMaskScreenWhileRecording = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskScreenWhileRecording) } open func encode(to encoder: Encoder) throws { @@ -89,5 +93,6 @@ import Foundation try container.encodeModelIfPresent(navigationBar, forKey: .navigationBar) try container.encode(tabBarHidden, forKey: .tabBarHidden) try container.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex) + try container.encode(shouldMaskScreenWhileRecording, forKey: .shouldMaskScreenWhileRecording) } } diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index b1128d8e..6188aeae 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -46,7 +46,7 @@ open override func viewForTop() -> UIView? { guard let headerModel = templateModel?.header, - let molecule = ModelRegistry.createMolecule(headerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: headerModel) else { return super.viewForTop() } // Temporary, Default the horizontal padding @@ -59,7 +59,7 @@ override open func viewForBottom() -> UIView? { guard let footerModel = templateModel?.footer, - let molecule = ModelRegistry.createMolecule(footerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: footerModel) else { return super.viewForBottom() } return molecule @@ -124,7 +124,9 @@ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath) (cell as? MoleculeViewProtocol)?.reset() - (cell as? MoleculeViewProtocol)?.set(with: moleculeInfo.molecule, delegateObjectIVar, nil) + if let molecularCell = cell as? MoleculeViewProtocol { + updateMoleculeView(molecularCell, from: moleculeInfo.molecule) + } update(cell: cell, size: view.frame.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells cell.layoutIfNeeded() diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index d797038d..cace752a 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -58,7 +58,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func viewForTop() -> UIView { guard let headerModel = templateModel?.header, - let molecule = ModelRegistry.createMolecule(headerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: headerModel) else { return super.viewForTop() } // Temporary, Default the horizontal padding @@ -71,7 +71,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol override open func viewForBottom() -> UIView { guard let footerModel = templateModel?.footer, - let molecule = ModelRegistry.createMolecule(footerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: footerModel) else { return super.viewForBottom() } return molecule @@ -140,7 +140,9 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol (cell as? MoleculeViewProtocol)?.reset() (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath) - (cell as? MoleculeViewProtocol)?.set(with: moleculeInfo.molecule, delegateObjectIVar, nil) + if let moleculeView = cell as? MoleculeViewProtocol { + updateMoleculeView(moleculeView, from: moleculeInfo.molecule) + } (cell as? MVMCoreViewProtocol)?.updateView(tableView.bounds.width) // Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells cell.layoutIfNeeded() diff --git a/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift index 61b247fb..8ef02f39 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeStackTemplate.swift @@ -54,7 +54,7 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol { open override func viewForTop() -> UIView? { guard let headerModel = templateModel?.header, - let molecule = ModelRegistry.createMolecule(headerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: headerModel) else { return nil } return molecule @@ -73,13 +73,13 @@ open class MoleculeStackTemplate: ThreeLayerViewController, TemplateProtocol { stackItem.useHorizontalMargins = true } - stack.set(with: moleculeStackModel, delegateObject() as? MVMCoreUIDelegateObject, nil) + updateMoleculeView(stack, from: moleculeStackModel) return stack } override open func viewForBottom() -> UIView? { guard let footerModel = templateModel?.footer, - let molecule = ModelRegistry.createMolecule(footerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: footerModel) else { return nil } return molecule diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift index 4d1a4a66..933d6043 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift @@ -27,7 +27,7 @@ import UIKit open override func viewForTop() -> UIView? { guard let headerModel = templateModel?.header, - let molecule = ModelRegistry.createMolecule(headerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: headerModel) else { return nil } return molecule @@ -35,7 +35,7 @@ import UIKit open override func viewForMiddle() -> UIView? { guard let middleModel = templateModel?.middle, - let molecule = ModelRegistry.createMolecule(middleModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: middleModel) else { return nil } return molecule @@ -43,7 +43,7 @@ import UIKit override open func viewForBottom() -> UIView? { guard let footerModel = templateModel?.footer, - let molecule = ModelRegistry.createMolecule(footerModel, delegateObject: delegateObjectIVar) + let molecule = generateMoleculeView(from: footerModel) else { return nil } return molecule diff --git a/MVMCoreUI/BaseClasses/TextField.swift b/MVMCoreUI/BaseClasses/TextField.swift index 260491dd..306b363f 100644 --- a/MVMCoreUI/BaseClasses/TextField.swift +++ b/MVMCoreUI/BaseClasses/TextField.swift @@ -13,7 +13,8 @@ public protocol TextInputDidDeleteProtocol: AnyObject { } -@objcMembers open class TextField: UITextField { +@objcMembers open class TextField: UITextField, ViewMaskingProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -23,6 +24,8 @@ public protocol TextInputDidDeleteProtocol: AnyObject { /// Set to true to hide the blinking textField cursor. public var hideBlinkingCaret = false + public var shouldMaskWhileRecording: Bool = true + //-------------------------------------------------- // MARK: - Delegate //-------------------------------------------------- @@ -97,6 +100,8 @@ extension TextField: MoleculeViewProtocol { if let accessibilityIdentifier = model.accessibilityIdentifier { self.accessibilityIdentifier = accessibilityIdentifier } + + shouldMaskWhileRecording = model.shouldMaskRecordedView ?? true } open func reset() { diff --git a/MVMCoreUI/BaseClasses/View.swift b/MVMCoreUI/BaseClasses/View.swift index c4c36ae8..d7b1cd06 100644 --- a/MVMCoreUI/BaseClasses/View.swift +++ b/MVMCoreUI/BaseClasses/View.swift @@ -9,7 +9,8 @@ import UIKit -@objcMembers open class View: UIView, MoleculeViewProtocol { +@objcMembers open class View: UIView, MoleculeViewProtocol, ViewMaskingProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +19,10 @@ import UIKit private var initialSetupPerformed = false + public var shouldMaskWhileRecording: Bool { + return model?.shouldMaskRecordedView ?? false + } + //-------------------------------------------------- // MARK: - Initialization //-------------------------------------------------- diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index be917ab2..c40548b3 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -246,6 +246,27 @@ import MVMCore manager?.newDataReceived?(in: self) } + public func generateMoleculeView(from model: MoleculeModelProtocol) -> MoleculeViewProtocol? { + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + behavior.willSetupMolecule(with: model, updating: nil) + } + guard let moleculeView = ModelRegistry.createMolecule(model, delegateObject: delegateObjectIVar) else { return nil } + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + behavior.didSetupMolecule(view: moleculeView, withModel: model) + } + return moleculeView + } + + public func updateMoleculeView(_ view: MoleculeViewProtocol, from model: MoleculeModelProtocol) { + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + behavior.willSetupMolecule(with: model, updating: view) + } + view.set(with: model, delegateObjectIVar, nil) + executeBehaviors { (behavior: PageMoleculeTransformationBehavior) in + behavior.didSetupMolecule(view: view, withModel: model) + } + } + //-------------------------------------------------- // MARK: - Navigation Item //-------------------------------------------------- @@ -322,6 +343,14 @@ import MVMCore } } + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in + behavior.willShowPage(self?.delegateObjectIVar) + } + } + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -330,6 +359,14 @@ import MVMCore } } + open override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in + behavior.willHidePage(self?.delegateObjectIVar) + } + } + open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift index 79f3364f..971a052b 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorHandlerProtocol.swift @@ -14,12 +14,17 @@ public protocol PageBehaviorHandlerProtocol { public extension PageBehaviorHandlerProtocol { /// Creates the behaviors and sets the variable. mutating func createBehaviors(for model: PageBehaviorHandlerModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { - guard let behaviorModels = model.behaviors else { + + behaviors = behaviors?.filter { $0.transcendsPageUpdates } + if behaviors?.isEmpty ?? false { behaviors = nil + } + + guard let behaviorModels = model.behaviors else { return } - var behaviors: [PageBehaviorProtocol] = [] + var behaviors: [PageBehaviorProtocol] = behaviors ?? [] for behaviorModel in behaviorModels { do { diff --git a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift index eadd5e4e..453041c2 100644 --- a/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift +++ b/MVMCoreUI/Behaviors/Protocols/PageBehaviorProtocol.swift @@ -9,25 +9,50 @@ public protocol PageBehaviorProtocol: ModelHandlerProtocol { + /// Should the behavior persist regardless of page behavior model updates. + var transcendsPageUpdates: Bool { get } + /// Initializes the behavior with the model init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) } +public extension PageBehaviorProtocol { + var transcendsPageUpdates: Bool { return false } +} + /** Behavior conforming protocols. Behaviors will conform to one or more of these protocols to receive page lifecycle events that pertain to them. */ public protocol PageMoleculeTransformationBehavior: PageBehaviorProtocol { - func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) + func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?) + func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) +} + +public extension PageMoleculeTransformationBehavior { + // All optional. + func onPageNew(rootMolecules: [MoleculeModelProtocol], _ delegateObject: MVMCoreUIDelegateObject?) {} + func willSetupMolecule(with model: MoleculeModelProtocol, updating view: MoleculeViewProtocol?) {} + func didSetupMolecule(view: MoleculeViewProtocol, withModel: MoleculeModelProtocol) {} } public protocol PageVisibilityBehavior: PageBehaviorProtocol { + func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) + func willHidePage(_ delegateObject: MVMCoreUIDelegateObject?) func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) } +public extension PageVisibilityBehavior { + // All optional. + func willShowPage(_ delegateObject: MVMCoreUIDelegateObject?) {} + func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) {} + func willHidePage(_ delegateObject: MVMCoreUIDelegateObject?) {} + func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) {} +} + public protocol PageScrolledBehavior: PageBehaviorProtocol { func pageScrolled(scrollView: UIScrollView, _ delegateObject: MVMCoreUIDelegateObject?) diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.h b/MVMCoreUI/OtherHandlers/MVMCoreUISession.h index ca2db26e..a81c8ee2 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.h @@ -13,6 +13,7 @@ @class MFViewController; @class MFLoadingViewController; @class NavigationController; + NS_ASSUME_NONNULL_BEGIN @interface MVMCoreUISession : MVMCoreSessionObject @@ -25,18 +26,21 @@ NS_ASSUME_NONNULL_BEGIN /// Tracks the current page type the user is currently viewing. KVO compliant. @property (nonatomic, strong, nullable) NSString *currentPageType; -// for handscroll Animation on subclasses of MFScrollingViewController +/// for handscroll Animation on subclasses of MFScrollingViewController @property (assign, nonatomic) BOOL enableHandScrollAnimation; -//indicates if the app launched successfully +/// indicates if the app launched successfully @property (assign, nonatomic) BOOL launchAppLoadedSuccessfully; -// Allows a global overload of the title view of navigation item. +/// Allows a global overload of the title view of navigation item. - (nullable UIView *)titleViewForController:(nonnull MFViewController *)controller; -// Sets up the session as delegate for standard load view controller. Pass the view controller that will be used to present and will be disabled when load view is presented. +/// Sets up the session as delegate for standard load view controller. Pass the view controller that will be used to present and will be disabled when load view is presented. - (void)setupAsStandardLoadViewDelegate:(nonnull UIViewController *)mainViewController; +/// Applies additional behaviors to a controller according to the session. Allows for packages to add additional cross cutting concerns. +- (void)applyGlobalBehaviorsToController:(nonnull UIViewController *)viewController; + @end NS_ASSUME_NONNULL_END diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m index 433b1ab1..957b5c19 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -62,4 +62,8 @@ self.mainViewController.view.userInteractionEnabled = YES; } +- (void)applyGlobalBehaviorsToController:(nonnull UIViewController *)viewController { + // Allow extending frameworks to apply behaviors to add cross cutting concerns to the base controllers. +} + @end