diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 94de6eb8..1e5ca199 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -60,6 +60,9 @@ 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 */; }; + 0A14F6A523E4803A00EDF7F7 /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6A423E4803A00EDF7F7 /* StackView.swift */; }; + 0A14F6A723E4AB6E00EDF7F7 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.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 */; }; @@ -377,6 +380,9 @@ 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 = ""; }; + 0A14F6A423E4803A00EDF7F7 /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = ""; }; + 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.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 = ""; }; @@ -1185,6 +1191,8 @@ 0AA33B392398524F0067DD0F /* Toggle.swift */, D260105423CEA7DC00764D80 /* MVMCoreUISwitch+Model.swift */, 012CA99D2385A2D3003F810F /* MFView+ModelExtension.swift */, + 0A14F69223E349EF00EDF7F7 /* PageControl.swift */, + 0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */, ); path = Views; sourceTree = ""; @@ -1351,6 +1359,7 @@ D2B18B802360945C00A9AEDC /* View.swift */, 0AE14F63238315D2005417F8 /* TextField.swift */, 0A5D59C323AD488600EFD9E9 /* Protocols */, + 0A14F6A423E4803A00EDF7F7 /* StackView.swift */, ); path = BaseClasses; sourceTree = ""; @@ -1645,6 +1654,7 @@ 017BEB7F23676E870024EF95 /* MoleculeObjectMapping.swift in Sources */, D274CA332236A78900B01B62 /* FooterView.swift in Sources */, D29DF2BF21E7BEA4003B2FB9 /* MVMCoreUITabBarPageControlViewController.m in Sources */, + 0A14F69323E349EF00EDF7F7 /* PageControl.swift in Sources */, 014AA72423C501E2006F3E93 /* MoleculeContainerModel.swift in Sources */, D29DF28321E7AB24003B2FB9 /* MVMCoreUICommonViewsUtility.m in Sources */, 011B58F223A2AE2C0085F53C /* DropDownListItemModel.swift in Sources */, @@ -1683,6 +1693,7 @@ 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 */, @@ -1714,6 +1725,7 @@ 012A889C23889E8400FE3DA1 /* TemplateModelProtocol.swift in Sources */, D29770FC21F7C77400B2F0D0 /* MVMCoreUITextFieldView.m in Sources */, C003506123AA94CD00B6AC29 /* Button.swift in Sources */, + 0A14F6A523E4803A00EDF7F7 /* StackView.swift in Sources */, DBC4391B224421A0001AB423 /* CaretButton.swift in Sources */, 0198F7A82256A80B0066C936 /* MFRadioButton.m in Sources */, 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */, diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator.swift new file mode 100644 index 00000000..686b1109 --- /dev/null +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator.swift @@ -0,0 +1,15 @@ +// +// 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/PageControl.swift b/MVMCoreUI/Atoms/Views/PageControl.swift new file mode 100644 index 00000000..2f3dada4 --- /dev/null +++ b/MVMCoreUI/Atoms/Views/PageControl.swift @@ -0,0 +1,323 @@ +// +// PageControl.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 1/30/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol PagingIndicatorProtocol: class { + typealias PagingTouchBlock = (PagingIndicatorProtocol) -> () + var currentPage: Int { get } + var numberOfPages: Int { get } + // func setPagingTouchBlock(_ pagingTouchBlock: PagingTouchBlock?) + // func scrollViewDidScroll(_ collectionView: UICollectionView) +} + + +open class PageControl: Control, PagingIndicatorProtocol { + //-------------------------------------------------- + // 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 containerStack.spacing } + set { + containerStack.spacing = newValue + containerStack.layoutIfNeeded() + } + } + public let indicatorBarWidth: CGFloat = 24 + public let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1) + + private(set) var indicators = [BarIndicator]() + + var containerStack: StackView = { + let stack = StackView() + stack.axis = .horizontal + stack.distribution = .equalSpacing + stack.spacing = 6 + return stack + }() + + public var pagingTouchBlock: PagingIndicatorProtocol.PagingTouchBlock? + + // 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 + 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 + + //-------------------------------------------------- + // 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 containerStack.subviews.isEmpty { + if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: isSlidesAccessibile ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") { + accessibilityValue = String(format: accessibleValue, currentPage + 1, numberOfPages) + } + + addSubview(containerStack) + containerStack.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + containerStack.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true + trailingAnchor.constraint(lessThanOrEqualTo: containerStack.trailingAnchor).isActive = true + + topConstraint = containerStack.topAnchor.constraint(equalTo: topAnchor, constant: PaddingThree) + topConstraint?.priority = .defaultHigh + topConstraint?.isActive = true + + bottomConstraint = bottomAnchor.constraint(equalTo: containerStack.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?(self) + } + } + } + + //-------------------------------------------------- + // 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) + } + + // 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. + func accessibilityAdjust(toPage index: Int) { + + if (index < numberOfPages && index >= 0) || alwaysSendingControlEvent { + isAnimated = false + currentPage = index + sendActions(for: .valueChanged) + pagingTouchBlock?(self) + } + } + + func setTopBottomSpace(constant: CGFloat) { + self.bottomConstraint?.constant = constant + self.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/Control.swift b/MVMCoreUI/BaseClasses/Control.swift index d73e0fef..ff3b8508 100644 --- a/MVMCoreUI/BaseClasses/Control.swift +++ b/MVMCoreUI/BaseClasses/Control.swift @@ -12,6 +12,7 @@ import UIKit //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + open var json: [AnyHashable: Any]? open var model: MoleculeModelProtocol? diff --git a/MVMCoreUI/BaseClasses/StackView.swift b/MVMCoreUI/BaseClasses/StackView.swift new file mode 100644 index 00000000..b71273a7 --- /dev/null +++ b/MVMCoreUI/BaseClasses/StackView.swift @@ -0,0 +1,97 @@ +// +// File.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 1/31/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + + +open class StackView: UIStackView, ModelMoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + open var json: [AnyHashable: Any]? + open var model: MoleculeModelProtocol? + + private var initialSetupPerformed = false + + //-------------------------------------------------- + // MARK: - Initialization + //-------------------------------------------------- + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public convenience init() { + self.init(frame: .zero) + } + + public required init(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + + public func initialSetup() { + if !initialSetupPerformed { + initialSetupPerformed = true + setupView() + } + } + + // MARK:- ModelMoleculeViewProtocol + open func setWithModel(_ 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 StackView: 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 StackView: MVMCoreUIMoleculeViewProtocol { + + open func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { + self.json = json + + if let backgroundColorString = json?.optionalStringForKey(KeyBackgroundColor) { + backgroundColor = UIColor.mfGet(forHex: backgroundColorString) + } + } + + open func reset() { + backgroundColor = .clear + } + + open func setAsMolecule() { } +}