This commit is contained in:
Pfeil, Scott Robert 2019-07-11 11:54:14 -04:00
parent 033cf878a6
commit 33020844dc
8 changed files with 840 additions and 56 deletions

View File

@ -31,6 +31,9 @@
D22D1F47220496A30077CEC0 /* MVMCoreUISwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = D22D1F45220496A30077CEC0 /* MVMCoreUISwitch.m */; };
D22D1F562204CE5D0077CEC0 /* MVMCoreUIStackableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D22D1F542204CE5D0077CEC0 /* MVMCoreUIStackableViewController.h */; settings = {ATTRIBUTES = (Public, ); }; };
D22D1F572204CE5D0077CEC0 /* MVMCoreUIStackableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D22D1F552204CE5D0077CEC0 /* MVMCoreUIStackableViewController.m */; };
D260D7B122D65BDD007E7233 /* MVMCoreUIPageControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D260D7AF22D65BDD007E7233 /* MVMCoreUIPageControl.h */; settings = {ATTRIBUTES = (Public, ); }; };
D260D7B222D65BDD007E7233 /* MVMCoreUIPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D260D7B022D65BDD007E7233 /* MVMCoreUIPageControl.m */; };
D260D7B622D68514007E7233 /* MVMCoreUIPagingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
D274CA332236A78900B01B62 /* StandardFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274CA322236A78900B01B62 /* StandardFooterView.swift */; };
D282AAB4223FDDAE00C46919 /* MFLoadImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282AAB3223FDDAE00C46919 /* MFLoadImageView.swift */; };
D282AABA224131D100C46919 /* MFTransparentGIFView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D282AAB9224131D100C46919 /* MFTransparentGIFView.swift */; };
@ -202,6 +205,9 @@
D22D1F45220496A30077CEC0 /* MVMCoreUISwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUISwitch.m; sourceTree = "<group>"; };
D22D1F542204CE5D0077CEC0 /* MVMCoreUIStackableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIStackableViewController.h; sourceTree = "<group>"; };
D22D1F552204CE5D0077CEC0 /* MVMCoreUIStackableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUIStackableViewController.m; sourceTree = "<group>"; };
D260D7AF22D65BDD007E7233 /* MVMCoreUIPageControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIPageControl.h; sourceTree = "<group>"; };
D260D7B022D65BDD007E7233 /* MVMCoreUIPageControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUIPageControl.m; sourceTree = "<group>"; };
D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIPagingProtocol.h; sourceTree = "<group>"; };
D274CA322236A78900B01B62 /* StandardFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardFooterView.swift; sourceTree = "<group>"; };
D282AAB3223FDDAE00C46919 /* MFLoadImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFLoadImageView.swift; sourceTree = "<group>"; };
D282AAB9224131D100C46919 /* MFTransparentGIFView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MFTransparentGIFView.swift; sourceTree = "<group>"; };
@ -481,6 +487,9 @@
D2A638FC22CA98280052ED1F /* HeadlineBody.swift */,
D2A6390022CBB1820052ED1F /* Carousel.swift */,
D2A6390422CBCE160052ED1F /* MoleculeCollectionViewCell.swift */,
D260D7AF22D65BDD007E7233 /* MVMCoreUIPageControl.h */,
D260D7B022D65BDD007E7233 /* MVMCoreUIPageControl.m */,
D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */,
);
path = Molecules;
sourceTree = "<group>";
@ -757,6 +766,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
D260D7B622D68514007E7233 /* MVMCoreUIPagingProtocol.h in Headers */,
D29DF18021E69E49003B2FB9 /* MFView.h in Headers */,
D29DF27921E7A533003B2FB9 /* MVMCoreUISession.h in Headers */,
D206997721FB8A0B00CAE0DE /* MVMCoreUINavigationController.h in Headers */,
@ -815,6 +825,7 @@
D29770FD21F7C77400B2F0D0 /* MVMCoreUITextFieldView.h in Headers */,
D29DF17421E69E1F003B2FB9 /* MFCustomButton.h in Headers */,
D29DF29721E7ADB8003B2FB9 /* MFScrollingViewController.h in Headers */,
D260D7B122D65BDD007E7233 /* MVMCoreUIPageControl.h in Headers */,
D29DF26F21E6AA0B003B2FB9 /* FLAnimatedImageView.h in Headers */,
D29DF2A121E7AF4E003B2FB9 /* MVMCoreUIUtility.h in Headers */,
D29DF17621E69E1F003B2FB9 /* PrimaryButton.h in Headers */,
@ -940,6 +951,7 @@
DBEFFA04225A829700230692 /* Label.swift in Sources */,
D29DF13021E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m in Sources */,
D28B4F8B21FF967C00712C7A /* MVMCoreUIObject.m in Sources */,
D260D7B222D65BDD007E7233 /* MVMCoreUIPageControl.m in Sources */,
D29DF26D21E6AA0B003B2FB9 /* FLAnimatedImageView.m in Sources */,
D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */,
D282AACB2243C61700C46919 /* ButtonView.swift in Sources */,

