shaping up the page control (carouselIndicator)

This commit is contained in:
Kevin G Christiano 2020-01-31 14:26:37 -05:00
parent cd0277bf16
commit 8832bbd0b7
5 changed files with 448 additions and 0 deletions

View File

@ -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 = "<group>"; };
01F2A03123A4498200D954D8 /* CaretLinkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretLinkModel.swift; sourceTree = "<group>"; };
0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionDetailWithImage.swift; sourceTree = "<group>"; };
0A14F69223E349EF00EDF7F7 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControl.swift; sourceTree = "<group>"; };
0A14F6A423E4803A00EDF7F7 /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = "<group>"; };
0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = "<group>"; };
0A209CD223A7E2810068F8B0 /* UIStackViewAlignment+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackViewAlignment+Extension.swift"; sourceTree = "<group>"; };
0A21DB7E235DECC500C160A2 /* EntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = "<group>"; };
0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnEntryField.swift; sourceTree = "<group>"; };
@ -1185,6 +1191,8 @@
0AA33B392398524F0067DD0F /* Toggle.swift */,
D260105423CEA7DC00764D80 /* MVMCoreUISwitch+Model.swift */,
012CA99D2385A2D3003F810F /* MFView+ModelExtension.swift */,
0A14F69223E349EF00EDF7F7 /* PageControl.swift */,
0A14F6A623E4AB6E00EDF7F7 /* CarouselIndicator.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1351,6 +1359,7 @@
D2B18B802360945C00A9AEDC /* View.swift */,
0AE14F63238315D2005417F8 /* TextField.swift */,
0A5D59C323AD488600EFD9E9 /* Protocols */,
0A14F6A423E4803A00EDF7F7 /* StackView.swift */,
);
path = BaseClasses;
sourceTree = "<group>";
@ -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 */,

View File

@ -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 {
}

View File

@ -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..<numberOfPages {
let indicator = BarIndicator()
newIndicators.append(indicator)
indicator.heightAnchor.constraint(equalToConstant: i == currentPage ? indicatorBarHeight.selected : indicatorBarHeight.unselected).isActive = true
indicator.widthAnchor.constraint(equalToConstant: indicatorBarWidth).isActive = true
indicator.backgroundColor = indicatorTintColor
}
self.indicators = newIndicators
}
/// Removes all indicators from their subview and then clears the holding array.
func removeIndicators() {
indicators.forEach { $0.removeFromSuperview() }
indicators = []
}
//--------------------------------------------------
// MARK: - Actions
//--------------------------------------------------
func indicatorTapped(_ tapGesture: UITapGestureRecognizer?) {
if isUserInteractionEnabled {
let touchPoint_X = tapGesture?.location(in: containerStack).x ?? 0.0
let index = indicators.firstIndex { indicator in
return indicator.frame.maxX >= 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")
}
}
}

View File

@ -12,6 +12,7 @@ import UIKit
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var json: [AnyHashable: Any]?
open var model: MoleculeModelProtocol?

View File

@ -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<MVMCoreErrorObject?>?) -> [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() { }
}