From 977bff851e3709a1bf29027b2082f4c3d8f5ff37 Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Thu, 6 Feb 2020 15:51:13 -0500 Subject: [PATCH] latest and greatest. --- MVMCoreUI.xcodeproj/project.pbxproj | 40 ++- MVMCoreUI/Atoms/Views/CarouselIndicator.swift | 15 - .../CarouselIndicator/CarouselIndicator.swift | 301 ++++++++++++++++ .../CarouselIndicatorModel.swift | 50 +++ .../IndicatorViews/BarsIndicatorView.swift | 164 +++++++++ .../IndicatorViews/NumericIndicatorView.swift | 161 +++++++++ MVMCoreUI/Atoms/Views/PageControl.swift | 322 ------------------ MVMCoreUI/BaseClasses/StackView.swift | 19 +- 8 files changed, 719 insertions(+), 353 deletions(-) delete mode 100644 MVMCoreUI/Atoms/Views/CarouselIndicator.swift create mode 100644 MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift create mode 100644 MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift create mode 100644 MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift create mode 100644 MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift delete mode 100644 MVMCoreUI/Atoms/Views/PageControl.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 1e5ca199..c7891f25 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -60,9 +60,11 @@ 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EB368D23609801006832FA /* HeadlineBodyModel.swift */; }; 01F2A03223A4498200D954D8 /* CaretLinkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F2A03123A4498200D954D8 /* CaretLinkModel.swift */; }; 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */; }; - 0A14F69323E349EF00EDF7F7 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F69223E349EF00EDF7F7 /* PageControl.swift */; }; + 0A14F69323E349EF00EDF7F7 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F69223E349EF00EDF7F7 /* CarouselIndicator.swift */; }; 0A14F6A523E4803A00EDF7F7 /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6A423E4803A00EDF7F7 /* StackView.swift */; }; - 0A14F6A723E4AB6E00EDF7F7 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */; }; + 0A14F6A923E8750300EDF7F7 /* CarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6A823E8750300EDF7F7 /* CarouselIndicatorModel.swift */; }; + 0A14F6B223E8C28D00EDF7F7 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6B123E8C28D00EDF7F7 /* BarsIndicatorView.swift */; }; + 0A14F6B423E8C29700EDF7F7 /* NumericIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6B323E8C29700EDF7F7 /* NumericIndicatorView.swift */; }; 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */; }; 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB7E235DECC500C160A2 /* EntryField.swift */; }; 0A21DB83235DFBC500C160A2 /* MdnEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */; }; @@ -380,9 +382,11 @@ 01EB368D23609801006832FA /* HeadlineBodyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadlineBodyModel.swift; sourceTree = ""; }; 01F2A03123A4498200D954D8 /* CaretLinkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretLinkModel.swift; sourceTree = ""; }; 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionDetailWithImage.swift; sourceTree = ""; }; - 0A14F69223E349EF00EDF7F7 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControl.swift; sourceTree = ""; }; + 0A14F69223E349EF00EDF7F7 /* CarouselIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = ""; }; 0A14F6A423E4803A00EDF7F7 /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = ""; }; - 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = ""; }; + 0A14F6A823E8750300EDF7F7 /* CarouselIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselIndicatorModel.swift; sourceTree = ""; }; + 0A14F6B123E8C28D00EDF7F7 /* BarsIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarsIndicatorView.swift; sourceTree = ""; }; + 0A14F6B323E8C29700EDF7F7 /* NumericIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumericIndicatorView.swift; sourceTree = ""; }; 0A209CD223A7E2810068F8B0 /* UIStackViewAlignment+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackViewAlignment+Extension.swift"; sourceTree = ""; }; 0A21DB7E235DECC500C160A2 /* EntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = ""; }; 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnEntryField.swift; sourceTree = ""; }; @@ -706,6 +710,25 @@ path = FormUIHelpers; sourceTree = ""; }; + 0A14F6B023E8C27A00EDF7F7 /* CarouselIndicator */ = { + isa = PBXGroup; + children = ( + 0A14F6B723ECA7F900EDF7F7 /* IndicatorViews */, + 0A14F69223E349EF00EDF7F7 /* CarouselIndicator.swift */, + 0A14F6A823E8750300EDF7F7 /* CarouselIndicatorModel.swift */, + ); + path = CarouselIndicator; + sourceTree = ""; + }; + 0A14F6B723ECA7F900EDF7F7 /* IndicatorViews */ = { + isa = PBXGroup; + children = ( + 0A14F6B123E8C28D00EDF7F7 /* BarsIndicatorView.swift */, + 0A14F6B323E8C29700EDF7F7 /* NumericIndicatorView.swift */, + ); + path = IndicatorViews; + sourceTree = ""; + }; 0A5D59C323AD488600EFD9E9 /* Protocols */ = { isa = PBXGroup; children = ( @@ -1162,6 +1185,7 @@ D29DF17D21E69E26003B2FB9 /* Views */ = { isa = PBXGroup; children = ( + 0A14F6B023E8C27A00EDF7F7 /* CarouselIndicator */, 9445890B2385BCE300DE9FD4 /* ProgressBarModel.swift */, 01509D922327ECFB00EF99AA /* ProgressBar.swift */, 9445890D2385C3F800DE9FD4 /* MultiProgressModel.swift */, @@ -1191,8 +1215,6 @@ 0AA33B392398524F0067DD0F /* Toggle.swift */, D260105423CEA7DC00764D80 /* MVMCoreUISwitch+Model.swift */, 012CA99D2385A2D3003F810F /* MFView+ModelExtension.swift */, - 0A14F69223E349EF00EDF7F7 /* PageControl.swift */, - 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */, ); path = Views; sourceTree = ""; @@ -1641,6 +1663,7 @@ 01F2A03223A4498200D954D8 /* CaretLinkModel.swift in Sources */, 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */, 011B58F023A2AA980085F53C /* ListItemModelProtocol.swift in Sources */, + 0A14F6A923E8750300EDF7F7 /* CarouselIndicatorModel.swift in Sources */, D22479962316AF6E003FCCF9 /* HeadlineBodyLink.swift in Sources */, D29DF2AE21E7B3A4003B2FB9 /* MFTextView.m in Sources */, 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */, @@ -1654,7 +1677,7 @@ 017BEB7F23676E870024EF95 /* MoleculeObjectMapping.swift in Sources */, D274CA332236A78900B01B62 /* FooterView.swift in Sources */, D29DF2BF21E7BEA4003B2FB9 /* MVMCoreUITabBarPageControlViewController.m in Sources */, - 0A14F69323E349EF00EDF7F7 /* PageControl.swift in Sources */, + 0A14F69323E349EF00EDF7F7 /* CarouselIndicator.swift in Sources */, 014AA72423C501E2006F3E93 /* MoleculeContainerModel.swift in Sources */, D29DF28321E7AB24003B2FB9 /* MVMCoreUICommonViewsUtility.m in Sources */, 011B58F223A2AE2C0085F53C /* DropDownListItemModel.swift in Sources */, @@ -1670,6 +1693,7 @@ 9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */, D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */, D2A6390122CBB1820052ED1F /* Carousel.swift in Sources */, + 0A14F6B423E8C29700EDF7F7 /* NumericIndicatorView.swift in Sources */, D29DF2C721E7BF57003B2FB9 /* MFTabBarInteractor.m in Sources */, D29DF29521E7ADB8003B2FB9 /* ProgrammaticScrollViewController.m in Sources */, D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */, @@ -1693,7 +1717,6 @@ 01EB3684236097C0006832FA /* MoleculeModelProtocol.swift in Sources */, D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, - 0A14F6A723E4AB6E00EDF7F7 /* CarouselIndicator.swift in Sources */, 0198F79F225679880066C936 /* FormValidationProtocol.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */, D260105B23D0BB7100764D80 /* StackModelProtocol.swift in Sources */, @@ -1739,6 +1762,7 @@ D2B18B922361E65A00A9AEDC /* CoreUIObject.swift in Sources */, D29DF2BE21E7BEA4003B2FB9 /* TopTabbar.m in Sources */, 014AA72E23C5059B006F3E93 /* StackCenteredPageTemplateModel.swift in Sources */, + 0A14F6B223E8C28D00EDF7F7 /* BarsIndicatorView.swift in Sources */, D2A514632213643100345BFB /* MoleculeStackCenteredTemplate.swift in Sources */, D260105923D0A92900764D80 /* ContainerProtocol.swift in Sources */, C695A69423C9909000BFB94E /* DoughnutChartModel.swift in Sources */, diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator.swift deleted file mode 100644 index 686b1109..00000000 --- a/MVMCoreUI/Atoms/Views/CarouselIndicator.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CarouselIndicator.swift -// MVMCoreUI -// -// Created by Kevin Christiano on 1/31/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - -import Foundation - - -open class CarouselIndicator: PageControl { - - -} diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift new file mode 100644 index 00000000..5201b920 --- /dev/null +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift @@ -0,0 +1,301 @@ +// +// CarouselIndicator.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 1/30/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol IndicatorViewProtocol { + func updateUI() + var isEnabled: Bool { get set } + var currentIndex: Int? { get set } + var numberOfPages: Int? { get } +} + +open class CarouselIndicator: Control { + //-------------------------------------------------- + // MARK: - Constraints + //-------------------------------------------------- + + public var topConstraint: NSLayoutConstraint? + public var bottomConstraint: NSLayoutConstraint? + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public typealias IndicatorView = UIView & IndicatorViewProtocol + + /// The types of indicators that can appear. + public enum IndicatorType: String { + case bar + case numeric + case hybrid // bar & numeric + } + + /// Determines interactivity and appearance of the indicator. + public var indicatorType: IndicatorType = .hybrid { + didSet { + assignIndicatorView() + } + } + + /// The view control relative to the state of the indicator type. + private(set) var indicatorView: IndicatorView? { + didSet { + topConstraint = indicatorView!.topAnchor.constraint(equalTo: topAnchor, constant: PaddingTwo) + topConstraint?.isActive = true + + bottomConstraint = bottomAnchor.constraint(equalTo: indicatorView!.bottomAnchor, constant: PaddingTwo) + bottomConstraint?.isActive = true + } + } + + /// The maxmum count of pages before the indicatorView forces a numeric Indicator insead of Bar. + public var hybridThreshold: Int = 5 + + /// Spacing used between bars of the Bars Indicator and between the title and arrows of the Numeric Indicator + public var indicatorBarSpacing: CGFloat = 6 { + didSet { + if let stackView = indicatorView as? StackView { + stackView.spacing = indicatorBarSpacing + stackView.layoutIfNeeded() + } + } + } + + // private(set) var indicatorBars = [BarIndicator]() + + /// Set this closure to perform an action when a different indicator was selected. + public var indicatorTappedBlock: ((Int)->())? + + /// Allows sendActions() to trigger even if index is min/max index. + public var alwaysSendControlEvent = 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 isSlidesAccessibile = false + + public var isAnimated = false + + /// Will hide this control if page count is 1. + public var hidesForSinglePage = false { + didSet { isHidden = hidesForSinglePage && numberOfPages <= 1 } + } + + /// If true, then index will wraparound, otherwise it will stop paging at min/max index. + public var allowIndexWraparound = false + + public override var isEnabled: Bool { + didSet { + isUserInteractionEnabled = isEnabled + indicatorView?.isEnabled = isEnabled + + if indicatorType != .bar && numberOfPages > hybridThreshold { + + } else { + if let stackView = indicatorView as? BarsIndicatorView { + stackView.stackView.arrangedSubviews.forEach { view in + // if let indicator = { + (view as? BarsIndicatorView)?.isEnabled = isEnabled + // } + } + } + } + } + } + + //-------------------------------------------------- + // MARK: - Computed Properties + //-------------------------------------------------- + + /// The currently active indicator view. + public weak var currentIndicator: IndicatorView? { + didSet { + + } + } + + private var _currentIndex = 0 + + public var currentIndex: Int { + get { return _currentIndex } + set { + guard _currentIndex != newValue else { return } + _currentIndex = newValue + } + } + + private var _numberOfPages = 0 + + public var numberOfPages: Int { + get { return _numberOfPages } + set { + guard _numberOfPages != newValue else { return } + _numberOfPages = newValue + + if hidesForSinglePage && newValue <= 1 { + isHidden = true + } else { + isHidden = false + indicatorView = BarsIndicatorView(numberOfBars: numberOfPages) + } + + indicatorView?.updateUI() + } + } + + private var _indicatorTintColor: UIColor = .mvmCoolGray6 + + public var indicatorTintColor: UIColor { + get { return _indicatorTintColor } + set { + _indicatorTintColor = newValue + if isBarIndicator(), let barsView = (indicatorView as? BarsIndicatorView)?.stackView.arrangedSubviews.isEmpty { + indicatorView = BarsIndicatorView(numberOfBars: numberOfPages) + } + (indicatorView as? BarsIndicatorView)?.stackView.arrangedSubviews.forEach { $0.backgroundColor = newValue} + } + } + + private var _currentPageIndicatorTintColor: UIColor = .black + + public var currentPageIndicatorTintColor: UIColor { + get { return _currentPageIndicatorTintColor } + set { + _currentPageIndicatorTintColor = newValue + if ((indicatorView as? BarsIndicatorView)?.stackView.arrangedSubviews.isEmpty)! { + indicatorView = BarsIndicatorView(numberOfBars: numberOfPages) + } + currentIndicator?.backgroundColor = newValue + } + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + override init(frame: CGRect) { + super.init(frame: frame) + } + + convenience override init() { + self.init(frame: .zero) + } + + public init(indicatorType: IndicatorType) { + self.indicatorType = indicatorType + super.init(frame: .zero) + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + public override func initialSetup() { + super.initialSetup() + + isAccessibilityElement = true + accessibilityTraits = .adjustable + } + + open override func setupView() { + super.setupView() + + if indicatorView == nil { + + assignIndicatorView() + + if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: isSlidesAccessibile ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") { + accessibilityValue = String(format: accessibleValue, currentIndex + 1, numberOfPages) + } + } + } + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + func assignIndicatorView() { + + switch indicatorType { + case .bar: + indicatorView = BarsIndicatorView(numberOfBars: numberOfPages) + + case .numeric: + indicatorView = NumericIndicatorView() + + case .hybrid: + indicatorView = numberOfPages >= hybridThreshold ? NumericIndicatorView() : BarsIndicatorView(numberOfBars: numberOfPages) + } + } + + /// Removes all indicators from their subview and then clears the holding array. + func removeIndicatorView() { + + // indicatorBars.forEach { $0.removeFromSuperview() } + // indicatorBars = [] + } + + /// Convenience to determine if current view is displaying bars. + func isBarIndicator() -> Bool { + return indicatorType != .bar && numberOfPages > hybridThreshold + } + + //-------------------------------------------------- + // MARK: - MoleculeViewProtocol + //-------------------------------------------------- + + override open func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.setWithModel(model, delegateObject, additionalData) + + guard let model = model as? CarouselIndicatorModel else { return } + + if let type = model.type, let indicator = IndicatorType(rawValue: type) { + indicatorType = indicator + } + + // backgroundColor = model.backgroundColor?.uiColor + // barsColor = model.barsColor + // pageIndicatorTintColor + // currentPageIndicatorTintColor + } + + //-------------------------------------------------- + // MARK: - Accessibility + //-------------------------------------------------- + + open override func accessibilityIncrement() { + accessibilityAdjust(toPage: currentIndex + 1) + } + + open override func accessibilityDecrement() { + accessibilityAdjust(toPage: currentIndex - 1) + } + + func accessibilityAdjust(toPage index: Int) { + + if (index < numberOfPages && index >= 0) || alwaysSendControlEvent { + isAnimated = false + currentIndex = index + sendActions(for: .valueChanged) + indicatorTappedBlock?(index) + } + } + + func setTopBottomSpace(constant: CGFloat) { + + bottomConstraint?.constant = constant + topConstraint?.constant = constant + } +} diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift new file mode 100644 index 00000000..7796b89c --- /dev/null +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -0,0 +1,50 @@ +// +// CarouselIndicatorModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + + +public class CarouselIndicatorModel: MoleculeModelProtocol { + + public var backgroundColor: Color? + public var barsColor: Color? + + public static var identifier: String { + return "carouselIndicator" + } + + public var moleculeName: String? + public var type: String? = "hybrid" + + private enum CodingKeys: String, CodingKey { + case moleculeName + case backgroundColor + case type + case barsColor + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) +// if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { +// self.state = state +// } +// action = try typeContainer.decodeModelIfPresent(codingKey: .action, typeCodingKey: ActionCodingKey.actionType) +// alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction, typeCodingKey: ActionCodingKey.actionType) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + barsColor = try typeContainer.decodeIfPresent(Color.self, forKey: .barsColor) + type = try typeContainer.decodeIfPresent(String.self, forKey: .type) ?? "hybrid" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(barsColor, forKey: .barsColor) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(type, forKey: .type) + } +} diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift new file mode 100644 index 00000000..57c2530d --- /dev/null +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift @@ -0,0 +1,164 @@ +// +// BarIndicatorView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class BarsIndicatorView: View, IndicatorViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + let stackView: StackView = { + let stackView = StackView() + stackView.axis = .horizontal + stackView.distribution = .equalSpacing + stackView.spacing = PaddingOne + return stackView + }() + + var barsReference: [(View, 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) + + public var enabledColor: UIColor = .black + public var disabledColor: UIColor = .mvmCoolGray3 + + private var oldIndex: Int = 0 + + /// Returns the currentIndex from its parent CarouselIndicator. + public var currentIndex: Int? { + get { return (superview as? CarouselIndicator)?.currentIndex } + set { + guard let newValue = newValue else { return } + (superview as? CarouselIndicator)?.currentIndex = newValue + } + } + + /// Returns the numberOfPages count from its parent CarouselIndicator. + public var numberOfPages: Int? { + return (superview as? CarouselIndicator)?.numberOfPages + } + + public var numberOfBars: Int = 0 { + didSet { + // TODO: Generate bars... + } + } + + open var isEnabled: Bool = true { + didSet { + if isEnabled { + + } else { + + } + } + } + + open var isAnimated: Bool { + return (superview as? CarouselIndicator)?.isAnimated ?? true + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + public init(numberOfBars: Int) { + super.init(frame: .zero) + self.numberOfBars = numberOfBars + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + open override func setupView() { + super.setupView() + + if subviews.isEmpty { + addSubview(stackView) + + stackView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + stackView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true + trailingAnchor.constraint(lessThanOrEqualTo: stackView.trailingAnchor).isActive = true + + removeIndicators() + + let tapGesture = UITapGestureRecognizer() + tapGesture.addTarget(self, action: #selector(indicatorTapped(_:))) + addGestureRecognizer(tapGesture) + } + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + func generateBars() { + + var bars = [(View, NSLayoutConstraint)]() + + for i in 0..= touchPoint_X && indicator.frame.minX <= touchPoint_X +// } +// +// if let selectIndex = index { +// currentIndex = selectIndex +// (indicatorView as? BarsIndicatorView)?sendActions(for: .valueChanged) +// indicatorTappedBlock?(selectIndex) +// } + } + } + + //-------------------------------------------------- + // MARK: - IndicatorViewProtocol + //-------------------------------------------------- + + public func updateUI() { + + let expression = { +// stackView.arrangedSubviews[oldIndex].heightConstraint?.constant = BarsIndicatorView.indicatorBarHeight.unselected +// self.heightConstraint?.constant = BarsIndicatorView.indicatorBarHeight.selected +// self.layoutIfNeeded() + } + +// isAnimated ? UIView.animate(withDuration: 0.3) { expression() } : expression() + } +} diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift new file mode 100644 index 00000000..4607dd2c --- /dev/null +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift @@ -0,0 +1,161 @@ +// +// NumericIndicatorView.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class NumericIndicatorView: View, IndicatorViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + /// Text to display the current count of total pages for viewing. + open var titleLabel: Label = { + let label = Label.commonLabelB2(true) + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.textAlignment = .center + return label + }() + + + // Left and right arrows for Numeric indicator + open var leftArrow = MFLoadImageView() + open var rightArrow = MFLoadImageView() + + // open var activeColor: (enabled: UIColor, disabled: UIColor) + + open var isEnabled: Bool = true { + didSet { + if isEnabled { + + } else { + + } + } + } + + /// Returns the currentIndex from its parent CarouselIndicator. + public var currentIndex: Int? { + get { return (superview as? CarouselIndicator)?.currentIndex } + set { + guard let newValue = newValue else { return } + (superview as? CarouselIndicator)?.currentIndex = newValue + } + } + + /// Returns the numberOfPages count from its parent CarouselIndicator. + public var numberOfPages: Int? { + return (superview as? CarouselIndicator)?.numberOfPages + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: .zero) + } + + public convenience init() { + self.init(frame: .zero) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + titleLabel.updateView(size) + } + + //-------------------------------------------------- + // MARK: - Setup + //-------------------------------------------------- + + open override func setupView() { + super.setupView() + + guard subviews.isEmpty else { return } + + addSubview(titleLabel) + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + NSLayoutConstraint.constraintPinSubview(titleLabel, pinTop: true, pinBottom: true, pinLeft: false, pinRight: false) + + addSubview(leftArrow) + NSLayoutConstraint.constraintPinView(leftArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) + leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + leftArrow.isUserInteractionEnabled = true + + leftArrow.loadImage(withName: "peakingRightArrow", width: nil, height: nil) { [weak self] image, _, _ in + DispatchQueue.main.async { [weak self] in + guard let image = image else { return } + self?.leftArrow.imageView.image = image.withHorizontallyFlippedOrientation() + self?.leftArrow.layoutIfNeeded() + } + } + + let leftTap = UITapGestureRecognizer() + leftTap.addTarget(self, action: #selector(pageValueDecrement)) + leftArrow.addGestureRecognizer(leftTap) + + rightArrow.loadImage(withName: "peakingRightArrow") + addSubview(rightArrow) + NSLayoutConstraint.constraintPinView(rightArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) + rightArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + rightArrow.isUserInteractionEnabled = true + + let rightTap = UITapGestureRecognizer() + rightTap.addTarget(self, action: #selector(pageValueIncrement)) + rightArrow.addGestureRecognizer(rightTap) + + NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[leftArrowView]-(padding)-[titleLabel]-(padding)-[rightArrowView]-0-|", + options: .directionLeadingToTrailing, + metrics: ["padding": PaddingOne], + views: ["leftArrowView": leftArrow, + "titleLabel": titleLabel, + "rightArrowView": rightArrow])) + } + + //-------------------------------------------------- + // MARK: - Actions + //-------------------------------------------------- + + @objc func pageValueIncrement() { + guard let currentIndex = currentIndex, + let numberOfPages = numberOfPages + else { return } + + self.currentIndex = min(currentIndex + 1, numberOfPages - 1) + } + + @objc func pageValueDecrement() { + guard let currentIndex = currentIndex else { return } + + self.currentIndex = max(0, currentIndex - 1) + } + + //-------------------------------------------------- + // MARK: - IndicatorViewProtocol + //-------------------------------------------------- + + open func updateUI() { + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.titleLabel.text = "\(self.currentIndex)/\(self.numberOfPages)" + self.layoutIfNeeded() + (self.superview as? CarouselIndicator)?.sendActions(for: .valueChanged) + } + } +} diff --git a/MVMCoreUI/Atoms/Views/PageControl.swift b/MVMCoreUI/Atoms/Views/PageControl.swift deleted file mode 100644 index cac93fef..00000000 --- a/MVMCoreUI/Atoms/Views/PageControl.swift +++ /dev/null @@ -1,322 +0,0 @@ -// -// PageControl.swift -// MVMCoreUI -// -// Created by Kevin Christiano on 1/30/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - -import Foundation - -/** - This class is implemented to focus primarily on the page control logic. - Visual flourishes and bespoke behavior should be subclassed from here. - */ -open class PageControl: Control { - //-------------------------------------------------- - // MARK: - Constraints - //-------------------------------------------------- - - public var topConstraint: NSLayoutConstraint? - public var bottomConstraint: NSLayoutConstraint? - - //-------------------------------------------------- - // MARK: - Properties - //-------------------------------------------------- - - public enum IndicatorType { - case bar - case numeric - case hybrid // bar & numeric - } - - public var indicatorType: IndicatorType = .hybrid - - public var indicatorSpacing: CGFloat { - get { return stackView.spacing } - set { - stackView.spacing = newValue - stackView.layoutIfNeeded() - } - } - - public let indicatorBarWidth: CGFloat = 24 - public let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1) - - private(set) var indicators = [BarIndicator]() - - var stackView: StackView = { - let stack = StackView() - stack.axis = .horizontal - stack.distribution = .equalSpacing - stack.spacing = 6 - return stack - }() - - public var pagingTouchBlock: ((Int)->())? - - // a flag to allow to send UIControlEventValueChanged actions all the time - // e.g. going to previous element at first place and going to next at last place - // While current rectangle won't change, need update current page - // When awlaysSenfingControlEvent is false, and user is already at first or final index, if user try to increment or decrement, won't do action - // while self.awlaysSenfingControlEven is YES, it still send control event, while the rectangle won't change, need set currentPage again. - public var alwaysSendingControlEvent = 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 isSlidesAccessibile = false - public var isAnimated = false - public var hidesForSinglePage = false - - /// If true, then index will wraparound, otherwise it will stop paging at min/max index. - public var allowIndexWraparound = false - - //-------------------------------------------------- - // MARK: - Computed Properties - //-------------------------------------------------- - - /// The currently active indicator view. - public weak var currentIndicator: BarIndicator? { - didSet { - let expression = { - oldValue?.heightConstraint?.constant = self.indicatorBarHeight.unselected - self.currentIndicator?.heightConstraint?.constant = self.indicatorBarHeight.selected - self.layoutIfNeeded() - } - - isAnimated ? UIView.animate(withDuration: 0.3) { expression() } : expression() - } - } - - private var _currentPage = 0 - - public var currentPage: Int { - get { return _currentPage } - set { - guard _currentPage != newValue else { return } - _currentPage = newValue - currentIndicator = indicators[newValue] - } - } - - private var _numberOfPages = 0 - - public var numberOfPages: Int { - get { return _numberOfPages } - set { - guard _numberOfPages != newValue else { return } - _numberOfPages = newValue - setupIndicators() - } - } - - private var _indicatorTintColor: UIColor = .mvmCoolGray6 - - public var indicatorTintColor: UIColor { - get { return _indicatorTintColor } - set { - _indicatorTintColor = newValue - if indicators.isEmpty { setupIndicators() } - indicators.forEach { $0.backgroundColor = newValue} - } - } - - private var _currentPageIndicatorTintColor: UIColor = .black - - public var currentPageIndicatorTintColor: UIColor { - get { return _currentPageIndicatorTintColor } - set { - _currentPageIndicatorTintColor = newValue - if indicators.isEmpty { setupIndicators() } - currentIndicator?.backgroundColor = newValue - } - } - - //-------------------------------------------------- - // MARK: - Initializers - //-------------------------------------------------- - - override init(frame: CGRect) { - super.init(frame: frame) - } - - convenience override init() { - self.init(frame: .zero) - } - - convenience init(indicatorType: IndicatorType) { - self.init(frame: .zero) - self.indicatorType = indicatorType - } - - required public init?(coder: NSCoder) { - super.init(coder: coder) - } - - //-------------------------------------------------- - // MARK: - Lifecycle - //-------------------------------------------------- - - public override func initialSetup() { - super.initialSetup() - - isAccessibilityElement = true - accessibilityTraits = .adjustable - } - - open override func setupView() { - super.setupView() - - if stackView.subviews.isEmpty { - if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: isSlidesAccessibile ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") { - accessibilityValue = String(format: accessibleValue, currentPage + 1, numberOfPages) - } - - addSubview(stackView) - stackView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - stackView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true - trailingAnchor.constraint(lessThanOrEqualTo: stackView.trailingAnchor).isActive = true - - topConstraint = stackView.topAnchor.constraint(equalTo: topAnchor, constant: PaddingThree) - topConstraint?.priority = .defaultHigh - topConstraint?.isActive = true - - bottomConstraint = bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: PaddingThree) - bottomConstraint?.priority = .defaultHigh - bottomConstraint?.isActive = true - - setupIndicators() - - let tapGesture = UITapGestureRecognizer() - tapGesture.addTarget(self, action: #selector(indicatorTapped(_:))) - addGestureRecognizer(tapGesture) - } - } - - open override func updateView(_ size: CGFloat) { } - - //-------------------------------------------------- - // MARK: - Methods - //-------------------------------------------------- - - // func setPagingTouchBlock(_ pagingTouchBlock: PageControl.PagingTouchBlock?) { } - // - // func scrollViewDidScroll(_ collectionView: UICollectionView) { } - - func setupIndicators() { - - removeIndicators() - var newIndicators = [BarIndicator]() - - for i in 0..= touchPoint_X && indicator.frame.minX <= touchPoint_X - } - - if let selectIndex = index { - currentPage = selectIndex - sendActions(for: .valueChanged) - pagingTouchBlock?(selectIndex) - } - } - } - - //-------------------------------------------------- - // MARK: - MoleculeViewProtocol - //-------------------------------------------------- - - override open func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { - // TODO - /* - - var colorString = json?.string(KeyBackgroundColor) - - if colorString != nil { - backgroundColor = UIColor.mfGet(forHex: colorString) - } - - colorString = json?.string("barsColor") - - if colorString != nil { - let color = UIColor.mfGet(forHex: colorString) - pageIndicatorTintColor = color - currentPageIndicatorTintColor = color - } - - colorString = json?.string("currentBarColor") - - if colorString != nil { - currentPageIndicatorTintColor = UIColor.mfGet(forHex: colorString) - } - */ - } - - //-------------------------------------------------- - // MARK: - Accessibility - //-------------------------------------------------- - - open override func accessibilityIncrement() { - accessibilityAdjust(toPage: currentPage + 1) - } - - open override func accessibilityDecrement() { - accessibilityAdjust(toPage: currentPage - 1) - } - - func accessibilityAdjust(toPage index: Int) { - - if (index < numberOfPages && index >= 0) || alwaysSendingControlEvent { - isAnimated = false - currentPage = index - sendActions(for: .valueChanged) - pagingTouchBlock?(index) - } - } - - func setTopBottomSpace(constant: CGFloat) { - bottomConstraint?.constant = constant - topConstraint?.constant = constant - } - - public class BarIndicator: View { - - var heightConstraint: NSLayoutConstraint? - - override init(frame: CGRect) { - super.init(frame: .zero) - } - - convenience init() { - self.init(frame: .zero) - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } -} diff --git a/MVMCoreUI/BaseClasses/StackView.swift b/MVMCoreUI/BaseClasses/StackView.swift index b71273a7..0539ba9e 100644 --- a/MVMCoreUI/BaseClasses/StackView.swift +++ b/MVMCoreUI/BaseClasses/StackView.swift @@ -16,9 +16,9 @@ open class StackView: UIStackView, ModelMoleculeViewProtocol { open var json: [AnyHashable: Any]? open var model: MoleculeModelProtocol? - + private var initialSetupPerformed = false - + //-------------------------------------------------- // MARK: - Initialization //-------------------------------------------------- @@ -27,7 +27,7 @@ open class StackView: UIStackView, ModelMoleculeViewProtocol { super.init(frame: .zero) initialSetup() } - + public convenience init() { self.init(frame: .zero) } @@ -44,12 +44,15 @@ open class StackView: UIStackView, ModelMoleculeViewProtocol { } } - // MARK:- ModelMoleculeViewProtocol + //-------------------------------------------------- + // MARK: - ModelMoleculeViewProtocol + //-------------------------------------------------- + open func setWithModel(_ 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(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { @@ -69,7 +72,7 @@ open class StackView: UIStackView, ModelMoleculeViewProtocol { extension StackView: MVMCoreViewProtocol { open func updateView(_ size: CGFloat) {} - + /// Will be called only once. open func setupView() { translatesAutoresizingMaskIntoConstraints = false