View File

@ -108,5 +108,7 @@ FOUNDATION_EXPORT const unsigned char MVMCoreUIVersionString[];
#pragma mark - Molecules
#import <MVMCoreUI/TopLabelsView.h>
#import <MVMCoreUI/MVMCoreUIMoleculeViewProtocol.h>
#import <MVMCoreUI/MVMCoreUIPagingProtocol.h>
#import <MVMCoreUI/MVMCoreUIPageControl.h>
#pragma mark - Templates

View File

@ -10,12 +10,44 @@ import UIKit
open class Carousel: ViewConstrainingView {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
var currentIndex = 1
var numberOfCards = 0
var molecules: [[AnyHashable : Any]]?
var collectionViewHeight: NSLayoutConstraint?
/// 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 {
@ -33,8 +65,67 @@ open class Carousel: ViewConstrainingView {
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?) {
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) {
@ -44,51 +135,13 @@ open class Carousel: ViewConstrainingView {
}
}
/// Updates the layout being used
func setupLayout(with json:[AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = json?["spacing"] as? CGFloat ?? 0
layout.minimumInteritemSpacing = 0
//layout.itemSize = CGSize(width: 300, height: 200)
collectionView.collectionViewLayout = layout
}
func prepareMolecules(_ json: [AnyHashable : Any]?) {
guard let newMolecules = json?.optionalArrayForKey(KeyMolecules) as? [[AnyHashable: Any]] else {
numberOfCards = 0
molecules = nil
return
/// 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)
}
numberOfCards = newMolecules.count
molecules = newMolecules
if json?.boolForKey("loop") ?? false && newMolecules.count > 2 {
// Sets up the row data with a buffer cell on each side (for illusion of endless scroll... also has one more buffer cell on right since we can peek that cell).
molecules?.insert(newMolecules.last!, at: 0)
molecules?.append(newMolecules.first!)
molecules?.append(newMolecules[1])
}
}
open override func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) {
super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData)
collectionViewHeight?.constant = json?.optionalCGFloatForKey("height") ?? 300
registerCells(with: json, delegateObject: delegateObject)
setupLayout(with: json, delegateObject: delegateObject)
prepareMolecules(json)
collectionView.reloadData()
// Go to starting cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic.
collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: .left, animated: false)
collectionView.layoutIfNeeded()
}
open override func setAsMolecule() {
super.setAsMolecule()
updateViewHorizontalDefaults = false
addPaging(view: pagingView, position: (json?.optionalCGFloatForKey("position") ?? 20))
}
// MARK: - Convenience
@ -101,10 +154,60 @@ open class Carousel: ViewConstrainingView {
}
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: UICollectionViewDelegate {
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 {
@ -123,6 +226,195 @@ extension Carousel: UICollectionViewDataSource {
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];*/
}
}

View File

