421 lines
20 KiB
Swift
421 lines
20 KiB
Swift
//
|
|
// Carousel.swift
|
|
// MVMCoreUI
|
|
//
|
|
// Created by Scott Pfeil on 7/2/19.
|
|
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
open class Carousel: ViewConstrainingView {
|
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
|
|
|
|
/// The current index of the collection view. Includes dummy cells when looping.
|
|
var currentIndex = 0
|
|
|
|
/// The index of the page, does not include dummy cells.
|
|
var pageIndex: Int {
|
|
get {
|
|
return loop ? currentIndex - 2 : currentIndex
|
|
}
|
|
set(newIndex) {
|
|
currentIndex = loop ? newIndex + 2 : newIndex
|
|
}
|
|
}
|
|
|
|
/// The number of pages that there are. Used for the page control and for calculations. Should not include the looping dummy cells. Be sure to set this if subclassing and not using the molecules.
|
|
var numberOfPages = 0
|
|
|
|
/// The json for the molecules.
|
|
var molecules: [[AnyHashable: Any]]?
|
|
|
|
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
|
|
var itemAlignment = UICollectionView.ScrollPosition.left
|
|
|
|
/// From 0-1. The item width as a percent of the carousel width.
|
|
var itemWidthPercent: CGFloat = 1
|
|
|
|
/// The height of the carousel. Default is 300.
|
|
var collectionViewHeight: NSLayoutConstraint?
|
|
|
|
/// The view that we use for paging
|
|
var pagingView: (UIView & MVMCoreUIPagingProtocol)?
|
|
|
|
/// If the carousel should loop after scrolling past the first and final cells.
|
|
var loop = false
|
|
private var dragging = false
|
|
private var previousContentOffsetX: CGFloat = 0
|
|
|
|
// MARK: - MVMCoreViewProtocol
|
|
open override func setupView() {
|
|
super.setupView()
|
|
guard collectionView.superview == nil else {
|
|
return
|
|
}
|
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
collectionView.dataSource = self
|
|
collectionView.delegate = self
|
|
collectionView.showsHorizontalScrollIndicator = false
|
|
collectionView.backgroundColor = .clear
|
|
addSubview(collectionView)
|
|
pinView(toSuperView: collectionView)
|
|
|
|
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
|
|
collectionViewHeight?.isActive = true
|
|
}
|
|
|
|
open override func updateView(_ size: CGFloat) {
|
|
super.updateView(size)
|
|
collectionView.collectionViewLayout.invalidateLayout()
|
|
|
|
// Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled.
|
|
DispatchQueue.main.async {
|
|
self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: false)
|
|
self.collectionView.layoutIfNeeded()
|
|
}
|
|
}
|
|
|
|
// MARK: - MVMCoreUIMoleculeViewProtocol
|
|
open override func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) {
|
|
super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData)
|
|
registerCells(with: json, delegateObject: delegateObject)
|
|
setupLayout(with: json)
|
|
prepareMolecules(with: json)
|
|
itemWidthPercent = (json?.optionalCGFloatForKey("itemWidthPercent") ?? 100) / 100
|
|
setAlignment(with: json?.optionalStringForKey("itemAlignment"))
|
|
collectionViewHeight?.constant = json?.optionalCGFloatForKey("height") ?? 300
|
|
setupPagingMolecule(json: json?.optionalDictionaryForKey("pagingMolecule"), delegateObject: delegateObject)
|
|
collectionView.reloadData()
|
|
}
|
|
|
|
open override func shouldSetHorizontalMargins(_ shouldSet: Bool) {
|
|
super.shouldSetHorizontalMargins(shouldSet)
|
|
updateViewHorizontalDefaults = false
|
|
}
|
|
|
|
// MARK: - JSON Setters
|
|
/// Updates the layout being used
|
|
func setupLayout(with json:[AnyHashable: Any]?) {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.minimumLineSpacing = json?["spacing"] as? CGFloat ?? 1
|
|
layout.minimumInteritemSpacing = 0
|
|
collectionView.collectionViewLayout = layout
|
|
}
|
|
|
|
func prepareMolecules(with json: [AnyHashable: Any]?) {
|
|
guard let newMolecules = json?.optionalArrayForKey(KeyMolecules) as? [[AnyHashable: Any]] else {
|
|
numberOfPages = 0
|
|
molecules = nil
|
|
return
|
|
}
|
|
|
|
numberOfPages = newMolecules.count
|
|
molecules = newMolecules
|
|
if json?.boolForKey("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).
|
|
loop = true
|
|
molecules?.insert(newMolecules.last!, at: 0)
|
|
molecules?.insert(newMolecules[(newMolecules.count - 1)], at: 0)
|
|
molecules?.append(newMolecules.first!)
|
|
molecules?.append(newMolecules[1])
|
|
}
|
|
pageIndex = 0
|
|
}
|
|
|
|
/// Registers the cells with the collection view
|
|
func registerCells(with json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) {
|
|
if let molecules = json?.optionalArrayForKey(KeyMolecules) as? [[AnyHashable: Any]] {
|
|
for molecule in molecules {
|
|
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
|
|
collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets up the paging molecule
|
|
open func setupPagingMolecule(json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) {
|
|
var pagingView: (UIView & MVMCoreUIPagingProtocol)? = nil
|
|
if let json = json {
|
|
pagingView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: json, delegateObject: delegateObject, constrainIfNeeded: true) as? (UIView & MVMCoreUIPagingProtocol)
|
|
}
|
|
addPaging(view: pagingView, position: (json?.optionalCGFloatForKey("position") ?? 20))
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
/// Returns the (identifier, class) of the molecule for the given map.
|
|
func getMoleculeInfo(with molecule: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: [AnyHashable: Any])? {
|
|
guard let molecule = molecule,
|
|
let moleculeClass = MVMCoreUIMoleculeMappingObject.shared()?.getMoleculeClass(withJSON: molecule),
|
|
let moleculeName = moleculeClass.name?(forReuse: molecule, delegateObject: delegateObject) ?? molecule.optionalStringForKey(KeyMoleculeName) else {
|
|
return nil
|
|
}
|
|
return (moleculeName, moleculeClass, molecule)
|
|
}
|
|
|
|
/// Sets the alignment from the string.
|
|
open func setAlignment(with string: String?) {
|
|
switch string {
|
|
case "leading":
|
|
itemAlignment = .left
|
|
case "trailing":
|
|
itemAlignment = .right
|
|
case "center":
|
|
itemAlignment = .centeredHorizontally
|
|
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.
|
|
open func addPaging(view: (UIView & MVMCoreUIPagingProtocol)?, position: CGFloat) {
|
|
pagingView?.removeFromSuperview()
|
|
guard let pagingView = view else {
|
|
bottomPin?.isActive = false
|
|
bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
|
|
bottomPin?.isActive = true
|
|
return
|
|
}
|
|
pagingView.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(pagingView)
|
|
pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true
|
|
collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true
|
|
bottomAnchor.constraint(greaterThanOrEqualTo: pagingView.bottomAnchor).isActive = true
|
|
bottomPin?.isActive = false
|
|
bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
|
|
bottomPin?.priority = .defaultLow
|
|
bottomPin?.isActive = true
|
|
|
|
pagingView.setNumberOfPages(numberOfPages)
|
|
(pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill)
|
|
pagingView.setPagingTouch { [weak self] (pager) in
|
|
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
|
guard let localSelf = self else {
|
|
return
|
|
}
|
|
let currentPage = pager.currentPage()
|
|
localSelf.pageIndex = currentPage
|
|
self?.collectionView.scrollToItem(at: IndexPath(row: localSelf.currentIndex, section: 0), at: (self?.itemAlignment ?? .left), animated: true)
|
|
})
|
|
}
|
|
self.pagingView = pagingView
|
|
}
|
|
}
|
|
|
|
extension Carousel: UICollectionViewDelegateFlowLayout {
|
|
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
|
let itemWidth = collectionView.bounds.width * itemWidthPercent
|
|
return CGSize(width: itemWidth, height: collectionView.bounds.height)
|
|
}
|
|
}
|
|
|
|
extension Carousel: UICollectionViewDataSource {
|
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
return molecules?.count ?? 0
|
|
}
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
guard let molecule = molecules?[indexPath.row],
|
|
let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil) else {
|
|
return UICollectionViewCell()
|
|
}
|
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath)
|
|
if let protocolCell = cell as? MVMCoreUIMoleculeViewProtocol {
|
|
protocolCell.reset?()
|
|
protocolCell.setWithJSON(moleculeInfo.molecule, delegateObject: nil, additionalData: nil)
|
|
protocolCell.updateView(collectionView.bounds.width)
|
|
}
|
|
|
|
return cell
|
|
}
|
|
}
|
|
|
|
extension Carousel: UIScrollViewDelegate {
|
|
/*// For getting the scroll progress to set the page control color progress.
|
|
- (CGFloat)getPageControlPercentBasedOnScrollView:(UIScrollView *)scrollView {
|
|
CGFloat cardWidth = ((UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout).itemSize.width;
|
|
CGFloat separatorWidth = ((UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout).minimumLineSpacing;
|
|
CGFloat contentOfsetInCard = fmodf(scrollView.contentOffset.x, cardWidth + separatorWidth);
|
|
CGFloat endThresholdPageControl = cardWidth + separatorWidth - CGRectGetMaxX(self.pageControl.frame);
|
|
CGFloat progress = contentOfsetInCard - endThresholdPageControl;
|
|
CGFloat width = CGRectGetWidth(self.pageControl.bounds);
|
|
CGFloat percent = (width - progress)/width;
|
|
CGFloat cappedPercent = MAX(MIN(percent, 1), 0);
|
|
return cappedPercent;
|
|
}
|
|
|
|
- (void)setPageControlColorsBasedOnScrollView:(UIScrollView *)scrollView {
|
|
|
|
// Check if we will need to change colors.
|
|
BOOL needToShiftColors = NO;
|
|
NSInteger nextCardIndex = 0;
|
|
CGFloat cardWidth = ((UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout).itemSize.width;
|
|
CGFloat separatorWidth = ((UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout).minimumLineSpacing;
|
|
NSInteger currentCard = scrollView.contentOffset.x / (cardWidth + separatorWidth);
|
|
CGFloat cardStart = currentCard * (cardWidth + separatorWidth);
|
|
CGFloat cardEnd = cardStart + cardWidth + separatorWidth;
|
|
NSInteger pageIndicator = currentCard;
|
|
if ((self.previousContentOffsetX == NSNotFound || self.previousContentOffsetX <= cardStart) && scrollView.contentOffset.x >= cardStart) {
|
|
|
|
// We are passed the threshold and moving right, change to right card color.
|
|
needToShiftColors = YES;
|
|
nextCardIndex = currentCard + 1;
|
|
pageIndicator = currentCard - 1;
|
|
} else if ((self.previousContentOffsetX == NSNotFound || self.previousContentOffsetX >= cardEnd) && scrollView.contentOffset.x < cardEnd) {
|
|
|
|
// We are passed the threshold and moving left, change to left card color.
|
|
needToShiftColors = YES;
|
|
nextCardIndex = currentCard - 1;
|
|
}
|
|
|
|
if (needToShiftColors) {
|
|
// Only shift the page control if we are dragging still, otherwise end animation will control.
|
|
if (self.dragging) {
|
|
[self.pageControl setCurrentPage:pageIndicator];
|
|
}
|
|
|
|
// Get the current page color
|
|
NSString *colorString = [[self.feedModules objectAtIndex:currentCard] string:KeyPageIndicatorColor];
|
|
UIColor *currentCardPageControlColor = colorString ? [UIColor mfGetColorForHex:colorString] : [UIColor blackColor];
|
|
|
|
// Get the next page color and set accordingly.
|
|
colorString = [[self.feedModules dictionaryAtIndex:nextCardIndex] string:KeyPageIndicatorColor];
|
|
UIColor *nextCardPageControlColor = colorString ? [UIColor mfGetColorForHex:colorString] : [UIColor blackColor];
|
|
|
|
// Which color needs to be on top or bottom depends on which direction we are moving.
|
|
if (nextCardIndex > currentCard) {
|
|
[self setPageControlColor:nextCardPageControlColor progressColor:currentCardPageControlColor];
|
|
} else {
|
|
[self setPageControlColor:currentCardPageControlColor progressColor:nextCardPageControlColor];
|
|
}
|
|
}
|
|
}
|
|
|
|
*/
|
|
|
|
func handleUserOnBufferCell() {
|
|
guard loop else {
|
|
return
|
|
}
|
|
|
|
let lastPageIndex = numberOfPages + 1
|
|
let goToIndex = {(index: Int) in
|
|
self.currentIndex = index
|
|
self.previousContentOffsetX = CGFloat(NSNotFound)
|
|
self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: false)
|
|
self.collectionView.layoutIfNeeded()
|
|
self.pagingView?.setPage(self.pageIndex)
|
|
}
|
|
|
|
if currentIndex < 2 {
|
|
// If on a "buffer" last row (which is the first index), go to the real last row secretly. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking.
|
|
goToIndex(lastPageIndex)
|
|
} else if currentIndex > lastPageIndex {
|
|
// If on the "buffer" first row (which is the index after the real last row), go to the real first row secretly.
|
|
goToIndex(2)
|
|
}
|
|
}
|
|
|
|
func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) {
|
|
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.
|
|
if let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing {
|
|
let itemWidth = collectionView.bounds.width * itemWidthPercent
|
|
let index = Int(scrollView.contentOffset.x / (itemWidth + separatorWidth))
|
|
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
|
|
if index < 0 {
|
|
self.currentIndex = 0
|
|
} else if index > lastCellIndex {
|
|
self.currentIndex = lastCellIndex
|
|
}
|
|
}
|
|
|
|
handleUserOnBufferCell()
|
|
}
|
|
|
|
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
// Check if the user is dragging the card even further past the next card.
|
|
checkForDraggingOutOfBounds(scrollView)
|
|
|
|
// Set the page control direction colors if needed.
|
|
//[self setPageControlColorsBasedOnScrollView:scrollView];
|
|
|
|
// Set the percent of progress.
|
|
//self.pageControl.progressView.progress = [self getPageControlPercentBasedOnScrollView:scrollView];
|
|
|
|
previousContentOffsetX = scrollView.contentOffset.x;
|
|
}
|
|
|
|
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
dragging = true
|
|
|
|
// // Hide coverview and arrow.
|
|
// FeedBaseCollectionViewCell *peakingCell = (FeedBaseCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.currentIndex + 1 inSection:0]];
|
|
// [peakingCell setPeaking:NO animated:YES];
|
|
}
|
|
|
|
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
dragging = false
|
|
targetContentOffset.pointee = scrollView.contentOffset
|
|
|
|
// This is for setting up smooth custom paging. (Since UICollectionView only handles paging based on collection view size and not cell size).
|
|
guard let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing else {
|
|
return
|
|
}
|
|
|
|
// We switch cards if we pass the velocity threshold or position threshold (currently 50%).
|
|
let itemWidth = collectionView.bounds.width * itemWidthPercent
|
|
var cellToSwipeTo = Int(scrollView.contentOffset.x/(itemWidth + separatorWidth) + 0.5)
|
|
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
|
|
let velocityThreshold: CGFloat = 1.1
|
|
if velocity.x > velocityThreshold {
|
|
cellToSwipeTo = currentIndex + 1
|
|
} else if velocity.x < -velocityThreshold {
|
|
cellToSwipeTo = currentIndex - 1
|
|
}
|
|
|
|
// Cap the index.
|
|
currentIndex = min(max(cellToSwipeTo, 0), lastCellIndex)
|
|
|
|
collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: true)
|
|
|
|
// Notify that card changed
|
|
/*if (self.cardChanged) {
|
|
self.cardChanged(self.currentIndex, [self.module string:[MFBaseHomeViewController getFeedContainerNameKey]]);
|
|
}*/
|
|
}
|
|
|
|
// To give the illusion of endless scrolling. Since we are always calling scrollToItem we can assume finished paging in here.
|
|
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
// Cycle to other end if on buffer cell.
|
|
handleUserOnBufferCell()
|
|
|
|
pagingView?.setPage(pageIndex)
|
|
/*
|
|
// Update to the new page in the control if needed.
|
|
if (self.currentIndex - 1 != self.pageControl.currentPage) {
|
|
[self.pageControl setCurrentPage:self.currentIndex - 1];
|
|
}
|
|
// Show overlay and arrow in next Cell
|
|
FeedBaseCollectionViewCell *nextCell = (FeedBaseCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.currentIndex + 1 inSection:0]];
|
|
[nextCell setPeaking:YES animated:YES];
|
|
|
|
FeedBaseCollectionViewCell *currentCell = (FeedBaseCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.currentIndex inSection:0]];
|
|
if (currentCell) {
|
|
self.accessibilityElements = @[currentCell.containerView, self.pageControl];
|
|
currentCell.containerView.isAccessibilityElement = YES;
|
|
currentCell.accessibilityElementsHidden = NO;
|
|
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,currentCell.containerView);
|
|
}
|
|
|
|
// Set the page control again if pageControl is tapped or voice over is using.
|
|
[self setPageControlColorsBasedOnScrollView:scrollView];
|
|
|
|
// send adobe tracker action
|
|
[self sendAdobeTrackerAction];*/
|
|
}
|
|
}
|