latest carousel state

This commit is contained in:
Kevin G Christiano 2020-02-17 13:33:25 -05:00
parent 5558cc8324
commit faa9c94f90
10 changed files with 193 additions and 103 deletions

View File

@ -15,15 +15,6 @@ public protocol IndicatorViewProtocol {
var isEnabled: Bool { get set } var isEnabled: Bool { get set }
} }
/// Contracts behavior between carousel and its page control.
public protocol CarouselPageControlProtocol {
typealias PagingTouchBlock = ((CarouselPageControlProtocol)) -> ()
var currentIndex: Int { get set }
var numberOfPages: Int { get set }
var indicatorTouchAction: PagingTouchBlock? { get set }
func scrollViewDidScroll(_ collectionView: UICollectionView)
}
open class CarouselIndicator: Control, CarouselPageControlProtocol { open class CarouselIndicator: Control, CarouselPageControlProtocol {
//-------------------------------------------------- //--------------------------------------------------
@ -83,7 +74,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
/// Set this closure to perform an action when a different indicator was selected. /// Set this closure to perform an action when a different indicator was selected.
/// Passes through oldInde and newIndex, respectively. /// Passes through oldInde and newIndex, respectively.
public var indicatorTouchAction: CarouselIndicator.PagingTouchBlock? public var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())?
/// Allows sendActions() to trigger even if index is already at min/max index. /// Allows sendActions() to trigger even if index is already at min/max index.
public var alwaysSendAction = false public var alwaysSendAction = false
@ -107,7 +98,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
indicatorView?.isEnabled = isEnabled indicatorView?.isEnabled = isEnabled
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Computed Properties // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
@ -160,7 +151,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
_indicatorTintColor = newColor _indicatorTintColor = newColor
if isBarIndicator(), let barIndicator = indicatorView as? BarsIndicatorView { if isBarIndicator(), let barIndicator = indicatorView as? BarsIndicatorView {
for (i, barTuple) in barIndicator.barsReference.enumerated() where i != currentIndex { for (i, barTuple) in barIndicator.barReferences.enumerated() where i != currentIndex {
barTuple.view.backgroundColor = newColor barTuple.view.backgroundColor = newColor
} }
} }
@ -177,7 +168,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
if isBarIndicator() { if isBarIndicator() {
if let barIndicator = indicatorView as? BarsIndicatorView { if let barIndicator = indicatorView as? BarsIndicatorView {
barIndicator.barsReference[currentIndex].view.backgroundColor = newColor barIndicator.barReferences[currentIndex].view.backgroundColor = newColor
} }
} }
} }
@ -265,9 +256,11 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
let touchPoint = tapGesture?.location(in: self) let touchPoint = tapGesture?.location(in: self)
let touchPoint_X = touchPoint?.x ?? 0.0 let touchPoint_X = touchPoint?.x ?? 0.0
if isEnabled, indicatorType == .bar, let bars = (indicatorView as? BarsIndicatorView)?.barsReference { if isBarIndicator(), let bars = (indicatorView as? BarsIndicatorView)?.barReferences {
currentIndex = bars.firstIndex { $0.0.frame.maxX >= touchPoint_X && $0.0.frame.minX <= touchPoint_X } ?? 0 currentIndex = bars.firstIndex { $0.0.frame.maxX >= touchPoint_X && $0.0.frame.minX <= touchPoint_X } ?? 0
} else { } else {
// Determine which half of the view was touched.
if touchPoint_X > bounds.width / 2 { if touchPoint_X > bounds.width / 2 {
incrementCurrentIndex() incrementCurrentIndex()
} else { } else {
@ -289,7 +282,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
} }
public func performAction() { public func performAction() {
sendActions(for: .valueChanged) sendActions(for: .valueChanged)
indicatorTouchAction?(self) indicatorTouchAction?(self)
} }
@ -311,13 +304,11 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
/// Convenience to determine if current view is displaying bars. /// Convenience to determine if current view is displaying bars.
func isBarIndicator() -> Bool { func isBarIndicator() -> Bool {
return indicatorType != .bar && numberOfPages > hybridThreshold return indicatorType != .bar && numberOfPages > hybridThreshold
} }
public func scrollViewDidScroll(_ collectionView: UICollectionView) { public func scrollViewDidScroll(_ collectionView: UICollectionView) { }
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MoleculeViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------

View File

@ -15,7 +15,7 @@ public class CarouselIndicatorModel: MoleculeModelProtocol {
//-------------------------------------------------- //--------------------------------------------------
public var backgroundColor: Color? public var backgroundColor: Color?
public static var identifier: String { public static var identifier: String {
return "carouselIndicator" return "carouselIndicator"
} }
@ -33,8 +33,9 @@ public class CarouselIndicatorModel: MoleculeModelProtocol {
public var accessibilityHasSlidesInsteadOfPage: Bool? = false public var accessibilityHasSlidesInsteadOfPage: Bool? = false
public var isEnabled: Bool? = false public var isEnabled: Bool? = false
public var disabledIndicatorColor: Color? = Color(uiColor: .mvmCoolGray3) public var disabledIndicatorColor: Color? = Color(uiColor: .mvmCoolGray3)
public var indicatorTintColor: Color? = Color(uiColor: .black) public var indicatorTintColor: Color? = Color(uiColor: .mvmBlack)
public var currentIndicatorColor: Color? = Color(uiColor: .black) public var currentIndicatorColor: Color? = Color(uiColor: .mvmBlack)
// public var position: Float?
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Keys // MARK: - Keys
@ -44,8 +45,19 @@ public class CarouselIndicatorModel: MoleculeModelProtocol {
case moleculeName case moleculeName
case backgroundColor case backgroundColor
case type case type
case hybridThreshold
case barsColor case barsColor
case currentBarColor case currentBarColor
case currentIndex
case numberOfPages
case alwaysSendEvent
case isAnimated
case hidesForSinglePage
case accessibilityHasSlidesInsteadOfPage
case isEnabled
case disabledIndicatorColor
case indicatorTintColor
case currentIndicatorColor
} }
//-------------------------------------------------- //--------------------------------------------------
@ -54,18 +66,45 @@ public class CarouselIndicatorModel: MoleculeModelProtocol {
required public init(from decoder: Decoder) throws { required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName)
currentBarColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentBarColor) currentBarColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentBarColor)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
barsColor = try typeContainer.decodeIfPresent(Color.self, forKey: .barsColor) barsColor = try typeContainer.decodeIfPresent(Color.self, forKey: .barsColor)
type = try typeContainer.decodeIfPresent(String.self, forKey: .type) ?? "hybrid" type = try typeContainer.decodeIfPresent(String.self, forKey: .type) ?? "hybrid"
hybridThreshold = try typeContainer.decodeIfPresent(Int.self, forKey: .hybridThreshold) ?? 5
barsColor = try typeContainer.decodeIfPresent(Color.self, forKey: .barsColor)
currentBarColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentBarColor)
currentIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .currentIndex) ?? 0
numberOfPages = try typeContainer.decodeIfPresent(Int.self, forKey: .numberOfPages) ?? 0
alwaysSendEvent = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysSendEvent) ?? false
isAnimated = try typeContainer.decodeIfPresent(Bool.self, forKey: .isAnimated) ?? true
hidesForSinglePage = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesForSinglePage) ?? false
accessibilityHasSlidesInsteadOfPage = try typeContainer.decodeIfPresent(Bool.self, forKey: .accessibilityHasSlidesInsteadOfPage) ?? false
isEnabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? false
disabledIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledIndicatorColor) ?? Color(uiColor: .mvmCoolGray3)
indicatorTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .indicatorTintColor) ?? Color(uiColor: .mvmBlack)
currentIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentIndicatorColor) ?? Color(uiColor: .mvmBlack)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(barsColor, forKey: .barsColor) try container.encodeIfPresent(barsColor, forKey: .barsColor)
try container.encodeIfPresent(currentBarColor, forKey: .currentBarColor) try container.encodeIfPresent(currentBarColor, forKey: .currentBarColor)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(type, forKey: .type) try container.encodeIfPresent(type, forKey: .type)
try container.encodeIfPresent(hybridThreshold, forKey: .hybridThreshold)
try container.encodeIfPresent(barsColor, forKey: .barsColor)
try container.encodeIfPresent(currentBarColor, forKey: .currentBarColor)
try container.encodeIfPresent(currentIndex, forKey: .currentIndex)
try container.encodeIfPresent(numberOfPages, forKey: .numberOfPages)
try container.encodeIfPresent(alwaysSendEvent, forKey: .alwaysSendEvent)
try container.encodeIfPresent(isAnimated, forKey: .isAnimated)
try container.encodeIfPresent(hidesForSinglePage, forKey: .hidesForSinglePage)
try container.encodeIfPresent(accessibilityHasSlidesInsteadOfPage, forKey: .accessibilityHasSlidesInsteadOfPage)
try container.encodeIfPresent(isEnabled, forKey: .isEnabled)
try container.encodeIfPresent(disabledIndicatorColor, forKey: .disabledIndicatorColor)
try container.encodeIfPresent(indicatorTintColor, forKey: .indicatorTintColor)
try container.encodeIfPresent(currentIndicatorColor, forKey: .currentIndicatorColor)
} }
} }