@ -0,0 +1,60 @@
//
// MVMCoreUIPageControl.h
// MobileFirstFramework
//
// Created by Seshamani, Shreyas on 1/5/18.
// Copyright © 2018 Verizon Wireless. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <MVMCore/MVMCoreViewProtocol.h>
#import <MVMCoreUI/MVMCoreUIMoleculeViewProtocol.h>
#import <MVMCoreUI/MVMCoreUIPagingProtocol.h>
@interface MVMCoreUIPageControl : UIControl <MVMCoreViewProtocol, MVMCoreUIMoleculeViewProtocol, MVMCoreUIPagingProtocol>
// These properties effectively do what their corresponding namesakes do in UIPageControl
@property (nonatomic) NSInteger currentPage;
@property (nonatomic) NSInteger numberOfPages;
@property (nonatomic, getter=isAnimated) BOOL animated;
@property (nullable, strong, nonatomic) UIColor *pageIndicatorTintColor;
@property (nullable, strong, nonatomic) UIColor *currentPageIndicatorTintColor;
@property (nullable, strong, nonatomic, readonly) NSArray *rectangles;
@property (nullable, weak, nonatomic) UIView *containerView;
@property (nullable, weak, nonatomic) UIView *indicatorRectangle;
@property (nullable, copy, nonatomic) PagingTouchBlock pagingTouchBlock;
///set YES to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is NO
@property (nonatomic) BOOL isSlidesAcc;
/// This property may be used for indicating the user's selected option (not the currentPage). For instance, in Plan Explore Sizes, it indicates what plan the user is currently on.
@property (nonatomic) NSInteger persistentPreselectedPage;
//customize pagecontrol properties
@property (nonatomic) CGFloat rectangleWidth;
/// Indicates the color of the persistentPreselectedPage
@property (nullable, strong, nonatomic) UIColor *persisitentPreselectedPageTintColor;
//top bottom constraints
@property (nullable, strong, nonatomic) NSLayoutConstraint *topConstraint;
@property (nullable, strong, nonatomic) NSLayoutConstraint *bottomConstraint;
//a flag to allow to send UIControlEventValueChanged actions all the time
//e.g. going to previous element at first place and going to next at last place
//While current rectangle won't change, need update current page
@property (nonatomic) BOOL alwaysSendingControlEvent;
- (nullable instancetype)initWithAnimation:(BOOL)animated;
- (void)setCurrentPage:(NSInteger)currentPage animated:(BOOL)animated;
// Sets up the horizontal constraint to have the pages centered horizontally.
- (void)setupHorizontalConstraints;
// For subclassing only.
- (void)setupView;
- (void)setupRectangles;
- (void)setTopBottomSpace:(CGFloat)constant;
@end

View File

