adding new base class. improving carousel logic.

This commit is contained in:
Kevin G Christiano 2020-02-13 15:33:55 -05:00
parent 6ede310cd0
commit 5558cc8324
8 changed files with 291 additions and 75 deletions

View File

@ -85,6 +85,7 @@
0A4253AF23F5C2C100554656 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4253AE23F5C2C000554656 /* BarsIndicatorView.swift */; }; 0A4253AF23F5C2C100554656 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4253AE23F5C2C000554656 /* BarsIndicatorView.swift */; };
0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */; }; 0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */; };
0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */; }; 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */; };
0A7918F523F5E7EA00772FF4 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7918F423F5E7EA00772FF4 /* ImageView.swift */; };
0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; }; 0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; };
0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; };
0A7EF85B23D8A52800B2AAD1 /* EntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */; }; 0A7EF85B23D8A52800B2AAD1 /* EntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */; };
@ -412,6 +413,7 @@
0A4253AE23F5C2C000554656 /* BarsIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsIndicatorView.swift; sourceTree = "<group>"; }; 0A4253AE23F5C2C000554656 /* BarsIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsIndicatorView.swift; sourceTree = "<group>"; };
0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesProtocol.swift; sourceTree = "<group>"; }; 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesProtocol.swift; sourceTree = "<group>"; };
0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = "<group>"; }; 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = "<group>"; };
0A7918F423F5E7EA00772FF4 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = "<group>"; }; 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = "<group>"; };
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxLabel.swift; sourceTree = "<group>"; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxLabel.swift; sourceTree = "<group>"; };
@ -1471,6 +1473,7 @@
D2755D7A23689C7500485468 /* TableViewCell.swift */, D2755D7A23689C7500485468 /* TableViewCell.swift */,
0A5D59C323AD488600EFD9E9 /* Protocols */, 0A5D59C323AD488600EFD9E9 /* Protocols */,
0A14F6A423E4803A00EDF7F7 /* StackView.swift */, 0A14F6A423E4803A00EDF7F7 /* StackView.swift */,
0A7918F423F5E7EA00772FF4 /* ImageView.swift */,
); );
path = BaseClasses; path = BaseClasses;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1819,6 +1822,7 @@
01EB3684236097C0006832FA /* MoleculeModelProtocol.swift in Sources */, 01EB3684236097C0006832FA /* MoleculeModelProtocol.swift in Sources */,
D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */, D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */,
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
0A7918F523F5E7EA00772FF4 /* ImageView.swift in Sources */,
0198F79F225679880066C936 /* FormValidationProtocol.swift in Sources */, 0198F79F225679880066C936 /* FormValidationProtocol.swift in Sources */,
D243859923A16B1800332775 /* Container.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */,
D2C521A923EDE79E00CA2634 /* ViewController.swift in Sources */, D2C521A923EDE79E00CA2634 /* ViewController.swift in Sources */,

View File