View File

@ -11,7 +11,7 @@ import UIKit
open class BarsIndicatorView: View, IndicatorViewProtocol { open class BarsIndicatorView: View, IndicatorViewProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Stored Properties
//-------------------------------------------------- //--------------------------------------------------
let stackView: StackView = { let stackView: StackView = {
@ -19,40 +19,49 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
stackView.axis = .horizontal stackView.axis = .horizontal
stackView.distribution = .equalSpacing stackView.distribution = .equalSpacing
stackView.spacing = 6 stackView.spacing = 6
stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: BarsIndicatorView.indicatorBarHeight.selected).isActive = true
return stackView return stackView
}() }()
public var barsReference: [(view: View, constraint: NSLayoutConstraint)] = [] public var barReferences: [(view: View, constraint: NSLayoutConstraint)] = []
// Dimensions are based on InVision Design Guidelines. // Dimensions are based on InVision Design Guidelines.
public static let indicatorBarWidth: CGFloat = 24 public static let indicatorBarWidth: CGFloat = 24
public static let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1) public static let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1)
//--------------------------------------------------
// MARK: - Computed Properties
//--------------------------------------------------
public var parentCarouselIndicator: CarouselIndicator? {
return superview as? CarouselIndicator
}
public var enabledColor: UIColor { public var enabledColor: UIColor {
return (superview as? CarouselIndicator)?.indicatorTintColor ?? .black return parentCarouselIndicator?.indicatorTintColor ?? .mvmBlack
} }
public var currentIndexColor: UIColor { public var currentIndexColor: UIColor {
return (superview as? CarouselIndicator)?.currentIndicatorColor ?? .black return parentCarouselIndicator?.currentIndicatorColor ?? .mvmBlack
} }
public var disabledColor: UIColor { public var disabledColor: UIColor {
return (superview as? CarouselIndicator)?.disabledIndicatorColor ?? .mvmCoolGray3 return parentCarouselIndicator?.disabledIndicatorColor ?? .mvmCoolGray3
} }
/// Returns the numberOfPages count from its parent CarouselIndicator. /// Returns the numberOfPages count from its parent CarouselIndicator.
public var numberOfPages: Int? { public var numberOfPages: Int? {
return (superview as? CarouselIndicator)?.numberOfPages return parentCarouselIndicator?.numberOfPages
} }
/// Returns the numberOfPages count from its parent CarouselIndicator. /// Returns the numberOfPages count from its parent CarouselIndicator.
public var currentIndex: Int? { public var currentIndex: Int? {
return (superview as? CarouselIndicator)?.currentIndex return parentCarouselIndicator?.currentIndex
} }
open var isEnabled: Bool = true { open var isEnabled: Bool = true {
didSet { didSet {
barsReference.forEach { view, heightConstraint in barReferences.forEach { view, heightConstraint in
view.backgroundColor = isEnabled ? enabledColor : disabledColor view.backgroundColor = isEnabled ? enabledColor : disabledColor
} }
} }
@ -82,7 +91,7 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
stackView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true stackView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
stackView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true stackView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor).isActive = true
trailingAnchor.constraint(lessThanOrEqualTo: stackView.trailingAnchor).isActive = true trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor).isActive = true
generateBars() generateBars()
} }
@ -102,7 +111,7 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
for i in 0..<numberOfPages { for i in 0..<numberOfPages {
let bar = View() let bar = View()
bar.widthAnchor.constraint(equalToConstant: BarsIndicatorView.indicatorBarWidth).isActive = true bar.widthAnchor.constraint(equalToConstant: BarsIndicatorView.indicatorBarWidth).isActive = true
bar.backgroundColor = enabledColor bar.backgroundColor = isEnabled ? enabledColor : disabledColor
let barHeight = i == currentIndex ? BarsIndicatorView.indicatorBarHeight.selected : BarsIndicatorView.indicatorBarHeight.unselected let barHeight = i == currentIndex ? BarsIndicatorView.indicatorBarHeight.selected : BarsIndicatorView.indicatorBarHeight.unselected
let heightConstraint = bar.heightAnchor.constraint(equalToConstant: barHeight) let heightConstraint = bar.heightAnchor.constraint(equalToConstant: barHeight)
heightConstraint.isActive = true heightConstraint.isActive = true
@ -111,7 +120,7 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
bars.append((bar, heightConstraint)) bars.append((bar, heightConstraint))
} }
barsReference = bars barReferences = bars
} }
//-------------------------------------------------- //--------------------------------------------------
@ -120,17 +129,17 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
public override func reset() { public override func reset() {
super.reset() super.reset()
barsReference.forEach { $0.view.removeFromSuperview() } barReferences.forEach { $0.view.removeFromSuperview() }
barsReference = [] barReferences = []
} }
public func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { public func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) {
let expression = { let expression = {
self.barsReference[previousIndex].view.backgroundColor = self.enabledColor self.barReferences[previousIndex].view.backgroundColor = self.enabledColor
self.barsReference[newIndex].view.backgroundColor = self.currentIndexColor self.barReferences[newIndex].view.backgroundColor = self.currentIndexColor
self.barsReference[previousIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.unselected self.barReferences[previousIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.unselected
self.barsReference[newIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.selected self.barReferences[newIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.selected
self.layoutIfNeeded() self.layoutIfNeeded()
} }

View File

@ -15,7 +15,7 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
//-------------------------------------------------- //--------------------------------------------------
/// Text to display the current count of total pages for viewing. /// Text to display the current count of total pages for viewing.
open var titleLabel: Label = { open var pageCountLabel: Label = {
let label = Label.commonLabelB2(true) let label = Label.commonLabelB2(true)
label.setContentCompressionResistancePriority(.required, for: .vertical) label.setContentCompressionResistancePriority(.required, for: .vertical)
label.textAlignment = .center label.textAlignment = .center
@ -25,41 +25,43 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
let leftArrow: ImageView = { let leftArrow: ImageView = {
let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate).withHorizontallyFlippedOrientation() let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate).withHorizontallyFlippedOrientation()
let imageView = ImageView(image: arrow) let imageView = ImageView(image: arrow)
imageView.isUserInteractionEnabled = true
imageView.tintColor = .mvmBlack imageView.tintColor = .mvmBlack
imageView.heightAnchor.constraint(equalToConstant: PaddingTwo).isActive = true
imageView.widthAnchor.constraint(equalToConstant: PaddingTwo).isActive = true
return imageView return imageView
}() }()
let rightArrow: ImageView = { let rightArrow: ImageView = {
let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate) let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate)
let imageView = ImageView(image: arrow) let imageView = ImageView(image: arrow)
imageView.isUserInteractionEnabled = true
imageView.tintColor = .mvmBlack imageView.tintColor = .mvmBlack
imageView.heightAnchor.constraint(equalToConstant: PaddingTwo).isActive = true
imageView.widthAnchor.constraint(equalToConstant: PaddingTwo).isActive = true
return imageView return imageView
}() }()
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
open var isEnabled: Bool = true { open var isEnabled: Bool = true {
didSet { didSet {
titleLabel.isEnabled = isEnabled pageCountLabel.textColor = isEnabled ? enabledColor : disabledColor
leftArrow.tintColor = isEnabled ? enabledColor : disabledColor leftArrow.tintColor = isEnabled ? enabledColor : disabledColor
rightArrow.tintColor = isEnabled ? enabledColor : disabledColor rightArrow.tintColor = isEnabled ? enabledColor : disabledColor
} }
} }
public var parentCarouselIndicator: CarouselIndicator? {
return superview as? CarouselIndicator
}
public var enabledColor: UIColor { public var enabledColor: UIColor {
return (superview as? CarouselIndicator)?.indicatorTintColor ?? .black return parentCarouselIndicator?.indicatorTintColor ?? .mvmBlack
} }
public var disabledColor: UIColor { public var disabledColor: UIColor {
return (superview as? CarouselIndicator)?.disabledIndicatorColor ?? .mvmCoolGray3 return parentCarouselIndicator?.disabledIndicatorColor ?? .mvmCoolGray3
}
public var parentCarouselIndicator: CarouselIndicator? {
return superview as? CarouselIndicator
} }
/// Returns the currentIndex from its parent CarouselIndicator. /// Returns the currentIndex from its parent CarouselIndicator.
@ -98,7 +100,7 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
open override func updateView(_ size: CGFloat) { open override func updateView(_ size: CGFloat) {
super.updateView(size) super.updateView(size)
titleLabel.updateView(size) pageCountLabel.updateView(size)
} }
//-------------------------------------------------- //--------------------------------------------------
@ -109,29 +111,24 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
super.setupView() super.setupView()
isUserInteractionEnabled = false isUserInteractionEnabled = false
leftArrow.tintColor = isEnabled ? enabledColor : disabledColor
rightArrow.tintColor = isEnabled ? enabledColor : disabledColor
addSubview(titleLabel) addSubview(pageCountLabel)
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true pageCountLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
NSLayoutConstraint.constraintPinSubview(titleLabel, pinTop: true, pinBottom: true, pinLeft: false, pinRight: false) pageCountLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true
bottomAnchor.constraint(equalTo: pageCountLabel.bottomAnchor).isActive = true
addSubview(leftArrow) addSubview(leftArrow)
NSLayoutConstraint.constraintPinView(leftArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo)
leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
addSubview(rightArrow) addSubview(rightArrow)
NSLayoutConstraint.constraintPinView(rightArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo)
rightArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true rightArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
rightArrow.isUserInteractionEnabled = true
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[leftArrowView]-(padding)-[titleLabel]-(padding)-[rightArrowView]-0-|", NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[leftArrow]-(padding)-[pageCountLabel]-(padding)-[rightArrow]|",
options: .directionLeadingToTrailing, options: .directionLeadingToTrailing,
metrics: ["padding": PaddingOne], metrics: ["padding": PaddingOne],
views: ["leftArrowView": leftArrow, views: ["leftArrow": leftArrow,
"titleLabel": titleLabel, "pageCountLabel": pageCountLabel,
"rightArrowView": rightArrow])) "rightArrow": rightArrow]))
} }
//-------------------------------------------------- //--------------------------------------------------
@ -143,7 +140,7 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.titleLabel.text = "\(newIndex)/\(totalCount)" self.pageCountLabel.text = "\(newIndex)/\(totalCount)"
self.layoutIfNeeded() self.layoutIfNeeded()
} }
} }

