diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index e6626bd7..ec7e68b0 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -91,6 +91,12 @@ 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift */; }; 0A7EF86523D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */; }; 0A7EF86723D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */; }; + 0A9D091D2433796500D2E6C0 /* BarsCarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */; }; + 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */; }; + 0A9D091F2433796500D2E6C0 /* NumericIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */; }; + 0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */; }; + 0A9D09212433796500D2E6C0 /* CarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */; }; + 0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */; }; 0AA33B3A2398524F0067DD0F /* Toggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA33B392398524F0067DD0F /* Toggle.swift */; }; 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */; }; 0AB764D324460FA400E7FE72 /* UIPickerView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB764D224460FA400E7FE72 /* UIPickerView+Extension.swift */; }; @@ -480,6 +486,7 @@ 0A6682B4243769C700AD3CA1 /* TextViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewModel.swift; sourceTree = ""; }; 0A69F610241BDEA700F7231B /* RuleAnyRequiredModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyRequiredModel.swift; sourceTree = ""; }; 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = ""; }; + 0A7918F423F5E7EA00772FF4 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = ""; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxLabel.swift; sourceTree = ""; }; @@ -494,6 +501,12 @@ 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDropdownEntryFieldModel.swift; sourceTree = ""; }; 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateDropdownEntryFieldModel.swift; sourceTree = ""; }; 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitBox.swift; sourceTree = ""; }; + 0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsCarouselIndicatorModel.swift; sourceTree = ""; }; + 0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericCarouselIndicatorModel.swift; sourceTree = ""; }; + 0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericIndicatorView.swift; sourceTree = ""; }; + 0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsIndicatorView.swift; sourceTree = ""; }; + 0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselIndicatorModel.swift; sourceTree = ""; }; + 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = ""; }; 0AA33B33239813C50067DD0F /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; 0AA33B392398524F0067DD0F /* Toggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toggle.swift; sourceTree = ""; }; 0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDatePicker+Extension.swift"; sourceTree = ""; }; @@ -891,6 +904,19 @@ path = Protocols; sourceTree = ""; }; + 0A9D09162433796500D2E6C0 /* CarouselIndicator */ = { + isa = PBXGroup; + children = ( + 0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */, + 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */, + 0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */, + 0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */, + 0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */, + 0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */, + ); + path = CarouselIndicator; + sourceTree = ""; + }; 0ABD1369237B18EE0081388D /* Views */ = { isa = PBXGroup; children = ( @@ -1012,13 +1038,6 @@ path = Label; sourceTree = ""; }; - 94FB5B83238D892800EB2193 /* Recovered References */ = { - isa = PBXGroup; - children = ( - ); - name = "Recovered References"; - sourceTree = ""; - }; AA4FC2A323F4F69600E251DB /* RightVariable */ = { isa = PBXGroup; children = ( @@ -1350,7 +1369,6 @@ D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */, D29DF0CD21E404D4003B2FB9 /* Products */, D29DF0E421E4F3C7003B2FB9 /* Frameworks */, - 94FB5B83238D892800EB2193 /* Recovered References */, ); sourceTree = ""; }; @@ -1570,6 +1588,7 @@ D29DF17D21E69E26003B2FB9 /* Views */ = { isa = PBXGroup; children = ( + 0A9D09162433796500D2E6C0 /* CarouselIndicator */, 9445890B2385BCE300DE9FD4 /* ProgressBarModel.swift */, 01509D922327ECFB00EF99AA /* ProgressBar.swift */, 9445890D2385C3F800DE9FD4 /* MultiProgressModel.swift */, @@ -1740,6 +1759,7 @@ D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */, D264FAA92440F97600D98315 /* CollectionView.swift */, 0A5D59C323AD488600EFD9E9 /* Protocols */, + 0A7918F423F5E7EA00772FF4 /* ImageView.swift */, ); path = BaseClasses; sourceTree = ""; @@ -1926,6 +1946,7 @@ D21B7F602437C5BC00051ABF /* MoleculeStackView.swift in Sources */, 0A6682A42434DB8D00AD3CA1 /* ListLeftVariableRadioButtonBodyTextModel.swift in Sources */, AA2AD116244EE46800BBFFE3 /* ListDeviceComplexLinkMedium.swift in Sources */, + 0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */, D2E2A99423D8CCBC000B42E6 /* HeadlineBodyLinkModel.swift in Sources */, 01004F3022721C3800991ECC /* RadioButton.swift in Sources */, D268C70E238C22D7007F2C1C /* DropDownFilterTableViewCell.swift in Sources */, @@ -1985,6 +2006,7 @@ D29DF27621E79E81003B2FB9 /* MVMCoreUILoggingHandler.m in Sources */, C695A69623C990BC00BFB94E /* DoughnutChart.swift in Sources */, 014AA72D23C5059B006F3E93 /* StackPageTemplateModel.swift in Sources */, + 0A9D091F2433796500D2E6C0 /* NumericIndicatorView.swift in Sources */, D260106123D0C02A00764D80 /* StackItemModelProtocol.swift in Sources */, 0AE98BAF23FEF956004C5109 /* ExternalLink.swift in Sources */, 012A88C4238D86E600FE3DA1 /* CarouselItemModelProtocol.swift in Sources */, @@ -2022,9 +2044,11 @@ D2A514672213885800345BFB /* MoleculeHeaderView.swift in Sources */, D29E28D823D21AB800ACEA85 /* StringAndMoleculeView.swift in Sources */, 01EB369023609801006832FA /* MoleculeListItemModel.swift in Sources */, - D28A838323CCBD3F00DFE4FC /* WheelModel.swift in Sources */, + 0A9D09212433796500D2E6C0 /* CarouselIndicatorModel.swift in Sources */, EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */, + D28A838323CCBD3F00DFE4FC /* WheelModel.swift in Sources */, D268C70C2386DFFD007F2C1C /* MoleculeStackItemModel.swift in Sources */, + 0A9D091D2433796500D2E6C0 /* BarsCarouselIndicatorModel.swift in Sources */, DBEFFA04225A829700230692 /* Label.swift in Sources */, D2D6CD4022E78C1A00D701B8 /* Scroller.swift in Sources */, 0A7ECC5D243CE85300C828E8 /* DoughnutChartItemModel.swift in Sources */, @@ -2061,6 +2085,7 @@ D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */, D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */, D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */, + 0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */, D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */, D260105D23D0BCD400764D80 /* Stack.swift in Sources */, 0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */, @@ -2078,7 +2103,7 @@ 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */, 011B58F023A2AA980085F53C /* ListItemModelProtocol.swift in Sources */, D22479962316AF6E003FCCF9 /* HeadlineBodyLink.swift in Sources */, - 8DE5BECD2456F7A200772E76 /* ListTwoColumnDropdownSelectorsModel.swift in Sources */, + 8DE5BECD2456F7A200772E76 /* ListTwoColumnDropdownSelectorsModel.swift in Sources */, 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */, BB55B51D244482C1002001AD /* ListRightVariablePriceChangeBodyText.swift in Sources */, 017BEB382360C6AC0024EF95 /* RadioButtonLabel.swift in Sources */, @@ -2111,6 +2136,7 @@ BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */, D2D90B42240463E100DD6EC9 /* MoleculeHeaderModel.swift in Sources */, 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, + 0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */, D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */, 0A6682B6243769C700AD3CA1 /* TextViewModel.swift in Sources */, 9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index abdc1242..b0e4d54d 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -8,40 +8,69 @@ import UIKit -public enum ButtonStyle: String, Codable { - case primary - case secondary -} +public typealias FacadeElements = (fill: UIColor?, text: UIColor?, border: UIColor?) -public enum ButtonSize: String, Codable { - case standard - case tiny -} public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "button" public var backgroundColor: Color? public var title: String public var action: ActionModelProtocol public var enabled: Bool = true - public var style: ButtonStyle? - public var size: ButtonSize? = .standard - public var fillColor: Color? - public var textColor: Color? - public var borderColor: Color? + public var style: Styler.Button.Style? { + didSet { + guard let style = style else { return } + setFacade(by: style) + } + } + public var size: Styler.Button.Size? = .standard + public var groupName: String = "" + public var inverted: Bool = false + + public lazy var enabledColors: FacadeElements = (fill: enabled_fillColor(), + text: enabled_textColor(), + border: enabled_borderColor()) + + public lazy var disabledColors: FacadeElements = (fill: disabled_fillColor(), + text: disabled_textColor(), + border: disabled_borderColor()) + + public var enabledFillColor: Color? + public var enabledTextColor: Color? + public var enabledBorderColor: Color? + + public var enabledFillColor_inverted: Color? + public var enabledTextColor_inverted: Color? + public var enabledBorderColor_inverted: Color? + public var disabledFillColor: Color? public var disabledTextColor: Color? public var disabledBorderColor: Color? - public var groupName: String = "" - + + public var disabledFillColor_inverted: Color? + public var disabledTextColor_inverted: Color? + public var disabledBorderColor_inverted: Color? + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + public func setValidity(_ valid: Bool, group: FormGroupRule) { enabled = valid updateUI?() } - + /// Temporary binding mechanism for the view to update on enable changes. - public var updateUI: (() -> Void)? - + public var updateUI: ActionBlock? + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public init(with title: String, action: ActionModelProtocol) { self.title = title self.action = action @@ -52,71 +81,185 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW self.action = action style = .secondary } - + public init(primaryButtonWith title: String, action: ActionModelProtocol) { self.title = title self.action = action style = .primary } + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + public func enabled_fillColor() -> UIColor? { + return (inverted ? enabledFillColor_inverted : enabledFillColor)?.uiColor + } + + public func enabled_textColor() -> UIColor? { + return (inverted ? enabledTextColor_inverted : enabledTextColor)?.uiColor + } + + public func enabled_borderColor() -> UIColor? { + return (inverted ? enabledBorderColor_inverted : enabledBorderColor)?.uiColor + } + + public func disabled_fillColor() -> UIColor? { + return (inverted ? disabledFillColor_inverted : disabledFillColor)?.uiColor + } + + public func disabled_textColor() -> UIColor? { + return (inverted ? disabledTextColor_inverted : disabledTextColor)?.uiColor + } + + public func disabled_borderColor() -> UIColor? { + return (inverted ? disabledBorderColor_inverted : disabledBorderColor)?.uiColor + } + + /// Defines the default appearance for the primary style. + func setPrimaryFacade() { + + if enabledFillColor == nil && enabledTextColor == nil { + enabledFillColor = Color(uiColor: .mvmBlack) + enabledTextColor = Color(uiColor: .mvmWhite) + } + + if disabledFillColor == nil && disabledTextColor == nil { + disabledFillColor = Color(uiColor: .mvmCoolGray6) + disabledTextColor = Color(uiColor: .mvmWhite) + } + + enabledFillColor_inverted = Color(uiColor: .mvmWhite) + enabledTextColor_inverted = Color(uiColor: .mvmBlack) + disabledFillColor_inverted = Color(uiColor: .mvmCoolGray6) + disabledTextColor_inverted = Color(uiColor: .mvmBlack) + } + + /// Defines the default appearance for the Secondary style. + func setSecondaryFacade() { + + if enabledTextColor == nil && enabledBorderColor == nil { + enabledTextColor = Color(uiColor: .mvmBlack) + enabledBorderColor = Color(uiColor: .mvmBlack) + } + + if disabledTextColor == nil && disabledBorderColor == nil { + disabledTextColor = Color(uiColor: .mvmCoolGray6) + disabledBorderColor = Color(uiColor: .mvmCoolGray6) + } + + enabledTextColor_inverted = Color(uiColor: .mvmWhite) + enabledBorderColor_inverted = Color(uiColor: .mvmWhite) + disabledTextColor_inverted = Color(uiColor: .mvmCoolGray6) + disabledBorderColor_inverted = Color(uiColor: .mvmCoolGray6) + } + + public func setFacade(by style: Styler.Button.Style) { + + switch style { + case .primary: + setPrimaryFacade() + + case .secondary: + setSecondaryFacade() + } + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor case title + case inverted case action case enabled case style case size + case groupName case fillColor case textColor case borderColor case disabledFillColor case disabledTextColor case disabledBorderColor - case groupName } - + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) - if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { - self.groupName = groupName - } - if let style = try typeContainer.decodeIfPresent(ButtonStyle.self, forKey: .style) { + + if let style = try typeContainer.decodeIfPresent(Styler.Button.Style.self, forKey: .style) { self.style = style + setFacade(by: style) } - if let size = try typeContainer.decodeIfPresent(ButtonSize.self, forKey: .size) { + + if let size = try typeContainer.decodeIfPresent(Styler.Button.Size.self, forKey: .size) { self.size = size } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { self.enabled = enabled } - fillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .fillColor) - textColor = try typeContainer.decodeIfPresent(Color.self, forKey: .textColor) - borderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .borderColor) - disabledFillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledFillColor) - disabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledTextColor) - disabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBorderColor) + + if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) { + self.inverted = inverted + } + + if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + self.groupName = groupName + } + + if let enabledFillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .fillColor) { + self.enabledFillColor = enabledFillColor + } + + if let enabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .textColor) { + self.enabledTextColor = enabledTextColor + } + + if let enabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .borderColor) { + self.enabledBorderColor = enabledBorderColor + } + + if let disabledFillColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledFillColor) { + self.disabledFillColor = disabledFillColor + } + + if let disabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledTextColor) { + self.disabledTextColor = disabledTextColor + } + + if let disabledBorderColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledBorderColor) { + self.disabledBorderColor = disabledBorderColor + } } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(title, forKey: .title) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) - try container.encodeModel(action, forKey: .action) try container.encode(enabled, forKey: .enabled) - try container.encodeIfPresent(style, forKey: .style) - try container.encodeIfPresent(size, forKey: .size) - try container.encodeIfPresent(fillColor, forKey: .fillColor) - try container.encodeIfPresent(textColor, forKey: .textColor) - try container.encodeIfPresent(borderColor, forKey: .borderColor) + try container.encode(inverted, forKey: .inverted) + try container.encodeModel(action, forKey: .action) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(enabledFillColor, forKey: .fillColor) + try container.encodeIfPresent(enabledTextColor, forKey: .textColor) + try container.encodeIfPresent(enabledBorderColor, forKey: .borderColor) try container.encodeIfPresent(disabledFillColor, forKey: .disabledFillColor) try container.encodeIfPresent(disabledTextColor, forKey: .disabledTextColor) try container.encodeIfPresent(disabledBorderColor, forKey: .disabledBorderColor) + try container.encodeIfPresent(style, forKey: .style) + try container.encodeIfPresent(size, forKey: .size) try container.encodeIfPresent(groupName, forKey: .groupName) } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift b/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift index 19f032fc..f5a7ed44 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift @@ -8,80 +8,121 @@ import UIKit + open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { - // Used to size the button. + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + /// Used to size the button. var size = MVMCoreUIUtility.getWidth() var buttonModel: ButtonModel? { get { return model as? ButtonModel } } - // Need to re-style on set. + /// Need to re-style on set. open override var isEnabled: Bool { + didSet { style() } + } + + open var buttonSize: Styler.Button.Size = .standard { didSet { - style() + buttonModel?.size = buttonSize } } - private enum ButtonHeight: CGFloat { - case tiny = 20 - case standard = 42 + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + @objc public convenience init(asPrimaryButton isPrimary: Bool, makeTiny istiny: Bool) { + self.init() + buttonSize = istiny ? .tiny : .standard + isPrimary ? stylePrimary() : styleSecondary() } + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + public var enabledTitleColor: UIColor? { + get { return titleColor(for: .normal) } + set { setTitleColor(newValue, for: .normal) } + } + + public var disabledTitleColor: UIColor? { + get { return titleColor(for: .disabled) } + set { setTitleColor(newValue, for: .disabled) } + } + + public var borderColor: UIColor? { + get { + guard let currentColor = layer.borderColor else { return nil } + return UIColor(cgColor: currentColor) + } + set { layer.borderColor = newValue?.cgColor } + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + /// The primary styling for a button. Should be used for main buttons public func stylePrimary() { - setTitleColor(.white, for: .normal) - setTitleColor(.white, for: .disabled) + + enabledTitleColor = buttonModel?.enabledColors.text ?? .mvmWhite + disabledTitleColor = buttonModel?.disabledColors.text ?? .mvmWhite layer.borderWidth = 0 - if isEnabled { - backgroundColor = .black - } else { - backgroundColor = .mvmCoolGray6 - } + backgroundColor = isEnabled ? buttonModel?.enabledColors.fill ?? .mvmBlack : buttonModel?.disabledColors.fill ?? .mvmCoolGray6 } /// The secondary styling for a button. Should be used for secondary buttons public func styleSecondary() { - setTitleColor(.black, for: .normal) - setTitleColor(.mvmCoolGray6, for: .disabled) + + enabledTitleColor = buttonModel?.enabledColors.text ?? .mvmBlack + disabledTitleColor = buttonModel?.disabledColors.text ?? .mvmCoolGray6 backgroundColor = .clear layer.borderWidth = 1 - if isEnabled { - layer.borderColor = UIColor.black.cgColor - } else { - layer.borderColor = UIColor.mvmCoolGray6.cgColor - } + borderColor = isEnabled ? buttonModel?.enabledColors.border ?? .mvmBlack : buttonModel?.disabledColors.border ?? .mvmCoolGray6 } /// Styles the button based on the model style private func style() { + switch buttonModel?.style { case .secondary: styleSecondary() + default: stylePrimary() } - if let titleColor = buttonModel?.textColor { - setTitleColor(titleColor.uiColor, for: .normal) + + if let titleColor = buttonModel?.enabledColors.text { + enabledTitleColor = titleColor } - if let disabledTitleColor = buttonModel?.disabledTextColor { - setTitleColor(disabledTitleColor.uiColor, for: .disabled) + + if let disabledTitleColor = buttonModel?.disabledColors.text { + self.disabledTitleColor = disabledTitleColor } + if isEnabled { - if let fillColor = buttonModel?.fillColor { - backgroundColor = fillColor.uiColor + if let fillColor = buttonModel?.enabledColors.fill { + backgroundColor = fillColor } - if let borderColor = buttonModel?.borderColor { + + if let borderColor = buttonModel?.enabledColors.border { layer.borderWidth = 1 - layer.borderColor = borderColor.cgColor + self.borderColor = borderColor } } else { - if let fillColor = buttonModel?.disabledFillColor { - backgroundColor = fillColor.uiColor + if let fillColor = buttonModel?.disabledColors.fill { + backgroundColor = fillColor } - if let borderColor = buttonModel?.disabledBorderColor { + + if let borderColor = buttonModel?.disabledColors.border { layer.borderWidth = 1 - layer.borderColor = borderColor.cgColor + self.borderColor = borderColor } } } @@ -91,41 +132,58 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { } private func getHeight() -> CGFloat { - PillButton.getHeight(for: buttonModel?.size, size: size) + PillButton.getHeight(for: buttonSize, size: size) } - public static func getHeight(for buttonSize: ButtonSize?, size: CGFloat) -> CGFloat { + public static func getHeight(for buttonSize: Styler.Button.Size?, size: CGFloat) -> CGFloat { + switch buttonSize { case .tiny: - return MFSizeObject(standardSize: ButtonHeight.tiny.rawValue, standardiPadPortraitSize: 34, iPadProLandscapeSize: 38)?.getValueBased(onSize: size) ?? ButtonHeight.tiny.rawValue + let tinyHeight = Styler.Button.Size.tiny.getHeight() + return MFSizeObject(standardSize: tinyHeight, + standardiPadPortraitSize: 34, + iPadProLandscapeSize: 38)?.getValueBased(onSize: size) ?? tinyHeight + default: - return MFSizeObject(standardSize: ButtonHeight.standard.rawValue, standardiPadPortraitSize: 46, iPadProLandscapeSize: 50)?.getValueBased(onSize: size) ?? ButtonHeight.standard.rawValue + let standardHeight = Styler.Button.Size.standard.getHeight() + return MFSizeObject(standardSize: standardHeight, + standardiPadPortraitSize: 46, + iPadProLandscapeSize: 50)?.getValueBased(onSize: size) ?? standardHeight } } - + private func getMinimumWidth() -> CGFloat { - switch buttonModel?.size { + + switch buttonSize { case .tiny: - return MFSizeObject(standardSize: 49.0, standardiPadPortraitSize: 90.0, iPadProLandscapeSize: 135.0)?.getValueBased(onSize: size) ?? 49.0 - default: - return 151.0 + return MFSizeObject(standardSize: 49, + standardiPadPortraitSize: 90, + iPadProLandscapeSize: 135)?.getValueBased(onSize: size) ?? 49 + + default: return 151 } } open override var intrinsicContentSize: CGSize { + let size = super.intrinsicContentSize let width = size.width + (2 * getInnerPadding()) return CGSize(width: max(width, getMinimumWidth()), height: getHeight()) } - // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + // MARK: - MVMCoreViewProtocol + //-------------------------------------------------- + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { // The button will get styled in the enable check in super. super.set(with: model, delegateObject, additionalData) - + guard let model = model as? ButtonModel else { return } setTitle(model.title, for: .normal) - + if let size = model.size { + buttonSize = size + } model.updateUI = { [weak self] in MVMCoreDispatchUtility.performBlock(onMainThread: { self?.enableField(model.enabled) @@ -134,39 +192,46 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) } - + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { PillButton.getHeight(for: (model as? ButtonModel)?.size, size: MVMCoreUIUtility.getWidth()) } - // MARK: - MVMCoreViewProtocol open override func updateView(_ size: CGFloat) { super.updateView(size) self.size = size + invalidateIntrinsicContentSize() - switch buttonModel?.size { + + switch buttonSize { case .tiny: - titleLabel?.font = MFFonts.mfFont75Bd(11 * (intrinsicContentSize.height / ButtonHeight.tiny.rawValue)) + titleLabel?.font = MFFonts.mfFont75Bd(11 * (intrinsicContentSize.height / Styler.Button.Size.tiny.getHeight())) + default: - titleLabel?.font = MFFonts.mfFont75Bd(13 * (intrinsicContentSize.height / ButtonHeight.standard.rawValue)) + titleLabel?.font = MFFonts.mfFont75Bd(13 * (intrinsicContentSize.height / Styler.Button.Size.standard.getHeight())) } + layer.cornerRadius = getInnerPadding() } - + open override func setupView() { super.setupView() + titleLabel?.numberOfLines = 1 titleLabel?.lineBreakMode = .byTruncatingTail titleLabel?.textAlignment = .center contentHorizontalAlignment = .center stylePrimary() } - + + //-------------------------------------------------- // MARK: - MVMCoreUIViewConstrainingProtocol + //-------------------------------------------------- + open func horizontalAlignment() -> UIStackView.Alignment { return .center } - + public func enableField(_ enable: Bool) { isEnabled = enable } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Arrow.swift b/MVMCoreUI/Atomic/Atoms/Views/Arrow.swift index e932dd60..ea5023c8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Arrow.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Arrow.swift @@ -8,6 +8,7 @@ import UIKit + open class Arrow: View { //-------------------------------------------------- // MARK: - Properties @@ -19,6 +20,41 @@ open class Arrow: View { return model as? ArrowModel } + public var direction: ArrowModel.Direction { + get { return ArrowModel.Direction(rawValue: degrees) ?? .right} + set { degrees = newValue.rawValue } + } + + open var isEnabled: Bool = true { + didSet { + arrowModel?.enabled = isEnabled + isUserInteractionEnabled = isEnabled + if isEnabled != oldValue { + setNeedsDisplay() + } + } + } + + open var disabledColor: UIColor { + get { return arrowModel?.disabledColor.uiColor ?? .mvmCoolGray3 } + set { arrowModel?.disabledColor = Color(uiColor: newValue) } + } + + open var color: UIColor { + get { return arrowModel?.color.uiColor ?? .mvmBlack } + set { arrowModel?.color = Color(uiColor: newValue) } + } + + open var degrees: Float { + get { return arrowModel?.degrees ?? 0 } + set { arrowModel?.degrees = newValue } + } + + open var lineWidth: CGFloat { + get { return arrowModel?.lineWidth ?? 1 } + set { arrowModel?.lineWidth = newValue } + } + //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- @@ -34,6 +70,18 @@ open class Arrow: View { widthConstraint?.isActive = true } + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: frame) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- @@ -51,33 +99,32 @@ open class Arrow: View { // MARK: - Drawing //-------------------------------------------------- + /// Draws the arrow pointing to the right and then rotates the arrow x degrees counter-clockwise. open override func draw(_ rect: CGRect) { super.draw(rect) arrowLayer.transform = CATransform3DIdentity drawShapeLayer() - if let degrees = arrowModel?.degrees { - let radians = CGFloat(degrees * Float.pi / 180) - arrowLayer.transform = CATransform3DMakeRotation(-radians, 0.0, 0.0, 1.0) - } + let radians = CGFloat(degrees * Float.pi / 180) + arrowLayer.transform = CATransform3DMakeRotation(-radians, 0, 0, 1) } private func drawShapeLayer() { arrowLayer.frame = bounds - arrowLayer.strokeColor = arrowModel?.color.cgColor + arrowLayer.strokeColor = isEnabled ? color.cgColor : disabledColor.cgColor arrowLayer.fillColor = UIColor.clear.cgColor arrowLayer.path = arrowPath() arrowLayer.lineJoin = .miter arrowLayer.lineCap = .butt - arrowLayer.lineWidth = arrowModel?.lineWidth ?? 1 + arrowLayer.lineWidth = lineWidth } private func arrowPath() -> CGPath { let length = max(bounds.size.height, bounds.size.width) - let inset = (arrowModel?.lineWidth ?? 1) / 2 + let inset = lineWidth / 2 let midLength = length / 2 var startPoint = CGPoint(x: midLength, y: inset) @@ -97,4 +144,16 @@ open class Arrow: View { return bezierPath.cgPath } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + + public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + + guard let model = model as? ArrowModel else { return } + + isEnabled = model.enabled + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift index 2547f8d1..ac2b2d90 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift @@ -8,20 +8,47 @@ import UIKit + open class ArrowModel: MoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public static var identifier: String = "arrow" - public var backgroundColor: Color? + public static var identifier: String { + return "arrow" + } + public var moleculeName: String? + public var backgroundColor: Color? + public var disabledColor: Color = Color(uiColor: .mvmCoolGray3) public var color: Color = Color(uiColor: .mvmBlack) public var degrees: Float = 0 public var lineWidth: CGFloat = 1 - public var height: CGFloat = 12 public var width: CGFloat = 12 + public var enabled: Bool = true + + //-------------------------------------------------- + // MARK: - Enum + //-------------------------------------------------- + + /// Conveniece for readability of arrow pointing direction. + public enum Direction: Float { + case right = 0 + case upperRight = 45 + case up = 90 + case upperLeft = 135 + case left = 180 + case bottomLeft = 225 + case down = 270 + case bottomRight = 315 + } + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init() { } //-------------------------------------------------- // MARK: - Keys @@ -30,12 +57,13 @@ open class ArrowModel: MoleculeModelProtocol { private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case disabledColor case color case degrees - case size case lineWidth case height case width + case enabled } //-------------------------------------------------- @@ -44,12 +72,21 @@ open class ArrowModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + if let disabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor) { + self.disabledColor = disabledColor + } + if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) { self.color = color } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { + self.enabled = enabled + } + if let degrees = try typeContainer.decodeIfPresent(Float.self, forKey: .degrees) { self.degrees = degrees } @@ -59,7 +96,7 @@ open class ArrowModel: MoleculeModelProtocol { } if let height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) { - self.lineWidth = height + self.height = height } if let width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width) { @@ -69,12 +106,14 @@ open class ArrowModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(disabledColor, forKey: .disabledColor) try container.encode(color, forKey: .color) try container.encode(degrees, forKey: .degrees) - try container.encodeIfPresent(backgroundColor, forKey: .lineWidth) + try container.encode(lineWidth, forKey: .lineWidth) try container.encode(width, forKey: .width) try container.encode(height, forKey: .height) + try container.encode(enabled, forKey: .enabled) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsCarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsCarouselIndicatorModel.swift new file mode 100644 index 00000000..1b5cae14 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsCarouselIndicatorModel.swift @@ -0,0 +1,50 @@ +// +// BarsCarouselIndicatorModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 3/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class BarsCarouselIndicatorModel: CarouselIndicatorModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public class override var identifier: String { + return "barsCarouselIndicator" + } + + public var currentIndicatorColor: Color = Color(uiColor: .mvmBlack) + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case currentIndicatorColor + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + public required init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + if let currentIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentIndicatorColor) { + self.currentIndicatorColor = currentIndicatorColor + } + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(currentIndicatorColor, forKey: .currentIndicatorColor) + try super.encode(to: encoder) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift new file mode 100644 index 00000000..972ea7ca --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/BarsIndicatorView.swift @@ -0,0 +1,177 @@ +// +// BarIndicatorView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class BarsIndicatorView: CarouselIndicator { + //-------------------------------------------------- + // MARK: - Stored Properties + //-------------------------------------------------- + + public let stackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.isAccessibilityElement = false + stackView.axis = .horizontal + stackView.alignment = .bottom + stackView.distribution = .equalSpacing + stackView.spacing = 6 + return stackView + }() + + public var barReferences: [(view: View, constraint: NSLayoutConstraint)] = [] + + // Dimensions are based on InVision Design Guidelines. + public static let indicatorBarWidth: CGFloat = 24 + public static let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1) + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + /// Convenience to access the model. + public var barsCarouselIndicatorModel: BarsCarouselIndicatorModel? { + return model as? BarsCarouselIndicatorModel + } + + open override var isEnabled: Bool { + didSet { + for (i, bar) in barReferences.enumerated() { + if i == currentIndex { + bar.view.backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor + } else { + bar.view.backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor + } + } + } + } + + /// Colors the currently selected index, unique from other indicators + public var currentIndicatorColor: UIColor { + get { return barsCarouselIndicatorModel?.currentIndicatorColor.uiColor ?? indicatorColor } + set (newColor) { + barsCarouselIndicatorModel?.currentIndicatorColor = Color(uiColor: newColor) + + if isEnabled && !barReferences.isEmpty { + barReferences[currentIndex].view.backgroundColor = newColor + } + } + } + + public override var indicatorColor: UIColor { + get { return super.indicatorColor } + set (newColor) { + super.indicatorColor = newColor + + if isEnabled { + for (i, barTuple) in barReferences.enumerated() { + barTuple.view.backgroundColor = i == currentIndex ? currentIndicatorColor : newColor + } + } + } + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + open override func setupView() { + super.setupView() + + addSubview(stackView) + isUserInteractionEnabled = false + isAccessibilityElement = false + + NSLayoutConstraint.activate([ + stackView.heightAnchor.constraint(equalToConstant: 4), + heightAnchor.constraint(equalTo: stackView.heightAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + trailingAnchor.constraint(equalTo: stackView.trailingAnchor) + ]) + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + func generateBars() { + + var bars = [(View, NSLayoutConstraint)]() + + let ordinalFormatter = NumberFormatter() + ordinalFormatter.numberStyle = .ordinal + + for i in 0..= touchPoint_X && $0.0.frame.minX <= touchPoint_X } ?? 0 + performAction() + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + + guard let model = model as? BarsCarouselIndicatorModel else { return } + + currentIndicatorColor = model.currentIndicatorColor.uiColor + } + + //-------------------------------------------------- + // MARK: - IndicatorViewProtocol + //-------------------------------------------------- + + public override func reset() { + super.reset() + barReferences.forEach { $0.view.removeFromSuperview() } + barReferences = [] + } + + public override func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { + + guard newIndex < totalCount else { return } + + guard !barReferences.isEmpty else { + generateBars() + return + } + + let expression = { + self.barReferences[previousIndex].view.backgroundColor = self.isEnabled ? self.indicatorColor : self.disabledIndicatorColor + self.barReferences[newIndex].view.backgroundColor = self.isEnabled ? self.currentIndicatorColor : self.disabledIndicatorColor + self.barReferences[previousIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.unselected + self.barReferences[newIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.selected + self.layoutIfNeeded() + } + + isAnimated ? UIView.animate(withDuration: 0.3) { expression() } : expression() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicator.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicator.swift new file mode 100644 index 00000000..9380e8c0 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicator.swift @@ -0,0 +1,245 @@ +// +// CarouselIndicator.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 1/30/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + + +open class CarouselIndicator: Control, CarouselPageControlProtocol { + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + public var topConstraint: NSLayoutConstraint? + public var bottomConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public var uiGestures: Set = [] + + /// Convenience to access the model. + public var carouselIndicatorModel: CarouselIndicatorModel? { + return model as? CarouselIndicatorModel + } + + /// Set this closure to perform an action when a different indicator was selected. + /// Passes through oldIndex and newIndex, respectively. + public var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())? + + open override var isEnabled: Bool { + didSet { isUserInteractionEnabled = isEnabled } + } + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + private(set) var previousIndex = 0 + + public var currentIndex: Int { + get { return carouselIndicatorModel?.currentIndex ?? 0 } + set (newIndex) { + previousIndex = currentIndex + carouselIndicatorModel?.currentIndex = newIndex + + updateUI(previousIndex: previousIndex, + newIndex: newIndex, + totalCount: numberOfPages, + isAnimated: carouselIndicatorModel?.animated ?? true) + } + } + + /// Holds the total number of pages displayed by the carousel. + /// Updating this property will potentially update the UI. + public var numberOfPages: Int { + get { return carouselIndicatorModel?.numberOfPages ?? 0 } + set (newTotal) { + guard numberOfPages != newTotal else { return } + + carouselIndicatorModel?.numberOfPages = newTotal + reset() + isHidden = (carouselIndicatorModel?.hidesForSinglePage ?? false) && newTotal <= 1 + updateUI(previousIndex: previousIndex, + newIndex: currentIndex, + totalCount: newTotal, + isAnimated: carouselIndicatorModel?.animated ?? true) + } + } + + public var disabledIndicatorColor: UIColor { + get { return carouselIndicatorModel?.disabledIndicatorColor.uiColor ?? .mvmCoolGray3 } + set { carouselIndicatorModel?.disabledIndicatorColor = Color(uiColor: newValue) } + } + + public var indicatorColor: UIColor { + get { return carouselIndicatorModel?.indicatorColor.uiColor ?? .mvmBlack } + set { carouselIndicatorModel?.indicatorColor = Color(uiColor: newValue) } + } + + var accessibilityValueFormat: String? { + return MVMCoreUIUtility.hardcodedString(withKey: (carouselIndicatorModel?.accessibilityHasSlidesInsteadOfPage ?? false) ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + override init(frame: CGRect) { + super.init(frame: frame) + } + + convenience override init() { + self.init(frame: .zero) + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func setupView() { + super.setupView() + + isAccessibilityElement = true + accessibilityTraits = .adjustable + setupGestures() + } + + //-------------------------------------------------- + // MARK: - UITouch + //-------------------------------------------------- + + private func setupGestures() { + + let tap = UITapGestureRecognizer(target: self, action: #selector(indicatorTapped)) + let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeLeft)) + let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeRight)) + + leftSwipe.direction = .left + rightSwipe.direction = .right + + addGestureRecognizer(tap) + addGestureRecognizer(leftSwipe) + addGestureRecognizer(rightSwipe) + + uiGestures.insert(tap) + uiGestures.insert(leftSwipe) + uiGestures.insert(rightSwipe) + } + + func incrementCurrentIndex() { + currentIndex = (currentIndex + 1) % numberOfPages + performAction() + } + + func decrementCurrentIndex() { + let newIndex = currentIndex - 1 + currentIndex = newIndex < 0 ? numberOfPages - 1 : newIndex + performAction() + } + + /// Increments the currentIndex value. + @objc func swipeLeft() { + incrementCurrentIndex() + } + + /// Decrement the currentIndex value + @objc func swipeRight() { + decrementCurrentIndex() + } + + /// Handles tap logic for Indicator + @objc func indicatorTapped(_ tapGesture: UITapGestureRecognizer?) { + + let touchPoint = tapGesture?.location(in: self) + let touchPoint_X = touchPoint?.x ?? 0.0 + + assessTouchOf(touchPoint_X) + } + + func assessTouchOf(_ touchPoint_X: CGFloat) { } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + open func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { } + + public func performAction() { + + sendActions(for: .valueChanged) + indicatorTouchAction?(self) + } + + public func scrollViewDidScroll(_ collectionView: UICollectionView) { } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + + guard let model = model as? CarouselIndicatorModel else { return } + + indicatorColor = model.indicatorColor.uiColor + disabledIndicatorColor = model.disabledIndicatorColor.uiColor + currentIndex = model.currentIndex + isEnabled = model.enabled + + formatAccessibilityValue(index: currentIndex + 1, total: numberOfPages) + } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + + open override func accessibilityIncrement() { + + adjustAccessibility(toPage: currentIndex + 1) + } + + open override func accessibilityDecrement() { + + adjustAccessibility(toPage: currentIndex - 1) + } + + func formatAccessibilityValue(index: Int, total: Int) { + + let ordinalFormatter = NumberFormatter() + ordinalFormatter.numberStyle = .ordinal + + guard let accessibleFormat = accessibilityValueFormat, + let accessibleIndex = ordinalFormatter.string(from: NSNumber(value: index)) + else { return } + + accessibilityValue = String(format: accessibleFormat, accessibleIndex, total) + } + + func adjustAccessibility(toPage index: Int) { + + formatAccessibilityValue(index: index, total: numberOfPages) + + if (index < numberOfPages && index >= 0) || carouselIndicatorModel?.alwaysSendAction ?? false { + carouselIndicatorModel?.animated = false + previousIndex = currentIndex + currentIndex = index + performAction() + } + } + + func setTopBottomSpace(constant: CGFloat) { + + bottomConstraint?.constant = constant + topConstraint?.constant = constant + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift new file mode 100644 index 00000000..a0ac2273 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -0,0 +1,119 @@ +// +// CarouselIndicatorModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + + +open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public class var identifier: String { + return "" + } + + public var backgroundColor: Color? + public var moleculeName: String? + public var numberOfPages: Int = 0 + + /// Sets the current Index to focus on. + public var currentIndex: Int = 0 + public var animated: Bool = true + public var hidesForSinglePage: Bool = false + /// Set true to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is false + public var accessibilityHasSlidesInsteadOfPage: Bool = false + public var enabled: Bool = true + public var disabledIndicatorColor: Color = Color(uiColor: .mvmCoolGray3) + public var indicatorColor: Color = Color(uiColor: .mvmBlack) + public var position: Float? + + /// Allows sendActions() to trigger even if index is already at min/max index. + public var alwaysSendAction = false + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case moleculeName + case backgroundColor + case currentIndex + case numberOfPages + case alwaysSendAction + case animated + case hidesForSinglePage + case accessibilityHasSlidesInsteadOfPage + case enabled + case disabledIndicatorColor + case indicatorColor + case position + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + + if let currentIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .currentIndex) { + self.currentIndex = currentIndex + } + + if let alwaysSendAction = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysSendAction) { + self.alwaysSendAction = alwaysSendAction + } + + if let position = try typeContainer.decodeIfPresent(Float.self, forKey: .position) { + self.position = position + } + + if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) { + self.animated = animated + } + + if let hidesForSinglePage = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesForSinglePage) { + self.hidesForSinglePage = hidesForSinglePage + } + + if let accessibilityHasSlidesInsteadOfPage = try typeContainer.decodeIfPresent(Bool.self, forKey: .accessibilityHasSlidesInsteadOfPage) { + self.accessibilityHasSlidesInsteadOfPage = accessibilityHasSlidesInsteadOfPage + } + + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { + self.enabled = enabled + } + + if let disabledIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledIndicatorColor) { + self.disabledIndicatorColor = disabledIndicatorColor + } + + if let indicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .indicatorColor) { + self.indicatorColor = indicatorColor + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encode(numberOfPages, forKey: .numberOfPages) + try container.encode(currentIndex, forKey: .currentIndex) + try container.encode(alwaysSendAction, forKey: .alwaysSendAction) + try container.encode(animated, forKey: .animated) + try container.encode(hidesForSinglePage, forKey: .hidesForSinglePage) + try container.encode(accessibilityHasSlidesInsteadOfPage, forKey: .accessibilityHasSlidesInsteadOfPage) + try container.encode(enabled, forKey: .enabled) + try container.encode(disabledIndicatorColor, forKey: .disabledIndicatorColor) + try container.encode(indicatorColor, forKey: .indicatorColor) + try container.encodeIfPresent(position, forKey: .position) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericCarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericCarouselIndicatorModel.swift new file mode 100644 index 00000000..7739dc12 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericCarouselIndicatorModel.swift @@ -0,0 +1,20 @@ +// +// NumericCarouselIndicatorModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 3/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class NumericCarouselIndicatorModel: CarouselIndicatorModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public class override var identifier: String { + return "numericCarouselIndicator" + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericIndicatorView.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericIndicatorView.swift new file mode 100644 index 00000000..3585be05 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/NumericIndicatorView.swift @@ -0,0 +1,126 @@ +// +// NumericIndicatorView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class NumericIndicatorView: CarouselIndicator { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + /// Text to display the current count of total pages for viewing. + open var pageCount: Label = { + let label = Label.commonLabelB2(true) + label.isAccessibilityElement = false + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.textAlignment = .center + return label + }() + + let leftArrow: Arrow = { + let arrow = Arrow(model: ArrowModel(), nil, nil) + arrow.isAccessibilityElement = false + arrow.direction = .left + arrow.pinHeightAndWidth() + return arrow + }() + + let rightArrow: Arrow = { + let arrow = Arrow(model: ArrowModel(), nil, nil) + arrow.pinHeightAndWidth() + return arrow + }() + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + open override var isEnabled: Bool { + didSet { setViewColor(isEnabled ? indicatorColor : disabledIndicatorColor) } + } + + /// Sets the color for pageCount text, left arrow and right arrow. + public override var indicatorColor: UIColor { + get { return super.indicatorColor } + set (newColor) { + super.indicatorColor = newColor + + if isEnabled { + setViewColor(newColor) + } + } + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + pageCount.updateView(size) + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + open override func setupView() { + super.setupView() + + accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint") + addSubview(pageCount) + addSubview(leftArrow) + addSubview(rightArrow) + + NSLayoutConstraint.activate([ + pageCount.centerXAnchor.constraint(equalTo: centerXAnchor), + pageCount.topAnchor.constraint(equalTo: topAnchor), + bottomAnchor.constraint(equalTo: pageCount.bottomAnchor), + leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor), + rightArrow.centerYAnchor.constraint(equalTo: centerYAnchor), + leftArrow.leadingAnchor.constraint(equalTo: leadingAnchor), + pageCount.leadingAnchor.constraint(equalTo: leftArrow.trailingAnchor, constant: Padding.Two), + rightArrow.leadingAnchor.constraint(equalTo: pageCount.trailingAnchor, constant: Padding.Two), + trailingAnchor.constraint(equalTo: rightArrow.trailingAnchor) + ]) + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + public override func assessTouchOf(_ touchPoint_X: CGFloat) { + + if touchPoint_X > bounds.width / 2 { + incrementCurrentIndex() + } else { + decrementCurrentIndex() + } + } + + private func setViewColor(_ newColor: UIColor) { + + pageCount.textColor = newColor + leftArrow.color = newColor + rightArrow.color = newColor + rightArrow.setNeedsDisplay() + leftArrow.setNeedsDisplay() + } + + //-------------------------------------------------- + // MARK: - IndicatorViewProtocol + //-------------------------------------------------- + + open override func updateUI(previousIndex oldIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { + + pageCount.text = "\(newIndex + 1)/\(totalCount)" + formatAccessibilityValue(index: newIndex + 1, total: totalCount) + layoutIfNeeded() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index ffa80ba3..a8d0ba9a 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -44,6 +44,10 @@ import UIKit super.init(frame: .zero) } + public override init(frame: CGRect) { + super.init(frame: frame) + } + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index bbe03278..a9e86d50 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -137,6 +137,8 @@ import Foundation // Other Organisms MoleculeObjectMapping.shared()?.register(viewClass: Carousel.self, viewModelClass: CarouselModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: BarsIndicatorView.self, viewModelClass: BarsCarouselIndicatorModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: NumericIndicatorView.self, viewModelClass: NumericCarouselIndicatorModel.self) // Designed List Items MoleculeObjectMapping.shared()?.register(viewClass: ListLeftVariableIconWithRightCaret.self, viewModelClass: ListLeftVariableIconWithRightCaretModel.self) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift index 41d4ec72..de9e5d63 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableButtonAllTextAndLinksModel.swift @@ -22,7 +22,7 @@ public class ListRightVariableButtonAllTextAndLinksModel: ListItemModel, Molecul override public func setDefaults() { super.setDefaults() self.button.size = .tiny - self.button.style = ButtonStyle.secondary + self.button.style = .secondary } private enum CodingKeys: String, CodingKey { diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonView.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonView.swift index 285c429c..31420e56 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonView.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonView.swift @@ -9,11 +9,24 @@ import UIKit @objcMembers open class TwoButtonView: View, MVMCoreUIViewConstrainingProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + open var primaryButton: PillButton = PillButton() open var secondaryButton: PillButton = PillButton() private var stack = UIStackView() + + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + private var equalWidthConstraint: NSLayoutConstraint? - + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public init() { super.init(frame: .zero) } @@ -26,16 +39,21 @@ import UIKit super.init(frame: frame) } - public func setDefault() { + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + public func setDefaultAppearance() { + primaryButton.stylePrimary() secondaryButton.styleSecondary() } - // MARK: - MVMCoreViewProtocol open override func updateView(_ size: CGFloat) { super.updateView(size) - self.primaryButton.updateView(size) - self.secondaryButton.updateView(size) + + primaryButton.updateView(size) + secondaryButton.updateView(size) } open override func setupView() { @@ -52,58 +70,70 @@ import UIKit equalWidthConstraint?.isActive = true } + //-------------------------------------------------- // MARK: - Stack Manipulation + //-------------------------------------------------- + public func showPrimaryButton() { + if !stack.arrangedSubviews.contains(primaryButton) { stack.addArrangedSubview(primaryButton) primaryButton.isHidden = false } + if secondaryButton.superview != nil { equalWidthConstraint?.isActive = true } } public func showSecondaryButton() { + if !stack.arrangedSubviews.contains(secondaryButton) { stack.insertArrangedSubview(secondaryButton, at: 0) secondaryButton.isHidden = false } + if primaryButton.superview != nil { equalWidthConstraint?.isActive = true } } public func hidePrimaryButton() { + if primaryButton.superview != nil { stack.removeArrangedSubview(primaryButton) primaryButton.isHidden = true } + equalWidthConstraint?.isActive = false } public func hideSecondaryButton() { + if secondaryButton.superview != nil { stack.removeArrangedSubview(secondaryButton) secondaryButton.isHidden = true } + equalWidthConstraint?.isActive = false } + //-------------------------------------------------- // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + open override func reset() { super.reset() - setDefault() - } - - // MARK: - MVMCoreUIViewConstrainingProtocol - open func horizontalAlignment() -> UIStackView.Alignment { - return .center + + setDefaultAppearance() } - // MARK: - MoleculeViewProtocol public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + guard let model = model as? TwoButtonViewModel, - let buttonModel = model.primaryButton ?? model.secondaryButton else { return 0 } + let buttonModel = model.primaryButton ?? model.secondaryButton + else { return 0 } + return PillButton.estimatedHeight(with: buttonModel, delegateObject) } @@ -118,6 +148,7 @@ import UIKit } else { hideSecondaryButton() } + if let primaryModel = model.primaryButton { showPrimaryButton() primaryButton.set(with: primaryModel, delegateObject, additionalData) @@ -125,4 +156,11 @@ import UIKit hidePrimaryButton() } } + //-------------------------------------------------- + // MARK: - MVMCoreUIViewConstrainingProtocol + //-------------------------------------------------- + + open func horizontalAlignment() -> UIStackView.Alignment { + return .center + } } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 057203e9..d4da0ba4 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -8,35 +8,54 @@ import UIKit + public class TwoButtonViewModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "twoButtonView" public var backgroundColor: Color? public var primaryButton: ButtonModel? public var secondaryButton: ButtonModel? + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor case primaryButton case secondaryButton } - + + //-------------------------------------------------- + // MARK: - Initialzer + //-------------------------------------------------- + public init(_ primaryButton: ButtonModel?, _ secondaryButton: ButtonModel?) { self.primaryButton = primaryButton self.secondaryButton = secondaryButton } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) primaryButton = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .primaryButton) + if primaryButton?.style == nil { + primaryButton?.style = .primary + } secondaryButton = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .secondaryButton) - // Default value if secondaryButton?.style == nil { secondaryButton?.style = .secondary } } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index ab6a1cbd..15a83552 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -10,17 +10,30 @@ import Foundation @objcMembers public class CarouselItemModel: MoleculeCollectionItemModel, CarouselItemModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public override class var identifier: String { return "carouselItem" } + public var peakingUI: Bool? public var peakingArrowColor: Color? + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case peakingUI case peakingArrowColor } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) peakingUI = try typeContainer.decodeIfPresent(Bool.self, forKey: .peakingUI) diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index 30b5e95e..aa847f88 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -21,11 +21,19 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco case backgroundColor } + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(with moleculeModel: MoleculeModelProtocol) { molecule = moleculeModel super.init() } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) molecule = try typeContainer.decodeModel(codingKey: .molecule) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel.swift index 820c5a5d..4cf65f1b 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel.swift @@ -8,6 +8,15 @@ import UIKit +/// Contracts behavior between carousel and its page control. +public protocol CarouselPageControlProtocol { + var currentIndex: Int { get set } + var numberOfPages: Int { get set } + var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())? { get set } + func scrollViewDidScroll(_ collectionView: UICollectionView) +} + + open class Carousel: View { public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) @@ -17,10 +26,8 @@ open class Carousel: View { /// The index of the page, does not include dummy cells. public var pageIndex: Int { - get { - return loop ? currentIndex - 2 : currentIndex - } - set(newIndex) { + get { return loop ? currentIndex - 2 : currentIndex } + set (newIndex) { currentIndex = loop ? newIndex + 2 : newIndex } } @@ -30,7 +37,7 @@ open class Carousel: View { /// The models for the molecules. var molecules: [MoleculeModelProtocol]? - + /// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%. public var itemAlignment = UICollectionView.ScrollPosition.left @@ -41,10 +48,11 @@ open class Carousel: View { public var collectionViewHeight: NSLayoutConstraint? /// The view that we use for paging - public var pagingView: (UIView & MVMCoreUIPagingProtocol)? + public var pagingView: (UIView & CarouselPageControlProtocol)? /// If the carousel should loop after scrolling past the first and final cells. var loop = false + private var dragging = false // For adding pager @@ -80,7 +88,10 @@ open class Carousel: View { } } + //-------------------------------------------------- // MARK: - MVMCoreViewProtocol + //-------------------------------------------------- + open override func setupView() { super.setupView() collectionView.dataSource = self @@ -103,16 +114,21 @@ open class Carousel: View { layoutCollection() } - // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + // MARK: - MVMCoreUIMoleculeViewProtocol + //-------------------------------------------------- + public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { self.delegateObject = delegateObject super.set(with: model, delegateObject, additionalData) + guard let carouselModel = model as? CarouselModel else { return } + collectionView.backgroundColor = backgroundColor collectionView.layer.borderColor = backgroundColor?.cgColor collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0 backgroundColor = .white - + registerCells(with: carouselModel, delegateObject: delegateObject) setupLayout(with: carouselModel) prepareMolecules(with: carouselModel) @@ -129,107 +145,119 @@ open class Carousel: View { setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject) pageIndex = carouselModel.index - pagingView?.setPage(carouselModel.index) + pagingView?.currentIndex = carouselModel.index collectionView.reloadData() } + //-------------------------------------------------- // MARK: - JSON Setters + //-------------------------------------------------- + /// Updates the layout being used - func setupLayout(with carouselModel: CarouselModel?) { + let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1) layout.minimumInteritemSpacing = 0 collectionView.collectionViewLayout = layout } - + func prepareMolecules(with carouselModel: CarouselModel?) { guard let newMolecules = carouselModel?.molecules else { numberOfPages = 0 molecules = nil return } - + numberOfPages = newMolecules.count molecules = newMolecules + if carouselModel?.loop ?? false && newMolecules.count > 2 { // Sets up the row data with buffer cells on each side (for illusion of endless scroll... also has one more buffer cell on each side in case we can peek that cell). loop = true + molecules?.insert(newMolecules.last!, at: 0) molecules?.insert(newMolecules[(newMolecules.count - 2)], at: 0) molecules?.append(newMolecules.first!) molecules?.append(newMolecules[1]) } + pageIndex = 0 } - + /// Sets up the paging molecule open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) { - var pagingView: (UIView & MVMCoreUIPagingProtocol)? = nil + var pagingView: (UIView & CarouselPageControlProtocol)? = nil if let molecule = molecule { - pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & MVMCoreUIPagingProtocol) + pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & CarouselPageControlProtocol) } + addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20))) } - + /// Registers the cells with the collection view func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { + for molecule in carouselModel.molecules { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) } } } - + + //-------------------------------------------------- // MARK: - Convenience + //-------------------------------------------------- + /// Returns the (identifier, class) of the molecule for the given map. func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? { - guard let className = MoleculeObjectMapping.shared()?.getMoleculeClass(molecule) else { - return nil - } + guard let className = MoleculeObjectMapping.shared()?.getMoleculeClass(molecule) else { return nil } return (className.nameForReuse(with: molecule, delegateObject) ?? molecule.moleculeName, className, molecule) } /// Adds a paging view. Centers it horizontally with the collection view. The position is the vertical distance from the center of the page view to the bottom of the collection view. - open func addPaging(view: (UIView & MVMCoreUIPagingProtocol)?, position: CGFloat) { + open func addPaging(view: (UIView & CarouselPageControlProtocol)?, position: CGFloat) { + pagingView?.removeFromSuperview() - guard let pagingView = view else { - bottomPin?.isActive = false + bottomPin?.isActive = false + + guard var pagingView = view else { bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) bottomPin?.isActive = true return } - pagingView.translatesAutoresizingMaskIntoConstraints = false + addSubview(pagingView) pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true bottomAnchor.constraint(greaterThanOrEqualTo: pagingView.bottomAnchor).isActive = true - bottomPin?.isActive = false bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) bottomPin?.priority = .defaultLow bottomPin?.isActive = true - pagingView.setNumberOfPages(numberOfPages) + pagingView.numberOfPages = numberOfPages (pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill) - pagingView.setPagingTouch { [weak self] (pager) in - MVMCoreDispatchUtility.performBlock(onMainThread: { - guard let localSelf = self else { - return - } - let currentPage = pager.currentPage() - localSelf.pageIndex = currentPage - localSelf.updateModelIndex() - localSelf.goTo(localSelf.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) - }) + pageIndex = pagingView.currentIndex + pagingView.indicatorTouchAction = { [weak self] pager in + DispatchQueue.main.async { + guard let self = self else { return } + let currentPage = pager.currentIndex + self.pageIndex = currentPage + self.updateModelIndex() + self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) + } } + self.pagingView = pagingView } open func showPeaking(_ peaking: Bool) { + if peaking && !UIAccessibility.isVoiceOverRunning { // Show overlay and arrow in peaking Cell let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row } + if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex { (collectionView.cellForItem(at: firstItem) as? CarouselItem)?.setPeaking(true, animated: true) } @@ -245,9 +273,8 @@ open class Carousel: View { } public func setAccessiblity(_ cell: UICollectionViewCell?, index: Int) { - guard let cell = cell else { - return - } + guard let cell = cell else { return } + if index == currentIndex { cell.accessibilityElementsHidden = false var array = cell.accessibilityElements @@ -260,7 +287,7 @@ open class Carousel: View { } } - self.accessibilityElements = array + accessibilityElements = array } else { cell.accessibilityElementsHidden = true } @@ -285,9 +312,9 @@ extension Carousel: UICollectionViewDataSource { open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let molecule = molecules?[indexPath.row], - let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil) else { - return UICollectionViewCell() - } + let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil) + else { return UICollectionViewCell() } + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath) if let protocolCell = cell as? MoleculeViewProtocol { protocolCell.reset() @@ -302,6 +329,7 @@ extension Carousel: UICollectionViewDataSource { extension Carousel: UIScrollViewDelegate { func goTo(_ index: Int, animated: Bool) { + showPeaking(false) setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index) currentIndex = index @@ -317,12 +345,12 @@ extension Carousel: UIScrollViewDelegate { guard loop else { return } let lastPageIndex = numberOfPages + 1 - let goToIndex = {(index: Int) in + let goToIndex = { (index: Int) in self.goTo(index, animated: false) self.collectionView.layoutIfNeeded() - self.pagingView?.setPage(self.pageIndex) + self.pagingView?.currentIndex = self.pageIndex } - + if currentIndex < 2 { // If on a "buffer" last row (which is the first index), go to the real last row secretly. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking. goToIndex(lastPageIndex) @@ -333,14 +361,15 @@ extension Carousel: UIScrollViewDelegate { } func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) { - guard loop, dragging else { - return - } + + guard loop, dragging else { return } + // Checks if the user is not paging but attempting to drag endlessly and goes out of bounds. Caps the index. if let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing { let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) let index = scrollView.contentOffset.x / (itemWidth + separatorWidth) let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1 + if index < 1 { currentIndex = 0 updateModelIndex() @@ -359,15 +388,17 @@ extension Carousel: UIScrollViewDelegate { //checkForDraggingOutOfBounds(scrollView) // Let the pager know our progress if needed. - pagingView?.scrollViewDidScroll?(collectionView) + pagingView?.scrollViewDidScroll(collectionView) } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + dragging = true showPeaking(false) } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + dragging = false targetContentOffset.pointee = scrollView.contentOffset @@ -376,11 +407,13 @@ extension Carousel: UIScrollViewDelegate { // We switch cards if we pass the velocity threshold or position threshold (currently 50%). let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) - var cellToSwipeTo = Int(scrollView.contentOffset.x/(itemWidth + separatorWidth) + 0.5) + var cellToSwipeTo = Int(scrollView.contentOffset.x / (itemWidth + separatorWidth) + 0.5) let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1 let velocityThreshold: CGFloat = 1.1 + if velocity.x > velocityThreshold { cellToSwipeTo = currentIndex + 1 + } else if velocity.x < -velocityThreshold { cellToSwipeTo = currentIndex - 1 } @@ -393,9 +426,7 @@ extension Carousel: UIScrollViewDelegate { public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { // Cycle to other end if on buffer cell. handleUserOnBufferCell() - - pagingView?.setPage(pageIndex) - + pagingView?.currentIndex = pageIndex showPeaking(true) } } diff --git a/MVMCoreUI/Atomic/Organisms/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/CarouselModel.swift index c5f73a01..857901e8 100644 --- a/MVMCoreUI/Atomic/Organisms/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/CarouselModel.swift @@ -8,8 +8,16 @@ import UIKit + @objcMembers public class CarouselModel: MoleculeModelProtocol { - public static var identifier: String = "carousel" + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String { + return "carousel" + } + public var backgroundColor: Color? public var molecules: [CarouselItemModel] public var index: Int = 0 @@ -21,11 +29,15 @@ import UIKit public var itemAlignment: UICollectionView.ScrollPosition? public var pagingMolecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)? - public init(molecules: [CarouselItemModel]){ + public init(molecules: [CarouselItemModel]) { self.molecules = molecules } - - private enum CodingKeys: String, CodingKey { + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor case molecules @@ -37,9 +49,13 @@ import UIKit case itemWidthPercent case itemAlignment case pagingMolecule - } - - required public init(from decoder: Decoder) throws { + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules) index = try typeContainer.decodeIfPresent(Int.self, forKey: .index) ?? 0 @@ -65,5 +81,5 @@ import UIKit try container.encode(itemWidthPercent, forKey: .itemWidthPercent) try container.encode(itemAlignment, forKey: .itemAlignment) try container.encodeModelIfPresent(pagingMolecule, forKey: .pagingMolecule) - } + } } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift index 1654a34b..91f461dd 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeViewProtocol.swift @@ -45,8 +45,8 @@ extension MoleculeViewProtocol { } // Do nothing, optionals. - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {} - public func reset() {} + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { } + public func reset() { } public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { return nil diff --git a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift index fc9873bd..56b1a86c 100644 --- a/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/TemplateProtocol.swift @@ -22,8 +22,5 @@ public extension TemplateProtocol where Self: ViewController { let templateModel = try decoder.decode(TemplateModel.self, from: data) self.templateModel = templateModel self.pageModel = templateModel as? MVMControllerModelProtocol - if let backgroundColor = templateModel.backgroundColor { - view.backgroundColor = backgroundColor.uiColor - } } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index e40fbc33..09ca1b41 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -12,6 +12,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol //-------------------------------------------------- // MARK: - Stored Properties //-------------------------------------------------- + public var moleculesInfo: [(identifier: String, class: AnyClass, molecule: (ListItemModelProtocol & MoleculeModelProtocol))]? var observer: NSKeyValueObservation? diff --git a/MVMCoreUI/Atomic/Templates/TemplateModel.swift b/MVMCoreUI/Atomic/Templates/TemplateModel.swift index 86da1981..3dcf3964 100644 --- a/MVMCoreUI/Atomic/Templates/TemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/TemplateModel.swift @@ -8,24 +8,39 @@ import Foundation + @objcMembers public class TemplateModel: MVMControllerModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public class var identifier: String { return "" } + public var pageType: String public var template: String { // Although this is done in the extension, it is needed for the encoding. return Self.identifier } + public var backgroundColor: Color? public var screenHeading: String? public var navigationItem: (NavigationItemModelProtocol & MoleculeModelProtocol)? public var formRules: [FormGroupRule]? + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(pageType: String) { self.pageType = pageType } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case pageType case template @@ -34,6 +49,10 @@ import Foundation case formRules case navigationItem } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift index c393297b..f1b2633c 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerTemplate.swift @@ -24,25 +24,25 @@ import UIKit open override func viewForTop() -> UIView? { guard let headerModel = templateModel?.header, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar) else { - return nil - } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar) + else { return nil } + return molecule } open override func viewForMiddle() -> UIView? { guard let middleModel = templateModel?.middle, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(middleModel, delegateObject: delegateObjectIVar) else { - return nil - } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(middleModel, delegateObject: delegateObjectIVar) + else { return nil } + return molecule } override open func viewForBottom() -> UIView? { guard let footerModel = templateModel?.footer, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) else { - return nil - } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) + else { return nil } + return molecule } diff --git a/MVMCoreUI/BaseClasses/ImageView.swift b/MVMCoreUI/BaseClasses/ImageView.swift new file mode 100644 index 00000000..aa004d27 --- /dev/null +++ b/MVMCoreUI/BaseClasses/ImageView.swift @@ -0,0 +1,99 @@ +// +// ImageView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/13/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + +open class ImageView: UIImageView, ModelMoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + open var model: MoleculeModelProtocol? + + private var initialSetupPerformed = false + + //-------------------------------------------------- + // MARK: - Initialization + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + override init(image: UIImage?) { + super.init(image: image) + initialSetup() + } + + public convenience init() { + self.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + public func initialSetup() { + if !initialSetupPerformed { + initialSetupPerformed = true + setupView() + } + } + + //-------------------------------------------------- + // MARK: - ModelMoleculeViewProtocol + //-------------------------------------------------- + + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + self.model = model + if let backgroundColor = model?.backgroundColor { + self.backgroundColor = backgroundColor.uiColor + } + } + + open class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { + return model?.moleculeName + } + + open class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { + return nil + } + + open class func requiredModules(_ molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { + return nil + } +} + +// MARK:- MVMCoreViewProtocol +extension ImageView: MVMCoreViewProtocol { + + open func updateView(_ size: CGFloat) { } + + /// Will be called only once. + open func setupView() { + translatesAutoresizingMaskIntoConstraints = false + insetsLayoutMarginsFromSafeArea = false + MVMCoreUIUtility.setMarginsFor(self, leading: 0, top: 0, trailing: 0, bottom: 0) + } +} + +// MARK:- MVMCoreUIMoleculeViewProtocol +extension ImageView: MVMCoreUIMoleculeViewProtocol { + + open func reset() { + backgroundColor = .clear + } + + open func setAsMolecule() { } +} diff --git a/MVMCoreUI/BaseClasses/TextViewModel.swift b/MVMCoreUI/BaseClasses/TextViewModel.swift index 49ac664c..d9505275 100644 --- a/MVMCoreUI/BaseClasses/TextViewModel.swift +++ b/MVMCoreUI/BaseClasses/TextViewModel.swift @@ -81,6 +81,7 @@ open class TextViewModel: TextEntryFieldModel { } public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encodeIfPresent(height, forKey: .height) diff --git a/MVMCoreUI/BaseClasses/View.swift b/MVMCoreUI/BaseClasses/View.swift index b5fdc5e7..c0ca8ee1 100644 --- a/MVMCoreUI/BaseClasses/View.swift +++ b/MVMCoreUI/BaseClasses/View.swift @@ -8,11 +8,16 @@ import UIKit + @objcMembers open class View: UIView, MoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + open var model: MoleculeModelProtocol? - + private var initialSetupPerformed = false - + //-------------------------------------------------- // MARK: - Initialization //-------------------------------------------------- @@ -21,7 +26,7 @@ import UIKit super.init(frame: .zero) initialSetup() } - + public convenience init() { self.init(frame: .zero) } @@ -38,12 +43,15 @@ import UIKit } } - // MARK:- MoleculeViewProtocol + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - self.model = model - if let backgroundColor = model.backgroundColor { + self.model = model + if let backgroundColor = model.backgroundColor { self.backgroundColor = backgroundColor.uiColor - } + } } open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { @@ -66,8 +74,8 @@ import UIKit // MARK:- MVMCoreViewProtocol extension View: MVMCoreViewProtocol { - open func updateView(_ size: CGFloat) {} - + open func updateView(_ size: CGFloat) { } + /// Will be called only once. open func setupView() { translatesAutoresizingMaskIntoConstraints = false diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 75cb897c..315a0476 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -171,6 +171,10 @@ import UIKit let rules = pageModel?.formRules formValidator = FormValidator(rules) } + + if let backgroundColor = pageModel?.backgroundColor { + view.backgroundColor = backgroundColor.uiColor + } } // MARK: - Navigation Item (Move to model base) @@ -258,7 +262,7 @@ import UIKit viewRespectsSystemMinimumLayoutMargins = false // Presents from the bottom. - modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? UIModalPresentationStyle.formSheet : UIModalPresentationStyle.overCurrentContext + modalPresentationStyle = MVMCoreGetterUtility.isOnIPad() ? .formSheet : .overCurrentContext // Create the default delegate object. delegateObjectIVar = MVMCoreUIDelegateObject.create(withDelegateForAll: self) @@ -339,18 +343,18 @@ import UIKit } // MARK: - MVMCoreActionDelegateProtocol - open func handleOpenPage(for requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable : Any]?, additionalData: [AnyHashable : Any]?) { + open func handleOpenPage(for requestParameters: MVMCoreRequestParameters, actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) { formValidator?.addFormParams(requestParameters: requestParameters) requestParameters.parentPageType = loadObject?.pageJSON?.optionalStringForKey("parentPageType") MVMCoreActionHandler.defaultHandleOpenPage(for: requestParameters, additionalData: additionalData, delegateObject: delegateObject()) } - open func logAction(withActionInformation actionInformation: [AnyHashable : Any]?, additionalData: [AnyHashable : Any]?) { + open func logAction(withActionInformation actionInformation: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?) { MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: self, actionInformation: actionInformation, additionalData: additionalData) } // MARK: - MoleculeDelegateProtocol - open func getModuleWithName(_ name: String?) -> [AnyHashable : Any]? { + open func getModuleWithName(_ name: String?) -> [AnyHashable: Any]? { guard let name = name else { return nil } return loadObject?.modulesJSON?.optionalDictionaryForKey(name) } @@ -391,7 +395,7 @@ import UIKit view.accessibilityElements = [pickerView, toolBar] } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: textField.inputView) + UIAccessibility.post(notification: .layoutChanged, argument: textField.inputView) } } } @@ -400,7 +404,7 @@ import UIKit if textField === selectedField { if UIAccessibility.isVoiceOverRunning { view.accessibilityElements = nil - UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: textField) + UIAccessibility.post(notification: .layoutChanged, argument: textField) } selectedField = nil } diff --git a/MVMCoreUI/Containers/Views/Container.swift b/MVMCoreUI/Containers/Views/Container.swift index eb05cd21..8c95f110 100644 --- a/MVMCoreUI/Containers/Views/Container.swift +++ b/MVMCoreUI/Containers/Views/Container.swift @@ -9,8 +9,10 @@ import UIKit open class Container: View, ContainerProtocol { + public var view: UIView? let containerHelper = ContainerHelper() + var containerModel: ContainerModelProtocol? { get { return model as? ContainerModelProtocol } } diff --git a/MVMCoreUI/CustomPrimitives/Color.swift b/MVMCoreUI/CustomPrimitives/Color.swift index f06b337c..42c7a600 100644 --- a/MVMCoreUI/CustomPrimitives/Color.swift +++ b/MVMCoreUI/CustomPrimitives/Color.swift @@ -55,6 +55,13 @@ public final class Color: Codable { determineRGBA() } + public init?(uiColor: UIColor?) { + guard let uiColor = uiColor else { return nil } + self.uiColor = uiColor + hex = UIColor.hexString(for: uiColor) ?? "" + determineRGBA() + } + init?(name: String) { guard let colorTuple = UIColor.names[name] else { return nil } self.uiColor = colorTuple.uiColor diff --git a/MVMCoreUI/Styles/Styler.swift b/MVMCoreUI/Styles/Styler.swift index 863d4703..d8d0a519 100644 --- a/MVMCoreUI/Styles/Styler.swift +++ b/MVMCoreUI/Styles/Styler.swift @@ -172,6 +172,29 @@ open class Styler { } } + public enum Button { + + public enum Style: String, Codable { + case primary + case secondary + } + + public enum Size: String, Codable { + case standard + case tiny + + func getHeight() -> CGFloat { + switch self { + case .standard: + return 42 + + case .tiny: + return 20 + } + } + } + } + //-------------------------------------------------- // MARK: - Functions //-------------------------------------------------- diff --git a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings index 48cbe3ce..25dd849b 100644 --- a/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/en.lproj/Localizable.strings @@ -68,8 +68,8 @@ // MARK: Carousel -"MVMCoreUIPageControl_currentpage_index" = "page %ld of %ld"; -"MVMCoreUIPageControlslides_currentpage_index" = "slide %ld of %ld"; +"MVMCoreUIPageControl_currentpage_index" = "page %@ of %d"; +"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d"; // MARK: Styler diff --git a/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings index 93c5dda5..0a9eadb9 100644 --- a/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/es-MX.lproj/Localizable.strings @@ -48,8 +48,8 @@ "AccOff" = "apagado"; "AccToggleHint" = "toca dos veces para alternar"; // Carousel -"MVMCoreUIPageControl_currentpage_index" = "página %ld de %ld"; -"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %ld of %ld"; +"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d"; +"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d"; //Styler "CountDownDay" = " día"; "CountDownHour" = " hora"; diff --git a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings index 053c4b02..bbc45f4a 100644 --- a/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings +++ b/MVMCoreUI/SupportingFiles/Strings/es.lproj/Localizable.strings @@ -50,8 +50,8 @@ "AccOff" = "apagado"; "AccToggleHint" = "toca dos veces para alternar"; // Carousel -"MVMCoreUIPageControl_currentpage_index" = "página %ld de %ld"; -"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %ld of %ld"; +"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d"; +"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d"; //Styler "CountDownDay" = " día"; "CountDownHour" = " hora"; diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m index 2021cb15..eeff9cfe 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m @@ -188,14 +188,13 @@ self.labelRightConstraint.active = NO; // Sets up to use a button action. Always uses the top view controller - PillButton *button = [[PillButton alloc] init]; + PillButton *button = [[PillButton alloc] initAsPrimaryButton:false makeTiny:true]; [button styleSecondary]; [button setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; [button setContentHuggingPriority:800 forAxis:UILayoutConstraintAxisHorizontal]; button.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:button]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=space-[button]->=space-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"space":@(PaddingFive)} views:NSDictionaryOfVariableBindings(button)]]; [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0].active = YES; [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.centerView attribute:NSLayoutAttributeRight multiplier:1 constant:PaddingThree].active = YES; [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:button attribute:NSLayoutAttributeRight multiplier:1 constant:(self.closeButton ? PaddingTen : PaddingFive)].active = YES; diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m index b3dacb21..d8c21b45 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m @@ -160,6 +160,7 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; UIColor *statusBarColor = nil; UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; MVMCoreUITopAlertBaseView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle]; + [view updateView:CGRectGetWidth(self.bounds)]; if (!statusBarColor) { statusBarColor = [UIColor whiteColor]; }