@ -8,14 +8,24 @@
import Foundation import Foundation
/// Set protocols for all indicator faces of the Carousel Indicator.
public protocol IndicatorViewProtocol { public protocol IndicatorViewProtocol {
func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool)
func reset() func reset()
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 {
open class CarouselIndicator: Control, CarouselPageControlProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Outlets // MARK: - Outlets
//-------------------------------------------------- //--------------------------------------------------
@ -45,9 +55,12 @@ open class CarouselIndicator: Control {
didSet { assignIndicatorView() } didSet { assignIndicatorView() }
} }
public var uiGestures: Set<UIGestureRecognizer> = []
/// The currently active indicator view. /// The currently active indicator view.
public var currentIndicator: IndicatorView? public var currentIndicator: IndicatorView?
/// Convenience to access the model.
public var carouselIndicatorModel: CarouselIndicatorModel? { public var carouselIndicatorModel: CarouselIndicatorModel? {
return model as? CarouselIndicatorModel return model as? CarouselIndicatorModel
} }
@ -65,14 +78,15 @@ open class CarouselIndicator: Control {
} }
} }
/// The maxmum count of pages before the indicatorView forces a numeric Indicator insead of Bar. /// The maxmum count of pages before the indicatorView forces a Numeric Indicator in place of Bar.
public var hybridThreshold: Int = 5 public var hybridThreshold: Int = 5
/// 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.
public var indicatorTouchAction: ((Int)->())? /// Passes through oldInde and newIndex, respectively.
public var indicatorTouchAction: CarouselIndicator.PagingTouchBlock?
/// Allows sendActions() to trigger even if index is min/max index. /// Allows sendActions() to trigger even if index is already at min/max index.
public var alwaysSendEvent = false public var alwaysSendAction = false
/// Set true to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is false /// Set true to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is false
public var accessibilityHasSlidesInsteadOfPage = false public var accessibilityHasSlidesInsteadOfPage = false
@ -91,17 +105,9 @@ open class CarouselIndicator: Control {
didSet { didSet {
isUserInteractionEnabled = isEnabled isUserInteractionEnabled = isEnabled
indicatorView?.isEnabled = isEnabled indicatorView?.isEnabled = isEnabled
if indicatorType != .bar && numberOfPages > hybridThreshold {
} else {
if let stackView = indicatorView as? BarsIndicatorView {
stackView.stackView.arrangedSubviews.forEach { ($0 as? BarsIndicatorView)?.isEnabled = isEnabled }
}
}
} }
} }
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Computed Properties // MARK: - Computed Properties
//-------------------------------------------------- //--------------------------------------------------
@ -112,21 +118,21 @@ open class CarouselIndicator: Control {
public var currentIndex: Int { public var currentIndex: Int {
get { return _currentIndex } get { return _currentIndex }
set (newIndex) { set (newIndex) {
guard _currentIndex != newIndex else { return } if !allowIndexWraparound {
guard _currentIndex != newIndex else { return }
}
previousIndex = _currentIndex previousIndex = _currentIndex
_currentIndex = newIndex _currentIndex = newIndex
sendActions(for: .valueChanged) performAction()
indicatorTouchAction?(newIndex) updateUI()
indicatorView?.updateUI(previousIndex: previousIndex,
newIndex: newIndex,
totalCount: numberOfPages,
isAnimated: isAnimated)
} }
} }
private var _numberOfPages = 0 private var _numberOfPages = 0
/// Holds the total number of pages displayed by the carousel.
/// Updating this property will potentially update the UI.
public var numberOfPages: Int { public var numberOfPages: Int {
get { return _numberOfPages } get { return _numberOfPages }
set (newTotal) { set (newTotal) {
@ -140,14 +146,7 @@ open class CarouselIndicator: Control {
indicatorView = BarsIndicatorView() indicatorView = BarsIndicatorView()
} }
if alwaysSendEvent { updateUI()
sendActions(for: .valueChanged)
}
indicatorView?.updateUI(previousIndex: previousIndex,
newIndex: currentIndex,
totalCount: newTotal,
isAnimated: isAnimated)
} }
} }
@ -159,6 +158,12 @@ open class CarouselIndicator: Control {
get { return _indicatorTintColor } get { return _indicatorTintColor }
set (newColor) { set (newColor) {
_indicatorTintColor = newColor _indicatorTintColor = newColor
if isBarIndicator(), let barIndicator = indicatorView as? BarsIndicatorView {
for (i, barTuple) in barIndicator.barsReference.enumerated() where i != currentIndex {
barTuple.view.backgroundColor = newColor
}
}
} }
} }
@ -169,6 +174,12 @@ open class CarouselIndicator: Control {
get { return _currentIndicatorColor } get { return _currentIndicatorColor }
set (newColor) { set (newColor) {
_currentIndicatorColor = newColor _currentIndicatorColor = newColor
if isBarIndicator() {
if let barIndicator = indicatorView as? BarsIndicatorView {
barIndicator.barsReference[currentIndex].view.backgroundColor = newColor
}
}
} }
} }
@ -207,9 +218,8 @@ open class CarouselIndicator: Control {
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
guard indicatorView == nil else { return }
assignIndicatorView() assignIndicatorView()
setupGestures()
if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: accessibilityHasSlidesInsteadOfPage ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") { if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: accessibilityHasSlidesInsteadOfPage ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") {
accessibilityValue = String(format: accessibleValue, currentIndex + 1, numberOfPages) accessibilityValue = String(format: accessibleValue, currentIndex + 1, numberOfPages)
@ -220,22 +230,49 @@ open class CarouselIndicator: Control {
// MARK: - UITouch // MARK: - UITouch
//-------------------------------------------------- //--------------------------------------------------
@objc func pageValueIncrement() { private func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(indicatorTapped(_:)))
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeLeft))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeRight))
uiGestures.insert(tap)
uiGestures.insert(leftSwipe)
uiGestures.insert(rightSwipe)
}
func incrementCurrentIndex() {
currentIndex = min(currentIndex + 1, numberOfPages - 1) currentIndex = min(currentIndex + 1, numberOfPages - 1)
} }
@objc func pageValueDecrement() { func decrementCurrentIndex() {
currentIndex = max(0, currentIndex - 1) currentIndex = max(0, currentIndex - 1)
} }
func indicatorTapped(_ tapGesture: UITapGestureRecognizer?) { /// Increments the currentIndex value.
@objc func swipeLeft() {
incrementCurrentIndex()
}
/// Decrement the currentIndex value
@objc func swipeRight() {
decrementCurrentIndex()
}
/// Handles tap logic for Indicator
@objc func indicatorTapped(_ tapGesture: UITapGestureRecognizer?) {
if isEnabled, let bars = (indicatorView as? BarsIndicatorView)?.barsReference { let touchPoint = tapGesture?.location(in: self)
let touchPoint_X = tapGesture?.location(in: self).x ?? 0.0 let touchPoint_X = touchPoint?.x ?? 0.0
if isEnabled, indicatorType == .bar, let bars = (indicatorView as? BarsIndicatorView)?.barsReference {
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 {
if touchPoint_X > bounds.width / 2 {
incrementCurrentIndex()
} else {
decrementCurrentIndex()
}
} }
} }
@ -243,6 +280,20 @@ open class CarouselIndicator: Control {
// MARK: - Methods // MARK: - Methods
//-------------------------------------------------- //--------------------------------------------------
public func updateUI() {
indicatorView?.updateUI(previousIndex: previousIndex,
newIndex: currentIndex,
totalCount: numberOfPages,
isAnimated: isAnimated)
}
public func performAction() {
sendActions(for: .valueChanged)
indicatorTouchAction?(self)
}
/// Sets the indicatorView based on the current indicatorType. /// Sets the indicatorView based on the current indicatorType.
func assignIndicatorView() { func assignIndicatorView() {
@ -260,9 +311,13 @@ open class CarouselIndicator: Control {
/// 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) {
}
//-------------------------------------------------- //--------------------------------------------------
// MARK: - MoleculeViewProtocol // MARK: - MoleculeViewProtocol
//-------------------------------------------------- //--------------------------------------------------
@ -284,20 +339,22 @@ open class CarouselIndicator: Control {
//-------------------------------------------------- //--------------------------------------------------
open override func accessibilityIncrement() { open override func accessibilityIncrement() {
accessibilityAdjust(toPage: currentIndex + 1) accessibilityAdjust(toPage: currentIndex + 1)
} }
open override func accessibilityDecrement() { open override func accessibilityDecrement() {
accessibilityAdjust(toPage: currentIndex - 1) accessibilityAdjust(toPage: currentIndex - 1)
} }
func accessibilityAdjust(toPage index: Int) { func accessibilityAdjust(toPage index: Int) {
if (index < numberOfPages && index >= 0) || alwaysSendEvent { if (index < numberOfPages && index >= 0) || alwaysSendAction {
isAnimated = false isAnimated = false
previousIndex = currentIndex
currentIndex = index currentIndex = index
sendActions(for: .valueChanged) performAction()
indicatorTouchAction?(index)
} }
} }

View File

@ -54,7 +54,6 @@ 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)
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)

View File

@ -77,8 +77,6 @@ open class BarsIndicatorView: View, IndicatorViewProtocol {
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
guard subviews.isEmpty else { return }
addSubview(stackView) addSubview(stackView)
isUserInteractionEnabled = false isUserInteractionEnabled = false

View File

@ -11,7 +11,7 @@ import UIKit
open class NumericIndicatorView: View, IndicatorViewProtocol { open class NumericIndicatorView: View, IndicatorViewProtocol {
//-------------------------------------------------- //--------------------------------------------------
// MARK: - Properties // MARK: - Outlets
//-------------------------------------------------- //--------------------------------------------------
/// Text to display the current count of total pages for viewing. /// Text to display the current count of total pages for viewing.
@ -22,7 +22,41 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
return label return label
}() }()
open var isEnabled: Bool = true let leftArrow: ImageView = {
let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate).withHorizontallyFlippedOrientation()
let imageView = ImageView(image: arrow)
imageView.isUserInteractionEnabled = true
imageView.tintColor = .mvmBlack
return imageView
}()
let rightArrow: ImageView = {
let arrow = UIImage(named: "peakingRightArrow")?.withRenderingMode(.alwaysTemplate)
let imageView = ImageView(image: arrow)
imageView.isUserInteractionEnabled = true
imageView.tintColor = .mvmBlack
return imageView
}()
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var isEnabled: Bool = true {
didSet {
titleLabel.isEnabled = isEnabled
leftArrow.tintColor = isEnabled ? enabledColor : disabledColor
rightArrow.tintColor = isEnabled ? enabledColor : disabledColor
}
}
public var enabledColor: UIColor {
return (superview as? CarouselIndicator)?.indicatorTintColor ?? .black
}
public var disabledColor: UIColor {
return (superview as? CarouselIndicator)?.disabledIndicatorColor ?? .mvmCoolGray3
}
public var parentCarouselIndicator: CarouselIndicator? { public var parentCarouselIndicator: CarouselIndicator? {
return superview as? CarouselIndicator return superview as? CarouselIndicator
@ -74,23 +108,18 @@ open class NumericIndicatorView: View, IndicatorViewProtocol {
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
guard subviews.isEmpty else { return }
isUserInteractionEnabled = false isUserInteractionEnabled = false
leftArrow.tintColor = isEnabled ? enabledColor : disabledColor
rightArrow.tintColor = isEnabled ? enabledColor : disabledColor
addSubview(titleLabel) addSubview(titleLabel)
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
NSLayoutConstraint.constraintPinSubview(titleLabel, pinTop: true, pinBottom: true, pinLeft: false, pinRight: false) NSLayoutConstraint.constraintPinSubview(titleLabel, pinTop: true, pinBottom: true, pinLeft: false, pinRight: false)
let arrow = UIImage(named: "peakingRightArrow")?.withHorizontallyFlippedOrientation()
let leftArrow = UIImageView(image: arrow)
leftArrow.isUserInteractionEnabled = true
addSubview(leftArrow) addSubview(leftArrow)
NSLayoutConstraint.constraintPinView(leftArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) 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
let rightArrow = UIImageView(image: UIImage(named: "peakingRightArrow"))
addSubview(rightArrow) addSubview(rightArrow)
NSLayoutConstraint.constraintPinView(rightArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) NSLayoutConstraint.constraintPinView(rightArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo)

View File

@ -0,0 +1,102 @@
//
// ImageView.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 2/13/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
class ImageView: UIImageView, 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()
}
override init(image: UIImage?) {
super.init(image: image)
initialSetup()
}
public convenience init() {
self.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
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 ImageView: MVMCoreViewProtocol {
open func updateView(_ size: CGFloat) {}
/// Will be called only once.
open func setupView() {
translatesAutoresizingMaskIntoConstraints = false
insetsLayoutMarginsFromSafeArea = false
MVMCoreUIUtility.setMarginsFor(self, leading: 0, top: 0, trailing: 0, bottom: 0)
}
}
// MARK:- MVMCoreUIMoleculeViewProtocol
extension ImageView: MVMCoreUIMoleculeViewProtocol {
open func 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() { }
}

View File

@ -9,6 +9,10 @@
import UIKit import UIKit
@objcMembers open class View: UIView, ModelMoleculeViewProtocol { @objcMembers open class View: UIView, ModelMoleculeViewProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var json: [AnyHashable: Any]? open var json: [AnyHashable: Any]?
open var model: MoleculeModelProtocol? open var model: MoleculeModelProtocol?

View File

@ -9,6 +9,9 @@
import UIKit import UIKit
open class Carousel: View { open class Carousel: View {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
@ -45,13 +48,15 @@ open class Carousel: View {
var loop = false var loop = false
private var dragging = false private var dragging = false
// For adding pager /// For adding pager
private var bottomPin: NSLayoutConstraint? private var bottomPin: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - MVMCoreViewProtocol // MARK: - MVMCoreViewProtocol
//--------------------------------------------------
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
guard collectionView.superview == nil else { return }
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = self collectionView.dataSource = self
@ -68,6 +73,7 @@ open class Carousel: View {
open override func updateView(_ size: CGFloat) { open override func updateView(_ size: CGFloat) {
super.updateView(size) super.updateView(size)
collectionView.collectionViewLayout.invalidateLayout() collectionView.collectionViewLayout.invalidateLayout()
showPeaking(false) showPeaking(false)
@ -79,7 +85,9 @@ open class Carousel: View {
} }
} }
//--------------------------------------------------
// MARK: - MVMCoreUIMoleculeViewProtocol // MARK: - MVMCoreUIMoleculeViewProtocol
//--------------------------------------------------
public override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { public override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.setWithModel(model, delegateObject, additionalData) super.setWithModel(model, delegateObject, additionalData)
@ -106,10 +114,13 @@ open class Carousel: View {
collectionView.reloadData() collectionView.reloadData()
} }
//--------------------------------------------------
// MARK: - JSON Setters // MARK: - JSON Setters
/// Updates the layout being used //--------------------------------------------------
/// Updates the layout being used
func setupLayout(with carouselModel: CarouselModel?) { func setupLayout(with carouselModel: CarouselModel?) {
let layout = UICollectionViewFlowLayout() let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal layout.scrollDirection = .horizontal
layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1) layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1)
@ -126,6 +137,7 @@ open class Carousel: View {
numberOfPages = newMolecules.count numberOfPages = newMolecules.count
molecules = newMolecules molecules = newMolecules
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
@ -134,6 +146,7 @@ open class Carousel: View {
molecules?.append(newMolecules.first!) molecules?.append(newMolecules.first!)
molecules?.append(newMolecules[1]) molecules?.append(newMolecules[1])
} }
pageIndex = 0 pageIndex = 0
} }
@ -144,6 +157,7 @@ open class Carousel: View {
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 & MVMCoreUIPagingProtocol)
} }
addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20))) addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20)))
} }
@ -156,7 +170,10 @@ open class Carousel: View {
} }
} }
//--------------------------------------------------
// MARK: - Convenience // MARK: - Convenience
//--------------------------------------------------
/// 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),
@ -182,6 +199,7 @@ open class Carousel: 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. /// 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 & MVMCoreUIPagingProtocol)?, position: CGFloat) {
pagingView?.removeFromSuperview() pagingView?.removeFromSuperview()
guard let pagingView = view else { guard let pagingView = view else {
bottomPin?.isActive = false bottomPin?.isActive = false
@ -189,6 +207,7 @@ open class Carousel: View {
bottomPin?.isActive = true bottomPin?.isActive = true
return return
} }
pagingView.translatesAutoresizingMaskIntoConstraints = false pagingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(pagingView) addSubview(pagingView)
pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true
@ -201,15 +220,13 @@ open class Carousel: View {
pagingView.setNumberOfPages(numberOfPages) pagingView.setNumberOfPages(numberOfPages)
(pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill) (pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill)
pagingView.setPagingTouch { [weak self] (pager) in pagingView.setPagingTouch { [weak self] pager in
MVMCoreDispatchUtility.performBlock(onMainThread: { DispatchQueue.main.async {
guard let localSelf = self else { guard let self = self else { return }
return
}
let currentPage = pager.currentPage() let currentPage = pager.currentPage()
localSelf.pageIndex = currentPage self.pageIndex = currentPage
localSelf.goTo(localSelf.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
}) }
} }
self.pagingView = pagingView self.pagingView = pagingView
} }
@ -246,7 +263,7 @@ open class Carousel: View {
array?.append(pagingView!) array?.append(pagingView!)
} }
self.accessibilityElements = array accessibilityElements = array
} else { } else {
cell.accessibilityElementsHidden = true cell.accessibilityElementsHidden = true
} }
@ -254,6 +271,7 @@ open class Carousel: View {
} }
extension Carousel: UICollectionViewDelegateFlowLayout { extension Carousel: UICollectionViewDelegateFlowLayout {
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
return CGSize(width: itemWidth, height: collectionView.bounds.height) return CGSize(width: itemWidth, height: collectionView.bounds.height)
@ -265,6 +283,7 @@ extension Carousel: UICollectionViewDelegateFlowLayout {
} }
extension Carousel: UICollectionViewDataSource { extension Carousel: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return molecules?.count ?? 0 return molecules?.count ?? 0
} }
@ -280,6 +299,7 @@ extension Carousel: UICollectionViewDataSource {
protocolCell.setWithModel(moleculeInfo.molecule, nil, nil) protocolCell.setWithModel(moleculeInfo.molecule, nil, nil)
protocolCell.updateView(collectionView.bounds.width) protocolCell.updateView(collectionView.bounds.width)
} }
setAccessiblity(cell, index: indexPath.row) setAccessiblity(cell, index: indexPath.row)
return cell return cell
} }
@ -290,20 +310,22 @@ extension Carousel: UIScrollViewDelegate {
func goTo(_ index: Int, animated: Bool) { func goTo(_ index: Int, animated: Bool) {
showPeaking(false) showPeaking(false)
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)), index: index) setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
self.currentIndex = index currentIndex = index
self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: animated) collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: animated)
if let cell = collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)) {
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)), index: index) if let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
UIAccessibility.post(notification: .layoutChanged, argument: cell) UIAccessibility.post(notification: .layoutChanged, argument: cell)
} }
} }
func handleUserOnBufferCell() { func handleUserOnBufferCell() {
guard loop else { return } guard loop else { return }
let lastPageIndex = numberOfPages + 1 let lastPageIndex = numberOfPages + 1
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?.setPage(self.pageIndex)
@ -319,6 +341,7 @@ extension Carousel: UIScrollViewDelegate {
} }
func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) { func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) {
guard loop, dragging else { return } guard loop, dragging else { return }
// Checks if the user is not paging but attempting to drag endlessly and goes out of bounds. Caps the index. // Checks if the user is not paging but attempting to drag endlessly and goes out of bounds. Caps the index.
@ -326,10 +349,12 @@ extension Carousel: UIScrollViewDelegate {
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
let index = scrollView.contentOffset.x / (itemWidth + separatorWidth) let index = scrollView.contentOffset.x / (itemWidth + separatorWidth)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1 let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
if index < 1 { if index < 1 {
self.currentIndex = 0 currentIndex = 0
} else if index > CGFloat(lastCellIndex - 1) { } else if index > CGFloat(lastCellIndex - 1) {
self.currentIndex = lastCellIndex currentIndex = lastCellIndex
} }
} }
@ -379,9 +404,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?.setPage(pageIndex)
showPeaking(true) showPeaking(true)
} }
} }