View File

@ -8,16 +8,16 @@
import UIKit import UIKit
class ImageView: UIImageView, ModelMoleculeViewProtocol { open class ImageView: UIImageView, ModelMoleculeViewProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
open var json: [AnyHashable: Any]? open var json: [AnyHashable: Any]?
open var model: MoleculeModelProtocol? open var model: MoleculeModelProtocol?
private var initialSetupPerformed = false private var initialSetupPerformed = false
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Initialization // MARK: - Initialization
//-------------------------------------------------- //--------------------------------------------------
@ -31,7 +31,7 @@ class ImageView: UIImageView, ModelMoleculeViewProtocol {
super.init(image: image) super.init(image: image)
initialSetup() initialSetup()
} }
public convenience init() { public convenience init() {
self.init(frame: .zero) self.init(frame: .zero)
} }
@ -41,6 +41,10 @@ class ImageView: UIImageView, ModelMoleculeViewProtocol {
initialSetup() initialSetup()
} }
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
public func initialSetup() { public func initialSetup() {
if !initialSetupPerformed { if !initialSetupPerformed {
initialSetupPerformed = true initialSetupPerformed = true
@ -48,12 +52,15 @@ class ImageView: UIImageView, ModelMoleculeViewProtocol {
} }
} }
// MARK:- ModelMoleculeViewProtocol //--------------------------------------------------
// MARK: - ModelMoleculeViewProtocol
//--------------------------------------------------
open func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { open func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.model = model self.model = model
if let backgroundColor = model?.backgroundColor { if let backgroundColor = model?.backgroundColor {
self.backgroundColor = backgroundColor.uiColor self.backgroundColor = backgroundColor.uiColor
} }
} }
open class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { open class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
@ -72,8 +79,8 @@ class ImageView: UIImageView, ModelMoleculeViewProtocol {
// MARK:- MVMCoreViewProtocol // MARK:- MVMCoreViewProtocol
extension ImageView: MVMCoreViewProtocol { extension ImageView: MVMCoreViewProtocol {
open func updateView(_ size: CGFloat) {} open func updateView(_ size: CGFloat) { }
/// Will be called only once. /// Will be called only once.
open func setupView() { open func setupView() {
translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false
@ -99,4 +106,3 @@ extension ImageView: MVMCoreUIMoleculeViewProtocol {
open func setAsMolecule() { } open func setAsMolecule() { }
} }

View File

@ -9,17 +9,29 @@
import Foundation import Foundation
public class MoleculeContainerModel: ContainerModel { public class MoleculeContainerModel: ContainerModel {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var molecule: MoleculeModelProtocol public var molecule: MoleculeModelProtocol
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case molecule case molecule
} }
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(with moleculeModel: MoleculeModelProtocol) { public init(with moleculeModel: MoleculeModelProtocol) {
molecule = moleculeModel molecule = moleculeModel
super.init() super.init()
} }
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws { required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
molecule = try typeContainer.decodeMolecule(codingKey: .molecule) molecule = try typeContainer.decodeMolecule(codingKey: .molecule)

View File

@ -8,6 +8,15 @@
import UIKit 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 { open class Carousel: View {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Properties
@ -33,7 +42,7 @@ open class Carousel: View {
var molecules: [MoleculeModelProtocol]? var molecules: [MoleculeModelProtocol]?
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%. /// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
var itemAlignment = UICollectionView.ScrollPosition.left var itemAlignment: UICollectionView.ScrollPosition = .left
/// From 0-1. The item width as a percent of the carousel width. /// From 0-1. The item width as a percent of the carousel width.
var itemWidthPercent: Float = 1 var itemWidthPercent: Float = 1
@ -42,10 +51,11 @@ open class Carousel: View {
var collectionViewHeight: NSLayoutConstraint? var collectionViewHeight: NSLayoutConstraint?
/// The view that we use for paging /// The view that we use for paging
var pagingView: (UIView & MVMCoreUIPagingProtocol)? var pagingView: (UIView & CarouselPageControlProtocol)?
/// If the carousel should loop after scrolling past the first and final cells. /// If the carousel should loop after scrolling past the first and final cells.
var loop = false var loop = false
private var dragging = false private var dragging = false
/// For adding pager /// For adding pager
@ -141,9 +151,17 @@ open class Carousel: View {
if carouselModel?.loop ?? false && newMolecules.count > 2 { 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). // 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 loop = true
molecules?.insert(newMolecules.last!, at: 0)
if let last = newMolecules.last {
molecules?.insert(last, at: 0)
}
molecules?.insert(newMolecules[(newMolecules.count - 2)], at: 0) molecules?.insert(newMolecules[(newMolecules.count - 2)], at: 0)
molecules?.append(newMolecules.first!)
if let first = newMolecules.first {
molecules?.append(first)
}
molecules?.append(newMolecules[1]) molecules?.append(newMolecules[1])
} }
@ -153,9 +171,10 @@ open class Carousel: View {
/// Sets up the paging molecule /// Sets up the paging molecule
open func setupPagingMolecule(_ molecule: CarouselPagingModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) { open func setupPagingMolecule(_ molecule: CarouselPagingModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) {
var pagingView: (UIView & MVMCoreUIPagingProtocol)? = nil var pagingView: (UIView & CarouselPageControlProtocol)? = nil
if let molecule = molecule { if let molecule = molecule {
pagingView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(molecule, delegateObject, false) as? (UIView & MVMCoreUIPagingProtocol) pagingView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(molecule, delegateObject, false) as? (UIView & CarouselPageControlProtocol)
} }
addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20))) addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20)))
@ -163,6 +182,7 @@ open class Carousel: View {
/// Registers the cells with the collection view /// Registers the cells with the collection view
func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) { func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) {
for molecule in carouselModel.molecules { for molecule in carouselModel.molecules {
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) { if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier) collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
@ -176,6 +196,7 @@ open class Carousel: View {
/// Returns the (identifier, class) of the molecule for the given map. /// Returns the (identifier, class) of the molecule for the given map.
func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? { func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? {
guard let className = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(molecule), guard let className = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(molecule),
let moleculeName = (className as? ModelMoleculeViewProtocol.Type)?.nameForReuse(molecule, delegateObject) ?? molecule.moleculeName let moleculeName = (className as? ModelMoleculeViewProtocol.Type)?.nameForReuse(molecule, delegateObject) ?? molecule.moleculeName
else { return nil } else { return nil }
@ -189,26 +210,29 @@ open class Carousel: View {
switch string { switch string {
case "leading": case "leading":
itemAlignment = .left itemAlignment = .left
case "trailing": case "trailing":
itemAlignment = .right itemAlignment = .right
case "center": case "center":
itemAlignment = .centeredHorizontally itemAlignment = .centeredHorizontally
default: break
default:
break
} }
} }
/// 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. /// 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() pagingView?.removeFromSuperview()
guard let pagingView = view else { guard var pagingView = view else {
bottomPin?.isActive = false bottomPin?.isActive = false
bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
bottomPin?.isActive = true bottomPin?.isActive = true
return return
} }
pagingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(pagingView) addSubview(pagingView)
pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true
@ -218,16 +242,17 @@ open class Carousel: View {
bottomPin?.priority = .defaultLow bottomPin?.priority = .defaultLow
bottomPin?.isActive = true bottomPin?.isActive = true
pagingView.setNumberOfPages(numberOfPages) pagingView.numberOfPages = numberOfPages
(pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill) (pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill)
pagingView.setPagingTouch { [weak self] pager in pagingView.indicatorTouchAction = { [weak self] pager in
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self = self else { return } guard let self = self else { return }
let currentPage = pager.currentPage() let currentPage = pager.currentIndex
self.pageIndex = currentPage self.pageIndex = currentPage
self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
} }
} }
self.pagingView = pagingView self.pagingView = pagingView
} }
@ -236,6 +261,7 @@ open class Carousel: View {
if peaking && !UIAccessibility.isVoiceOverRunning { if peaking && !UIAccessibility.isVoiceOverRunning {
// Show overlay and arrow in peaking Cell // Show overlay and arrow in peaking Cell
let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row } let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row }
if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex { if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex {
(collectionView.cellForItem(at: firstItem) as? MoleculeCollectionViewCell)?.setPeaking(true, animated: true) (collectionView.cellForItem(at: firstItem) as? MoleculeCollectionViewCell)?.setPeaking(true, animated: true)
} }
@ -257,10 +283,12 @@ open class Carousel: View {
cell.accessibilityElementsHidden = false cell.accessibilityElementsHidden = false
var array = cell.accessibilityElements var array = cell.accessibilityElements
if let acc = pagingView?.accessibilityElements { if let pagingView = pagingView {
array?.append(contentsOf: acc) if let acc = pagingView.accessibilityElements {
} else { array?.append(contentsOf: acc)
array?.append(pagingView!) } else {
array?.append(pagingView)
}
} }
accessibilityElements = array accessibilityElements = array
@ -328,7 +356,7 @@ extension Carousel: UIScrollViewDelegate {
let goToIndex = { (index: Int) in let goToIndex = { (index: Int) in
self.goTo(index, animated: false) self.goTo(index, animated: false)
self.collectionView.layoutIfNeeded() self.collectionView.layoutIfNeeded()
self.pagingView?.setPage(self.pageIndex) self.pagingView?.currentIndex = self.pageIndex
} }
if currentIndex < 2 { if currentIndex < 2 {
@ -367,7 +395,7 @@ extension Carousel: UIScrollViewDelegate {
checkForDraggingOutOfBounds(scrollView) checkForDraggingOutOfBounds(scrollView)
// Let the pager know our progress if needed. // Let the pager know our progress if needed.
pagingView?.scrollViewDidScroll?(collectionView) pagingView?.scrollViewDidScroll(collectionView)
} }
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
@ -386,12 +414,13 @@ extension Carousel: UIScrollViewDelegate {
// We switch cards if we pass the velocity threshold or position threshold (currently 50%). // We switch cards if we pass the velocity threshold or position threshold (currently 50%).
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) 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 lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
let velocityThreshold: CGFloat = 1.1 let velocityThreshold: CGFloat = 1.1
if velocity.x > velocityThreshold { if velocity.x > velocityThreshold {
cellToSwipeTo = currentIndex + 1 cellToSwipeTo = currentIndex + 1
} else if velocity.x < -velocityThreshold { } else if velocity.x < -velocityThreshold {
cellToSwipeTo = currentIndex - 1 cellToSwipeTo = currentIndex - 1
} }
@ -404,7 +433,7 @@ extension Carousel: UIScrollViewDelegate {
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// Cycle to other end if on buffer cell. // Cycle to other end if on buffer cell.
handleUserOnBufferCell() handleUserOnBufferCell()
pagingView?.setPage(pageIndex) pagingView?.currentIndex = pageIndex
showPeaking(true) showPeaking(true)
} }
} }

View File

@ -13,7 +13,10 @@ import UIKit
// MARK: - Properties // MARK: - Properties
//-------------------------------------------------- //--------------------------------------------------
public static var identifier: String = "carousel" public static var identifier: String {
return "carousel"
}
public var backgroundColor: Color? public var backgroundColor: Color?
public var molecules: [CarouselItemModel] public var molecules: [CarouselItemModel]
public var moleculeName: String? public var moleculeName: String?
@ -29,7 +32,7 @@ import UIKit
// MARK: - Initializer // MARK: - Initializer
//-------------------------------------------------- //--------------------------------------------------
public init(molecules: [CarouselItemModel]){ public init(molecules: [CarouselItemModel]) {
self.molecules = molecules self.molecules = molecules
} }
@ -56,6 +59,7 @@ import UIKit
required public init(from decoder: Decoder) throws { required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName)
molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules) molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
spacing = try typeContainer.decodeIfPresent(Float.self, forKey: .spacing) spacing = try typeContainer.decodeIfPresent(Float.self, forKey: .spacing)

View File

@ -19,6 +19,7 @@ public extension MVMCoreUIMoleculeMappingObject {
if let moleculeName = model.moleculeName { if let moleculeName = model.moleculeName {
return moleculeMapping.object(forKey: moleculeName) as? AnyClass return moleculeMapping.object(forKey: moleculeName) as? AnyClass
} }
return nil return nil
} }
@ -32,7 +33,7 @@ public extension MVMCoreUIMoleculeMappingObject {
return nil return nil
} }
let setData = {() in let setData = {
if let molecule = molecule as? ModelMoleculeViewProtocol { if let molecule = molecule as? ModelMoleculeViewProtocol {
molecule.setWithModel(model, delegateObject, nil) molecule.setWithModel(model, delegateObject, nil)
} else { } else {
@ -45,6 +46,7 @@ public extension MVMCoreUIMoleculeMappingObject {
let view = ViewConstrainingView(molecule: molecule, alignment: castMolecule.horizontalAlignment?() ?? .fill) let view = ViewConstrainingView(molecule: molecule, alignment: castMolecule.horizontalAlignment?() ?? .fill)
setData() setData()
return view return view
} else { } else {
setData() setData()
return molecule return molecule

View File

@ -8,7 +8,9 @@
import Foundation import Foundation
@objcMembers public class MoleculeObjectMapping: NSObject { @objcMembers public class MoleculeObjectMapping: NSObject {
public static func registerObjects() { public static func registerObjects() {
// Stacks // Stacks
MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: MoleculeStackView.self, viewModelClass: MoleculeStackModel.self) MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: MoleculeStackView.self, viewModelClass: MoleculeStackModel.self)
@ -95,7 +97,6 @@ import Foundation
// Other Organisms // Other Organisms
MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: Carousel.self, viewModelClass: CarouselModel.self) MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: Carousel.self, viewModelClass: CarouselModel.self)
MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: CarouselIndicator.self, viewModelClass: CarouselIndicatorModel.self) MVMCoreUIMoleculeMappingObject.shared()?.register(viewClass: CarouselIndicator.self, viewModelClass: CarouselIndicatorModel.self)
// TODO: Need model // TODO: Need model