@ -0,0 +1,395 @@
//
// MVMCoreUIPageControl.m
// MobileFirstFramework
//
// Created by Seshamani, Shreyas on 1/5/18.
// Copyright © 2018 Verizon Wireless. All rights reserved.
//
#import "MVMCoreUIPageControl.h"
#import "MVMCoreUICommonViewsUtility.h"
#import <MVMCoreUI/StackableViewController.h>
#import <MVMCoreUI/UIColor+MFConvenience.h>
#import "MVMCoreUIUtility.h"
#import "MVMCoreUIConstants.h"
@interface MVMCoreUIPageControl ()
@property (nullable, weak, nonatomic) UIView *animationRectangle;
@property (nullable, strong, nonatomic, readwrite) NSArray *rectangles;
@property (nullable, strong, nonatomic) NSLayoutConstraint *indicatorRectangleLeadingConstraint;
@property (nonatomic) CGFloat interRectangleSpacing;
@property (nonatomic) BOOL isDoAnimating;
@property (nonatomic) BOOL isDisAnimating;
@end
@implementation MVMCoreUIPageControl
static CGFloat const DefaultInterRectangleSpacing = 6;
static CGFloat const DefaultRectangleWidth = 24;
static CGFloat const RectangleHeight = 1;
static CGFloat const IndicatorRectangleHeight = 4;
- (void)updateView:(CGFloat)size {
}
#pragma mark - Properties
- (instancetype)init {
self = [super init];
if (self) {
[self initValues];
[self setupView];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initValues];
[self setupView];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
[self initValues];
[self setupView];
}
return self;
}
- (instancetype)initWithAnimation:(BOOL)animated {
self = [super init];
if (self) {
[self initValues];
[self setupView];
self.animated = animated;
}
return self;
}
- (void)initValues {
self.interRectangleSpacing = DefaultInterRectangleSpacing;
self.rectangleWidth = DefaultRectangleWidth;
self.animated = YES;
self.isSlidesAcc = NO;
}
- (void)setCurrentPage:(NSInteger)currentPage {
[self setCurrentPage:currentPage animated:self.animated];
}
- (void)setCurrentPage:(NSInteger)currentPage animated:(BOOL)animated {
if (_currentPage!=currentPage) {
_currentPage = currentPage;
if (currentPage >= 0 && currentPage < self.numberOfPages) {
self.animationRectangle.frame = self.indicatorRectangle.frame;
self.indicatorRectangleLeadingConstraint.constant = (_currentPage * (self.rectangleWidth + self.interRectangleSpacing) + self.interRectangleSpacing);
[self displayIndicator:animated];
[self dismissAnimationIndicator:animated];
[self layoutIfNeeded];
}
}
}
- (void)setNumberOfPages:(NSInteger)numberOfPages {
if (_numberOfPages != numberOfPages) {
_numberOfPages = numberOfPages;
[self updateRectangleWidthAndSpacing];
[self setupRectangles];
}
}
- (void)setPersistentPreselectedPage:(NSInteger)persistentPreselectedPage {
_persistentPreselectedPage = persistentPreselectedPage;
if (!self.rectangles) {
[self setupRectangles];
}
if (persistentPreselectedPage >= 0 && persistentPreselectedPage < self.numberOfPages) {
UIView *persistentRectangle = self.rectangles[persistentPreselectedPage];
persistentRectangle.backgroundColor = self.persisitentPreselectedPageTintColor;
}
}
@synthesize currentPageIndicatorTintColor = _currentPageIndicatorTintColor;
- (UIColor *)currentPageIndicatorTintColor {
if (!_currentPageIndicatorTintColor) {
_currentPageIndicatorTintColor = [UIColor blackColor];
}
return _currentPageIndicatorTintColor;
}
- (void)setCurrentPageIndicatorTintColor:(UIColor *)currentPageIndicatorTintColor {
_currentPageIndicatorTintColor = currentPageIndicatorTintColor;
if (!self.indicatorRectangle) {
[self setupRectangles];
}
self.indicatorRectangle.backgroundColor = currentPageIndicatorTintColor;
self.animationRectangle.backgroundColor = currentPageIndicatorTintColor;
}
@synthesize pageIndicatorTintColor = _pageIndicatorTintColor;
- (UIColor *)pageIndicatorTintColor {
if (!_pageIndicatorTintColor) {
return [UIColor mfBattleshipGrey];
}
return _pageIndicatorTintColor;
}
- (void)setPageIndicatorTintColor:(UIColor *)pageIndicatorTintColor {
_pageIndicatorTintColor = pageIndicatorTintColor;
if (!self.rectangles) {
[self setupRectangles];
}
for (UIView *rectangle in self.rectangles) {
rectangle.backgroundColor = pageIndicatorTintColor;
}
}
@synthesize persisitentPreselectedPageTintColor = _persisitentPreselectedPageTintColor;
- (UIColor *)persisitentPreselectedPageTintColor {
if (!_persisitentPreselectedPageTintColor) {
_persisitentPreselectedPageTintColor = [UIColor mfCerulean];
}
return _persisitentPreselectedPageTintColor;
}
- (void)setPersisitentPreselectedPageTintColor:(UIColor *)persisitentPreselectedPageTintColor {
_persisitentPreselectedPageTintColor = persisitentPreselectedPageTintColor;
if (!self.rectangles) {
[self setupRectangles];
}
if (self.persistentPreselectedPage >= 0 && self.persistentPreselectedPage < self.numberOfPages) {
UIView *persistentRectangle = self.rectangles[self.persistentPreselectedPage];
persistentRectangle.backgroundColor = persisitentPreselectedPageTintColor;
}
}
#pragma mark - Setup
- (void)updateRectangleWidthAndSpacing {
CGFloat screenWidth = [MVMCoreUIUtility getWidth];
CGFloat pageControlRequiredWidth = self.numberOfPages * (self.rectangleWidth + self.interRectangleSpacing) + DefaultInterRectangleSpacing;
if (pageControlRequiredWidth > screenWidth) {
// the Inter rectangle spacing is a quarter of the rectangle width
self.interRectangleSpacing = screenWidth / ((self.numberOfPages + 1) + (self.numberOfPages * 4));
self.rectangleWidth = self.interRectangleSpacing * 4;
}
}
- (void)setupRectangles {
// Create the rectangles for all the indexes
[self removeRectangles];
NSMutableArray *rectangles = [[NSMutableArray alloc] init];
for (int index = 0; index < self.numberOfPages; index++) {
UIView *rectangle = [MVMCoreUICommonViewsUtility commonView];
[rectangles addObject:rectangle];
rectangle.translatesAutoresizingMaskIntoConstraints = NO;
rectangle.frame = CGRectMake((self.interRectangleSpacing * (index + 1)) + (self.rectangleWidth * index), IndicatorRectangleHeight- RectangleHeight, self.rectangleWidth, RectangleHeight);
[rectangle.heightAnchor constraintEqualToConstant:RectangleHeight].active = YES;
[rectangle.widthAnchor constraintEqualToConstant:self.rectangleWidth].active = YES;
rectangle.backgroundColor = self.pageIndicatorTintColor;
}
[StackableViewController populateViewHorizontally:self.containerView withUIArray:rectangles withSpacingBlock:^UIEdgeInsets(id _Nullable object) {
if (object == [rectangles lastObject]) {
return UIEdgeInsetsMake(RectangleHeight, self.interRectangleSpacing, 0, self.interRectangleSpacing);
}
return UIEdgeInsetsMake(RectangleHeight, self.interRectangleSpacing, 0, 0);
}];
self.rectangles = rectangles;
// Create the indicator rectangle
UIView *indicatorRectangle = [MVMCoreUICommonViewsUtility commonView];
[self.containerView addSubview:indicatorRectangle];
indicatorRectangle.backgroundColor = self.currentPageIndicatorTintColor;
[indicatorRectangle.heightAnchor constraintEqualToConstant:IndicatorRectangleHeight].active = YES;
[indicatorRectangle.widthAnchor constraintEqualToConstant:self.rectangleWidth].active = YES;
[indicatorRectangle.topAnchor constraintGreaterThanOrEqualToAnchor:self.containerView.topAnchor].active = YES;
[indicatorRectangle.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor].active = YES;
self.indicatorRectangleLeadingConstraint = [indicatorRectangle.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor constant:self.interRectangleSpacing];
self.indicatorRectangleLeadingConstraint.active = YES;
self.indicatorRectangle = indicatorRectangle;
indicatorRectangle.frame = CGRectMake(self.interRectangleSpacing, 0, self.rectangleWidth, IndicatorRectangleHeight);
// Create shadow indicator rectangle for animaiton
UIView *animatedIndicatorRectangle = [MVMCoreUICommonViewsUtility commonView];
[self.containerView addSubview:animatedIndicatorRectangle];
animatedIndicatorRectangle.backgroundColor = self.currentPageIndicatorTintColor;
self.animationRectangle = animatedIndicatorRectangle;
}
- (void)setupView {
if (!self.containerView) {
// Create a container view that keeps everything centered
UIView *containerView = [MVMCoreUICommonViewsUtility commonView];
[self addSubview:containerView];
self.containerView = containerView;
[self setupHorizontalConstraints];
NSLayoutConstraint *topConstraint = [containerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:PaddingThree];
topConstraint.priority = UILayoutPriorityDefaultHigh;
topConstraint.active = YES;
self.topConstraint = topConstraint;
NSLayoutConstraint *bottomConstraint = [self.bottomAnchor constraintEqualToAnchor:containerView.bottomAnchor constant:PaddingThree];
bottomConstraint.priority = UILayoutPriorityDefaultHigh;
bottomConstraint.active = YES;
self.bottomConstraint = bottomConstraint;
[self updateRectangleWidthAndSpacing];
// Create the rectangles for all the indexes
[self setupRectangles];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
[tapGesture addTarget:self action:@selector(rectangleTapped:)];
[self addGestureRecognizer:tapGesture];
}
}
- (void)setupHorizontalConstraints {
[self.containerView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES;
[self.containerView.leadingAnchor constraintGreaterThanOrEqualToAnchor:self.leadingAnchor].active = YES;
[self.containerView.trailingAnchor constraintLessThanOrEqualToAnchor:self.trailingAnchor].active = YES;
}
- (void)removeRectangles {
for (UIView *subview in self.containerView.subviews) {
[subview removeFromSuperview];
}
}
#pragma mark - Actions
- (void)rectangleTapped:(UITapGestureRecognizer *)tapGesture {
if (self.userInteractionEnabled) {
CGPoint touchPoint = [tapGesture locationInView:self.containerView];
NSInteger selectedIndex = [self.rectangles indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIView *subview = self.rectangles[idx];
if (CGRectGetMaxX(subview.frame) >= touchPoint.x && CGRectGetMinX(subview.frame) <= touchPoint.x) {
*stop = YES;
}
return *stop;
}];
if (selectedIndex != NSNotFound) {
self.currentPage = selectedIndex;
[self sendActionsForControlEvents:UIControlEventValueChanged];
self.pagingTouchBlock(self);
}
}
}
#pragma mark - MVMCoreUIPagingProtocol
- (void)setPage:(NSUInteger)page {
self.currentPage = page;
}
#pragma mark - MoleculeViewProtocol
- (void)setWithJSON:(NSDictionary *)json delegateObject:(MVMCoreUIDelegateObject *)delegateObject additionalData:(NSDictionary *)additionalData {
NSString *colorString = [json string:KeyBackgroundColor];
if (colorString) {
self.backgroundColor = [UIColor mfGetColorForHex:colorString];
}
colorString = [json string:@"barsColor"];
if (colorString) {
UIColor *color = [UIColor mfGetColorForHex:colorString];
self.pageIndicatorTintColor = color;
self.currentPageIndicatorTintColor = color;
}
colorString = [json string:@"currentBarColor"];
if (colorString) {
self.currentPageIndicatorTintColor = [UIColor mfGetColorForHex:colorString];
}
}
#pragma mark - Accessibility
- (UIAccessibilityTraits)accessibilityTraits {
return UIAccessibilityTraitAdjustable;
}
- (NSString *)accessibilityValue {
NSString *stringKey = self.isSlidesAcc ? @"MVMCoreUIPageControlslides_currentpage_index" : @"MVMCoreUIPageControl_currentpage_index";
return [NSString stringWithFormat:[MVMCoreUIUtility hardcodedStringWithKey:stringKey],self.currentPage+1, self.numberOfPages];
}
- (void)accessibilityIncrement {
[self accessibilityAdjustToPage:self.currentPage + 1];
}
- (void)accessibilityDecrement {
[self accessibilityAdjustToPage:self.currentPage -1];
}
//when self.awlaysSenfingControlEven it NO, and user is already at first or final index, if user try to increment or decrement, won't do action
//while self.awlaysSenfingControlEven is YES, it still send control event, while the rectangle won't change, need set currentPage again.
- (void)accessibilityAdjustToPage:(NSInteger)index {
if ((index < self.numberOfPages && index >= 0) || self.alwaysSendingControlEvent) {
[self setCurrentPage:index animated:NO];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
#pragma mark - Animate pagecontrol indicator
- (void)displayIndicator:(BOOL)animated {
if (!animated) {
return;
}
if (!self.isDoAnimating) {
self.indicatorRectangle.frame = CGRectMake(CGRectGetMinX(self.indicatorRectangle.frame), CGRectGetMinY(self.indicatorRectangle.frame) + IndicatorRectangleHeight, CGRectGetWidth(self.indicatorRectangle.frame), 0.f);
[UIView animateWithDuration:0.3f animations:^{
self.isDoAnimating = YES;
self.indicatorRectangle.frame = CGRectMake(CGRectGetMinX(self.indicatorRectangle.frame), CGRectGetMinY(self.indicatorRectangle.frame)-IndicatorRectangleHeight, CGRectGetWidth(self.indicatorRectangle.frame), IndicatorRectangleHeight);
} completion:^(BOOL finished) {
self.isDoAnimating = NO;
}];
}
}
- (void)dismissAnimationIndicator:(BOOL)animated {
if (animated) {
if (!self.isDisAnimating) {
[UIView animateWithDuration:0.3f animations:^{
self.isDisAnimating = YES;
self.animationRectangle.frame = CGRectMake(CGRectGetMinX(self.animationRectangle.frame), CGRectGetMinY(self.animationRectangle.frame) + IndicatorRectangleHeight, CGRectGetWidth(self.animationRectangle.frame), 0.f);
} completion:^(BOOL finished) {
[self layoutIfNeeded];
self.animationRectangle.frame = self.indicatorRectangle.frame;
self.isDisAnimating = NO;
}];
}
} else {
[self layoutIfNeeded];
self.animationRectangle.frame = self.indicatorRectangle.frame;
}
}
- (void)setTopBottomSpace:(CGFloat)constant {
self.bottomConstraint.constant = constant;
self.topConstraint.constant = constant;
}
@end

View File

@ -0,0 +1,22 @@
//
// MVMCoreUIPagingProtocol.h
// MVMCoreUI
//
// Created by Scott Pfeil on 7/10/19.
// Copyright © 2019 Verizon Wireless. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol MVMCoreUIPagingProtocol <NSObject>
typedef void (^PagingTouchBlock)(NSObject<MVMCoreUIPagingProtocol>* _Nonnull sender);
- (NSInteger)currentPage;
- (void)setNumberOfPages:(NSInteger)numberOfPages;
- (void)setPage:(NSInteger)page;
- (void)setPagingTouchBlock:(nullable PagingTouchBlock)pagingTouchBlock;
@end

View File

@ -45,10 +45,9 @@ open class MoleculeCollectionViewCell: UICollectionViewCell, MVMCoreUIMoleculeVi
}
if let castView = molecule as? MVMCoreUIViewConstrainingProtocol {
let standardConstraints = castView.useStandardConstraints?() ?? true
castView.shouldSetHorizontalMargins?(!standardConstraints)
castView.shouldSetVerticalMargins?(!standardConstraints)
castView.shouldSetHorizontalMargins?(standardConstraints)
castView.shouldSetVerticalMargins?(standardConstraints)
}
backgroundColor = molecule?.backgroundColor
}
@ -60,6 +59,6 @@ open class MoleculeCollectionViewCell: UICollectionViewCell, MVMCoreUIMoleculeVi
}
public func updateView(_ size: CGFloat) {
molecule?.updateView(size)
}
}

View File

@ -14,6 +14,7 @@
#import "MVMCoreUIObject.h"
#import <MVMCoreUI/MVMCoreUI-Swift.h>
#import "MFTextField.h"
#import "MVMCoreUIPageControl.h"
#import "MVMCoreUIViewConstrainingProtocol.h"
@implementation MVMCoreUIMoleculeMappingObject
@ -50,7 +51,8 @@
@"moduleMolecule": ModuleMolecule.class,
@"headlineBody": HeadlineBody.class,
@"carousel": Carousel.class,
@"carouselItem": MoleculeCollectionViewCell.class
@"carouselItem": MoleculeCollectionViewCell.class,
@"barsPager": MVMCoreUIPageControl.class,
} mutableCopy];
});
return mapping;