From 5558cc832492d24686a78d39cdf95b4a41b5aa6b Mon Sep 17 00:00:00 2001 From: Kevin G Christiano Date: Thu, 13 Feb 2020 15:33:55 -0500 Subject: [PATCH] adding new base class. improving carousel logic. --- MVMCoreUI.xcodeproj/project.pbxproj | 4 + .../CarouselIndicator/CarouselIndicator.swift | 139 ++++++++++++------ .../CarouselIndicatorModel.swift | 1 - .../IndicatorViews/BarsIndicatorView.swift | 2 - .../IndicatorViews/NumericIndicatorView.swift | 47 ++++-- MVMCoreUI/BaseClasses/ImageView.swift | 102 +++++++++++++ MVMCoreUI/BaseClasses/View.swift | 4 + MVMCoreUI/Organisms/Carousel.swift | 67 ++++++--- 8 files changed, 291 insertions(+), 75 deletions(-) create mode 100644 MVMCoreUI/BaseClasses/ImageView.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index ffa59aaa..25481035 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 0A4253AF23F5C2C100554656 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4253AE23F5C2C000554656 /* BarsIndicatorView.swift */; }; 0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.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 */; }; 0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.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 = ""; }; 0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleGuidelinesProtocol.swift; sourceTree = ""; }; 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = ""; }; + 0A7918F423F5E7EA00772FF4 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = ""; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxLabel.swift; sourceTree = ""; }; @@ -1471,6 +1473,7 @@ D2755D7A23689C7500485468 /* TableViewCell.swift */, 0A5D59C323AD488600EFD9E9 /* Protocols */, 0A14F6A423E4803A00EDF7F7 /* StackView.swift */, + 0A7918F423F5E7EA00772FF4 /* ImageView.swift */, ); path = BaseClasses; sourceTree = ""; @@ -1819,6 +1822,7 @@ 01EB3684236097C0006832FA /* MoleculeModelProtocol.swift in Sources */, D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, + 0A7918F523F5E7EA00772FF4 /* ImageView.swift in Sources */, 0198F79F225679880066C936 /* FormValidationProtocol.swift in Sources */, D243859923A16B1800332775 /* Container.swift in Sources */, D2C521A923EDE79E00CA2634 /* ViewController.swift in Sources */, diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift index f263d7b9..7a776a94 100644 --- a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicator.swift @@ -8,14 +8,24 @@ import Foundation +/// Set protocols for all indicator faces of the Carousel Indicator. public protocol IndicatorViewProtocol { func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) func reset() 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 //-------------------------------------------------- @@ -45,9 +55,12 @@ open class CarouselIndicator: Control { didSet { assignIndicatorView() } } + public var uiGestures: Set = [] + /// The currently active indicator view. public var currentIndicator: IndicatorView? + /// Convenience to access the model. public var carouselIndicatorModel: 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 /// 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. - public var alwaysSendEvent = false + /// Allows sendActions() to trigger even if index is already at min/max index. + 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 public var accessibilityHasSlidesInsteadOfPage = false @@ -91,17 +105,9 @@ open class CarouselIndicator: Control { didSet { isUserInteractionEnabled = 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 //-------------------------------------------------- @@ -112,21 +118,21 @@ open class CarouselIndicator: Control { public var currentIndex: Int { get { return _currentIndex } set (newIndex) { - guard _currentIndex != newIndex else { return } + if !allowIndexWraparound { + guard _currentIndex != newIndex else { return } + } previousIndex = _currentIndex _currentIndex = newIndex - sendActions(for: .valueChanged) - indicatorTouchAction?(newIndex) - indicatorView?.updateUI(previousIndex: previousIndex, - newIndex: newIndex, - totalCount: numberOfPages, - isAnimated: isAnimated) + performAction() + updateUI() } } 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 { get { return _numberOfPages } set (newTotal) { @@ -140,14 +146,7 @@ open class CarouselIndicator: Control { indicatorView = BarsIndicatorView() } - if alwaysSendEvent { - sendActions(for: .valueChanged) - } - - indicatorView?.updateUI(previousIndex: previousIndex, - newIndex: currentIndex, - totalCount: newTotal, - isAnimated: isAnimated) + updateUI() } } @@ -159,6 +158,12 @@ open class CarouselIndicator: Control { get { return _indicatorTintColor } set (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 } set (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() { super.setupView() - guard indicatorView == nil else { return } - assignIndicatorView() + setupGestures() if let accessibleValue = MVMCoreUIUtility.hardcodedString(withKey: accessibilityHasSlidesInsteadOfPage ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index") { accessibilityValue = String(format: accessibleValue, currentIndex + 1, numberOfPages) @@ -220,22 +230,49 @@ open class CarouselIndicator: Control { // 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) } - @objc func pageValueDecrement() { - + func decrementCurrentIndex() { 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_X = tapGesture?.location(in: self).x ?? 0.0 - + let touchPoint = tapGesture?.location(in: self) + 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 + } else { + if touchPoint_X > bounds.width / 2 { + incrementCurrentIndex() + } else { + decrementCurrentIndex() + } } } @@ -243,6 +280,20 @@ open class CarouselIndicator: Control { // 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. func assignIndicatorView() { @@ -260,9 +311,13 @@ open class CarouselIndicator: Control { /// Convenience to determine if current view is displaying bars. func isBarIndicator() -> Bool { + return indicatorType != .bar && numberOfPages > hybridThreshold } + public func scrollViewDidScroll(_ collectionView: UICollectionView) { + + } //-------------------------------------------------- // MARK: - MoleculeViewProtocol //-------------------------------------------------- @@ -284,20 +339,22 @@ open class CarouselIndicator: Control { //-------------------------------------------------- open override func accessibilityIncrement() { + accessibilityAdjust(toPage: currentIndex + 1) } open override func accessibilityDecrement() { + accessibilityAdjust(toPage: currentIndex - 1) } func accessibilityAdjust(toPage index: Int) { - if (index < numberOfPages && index >= 0) || alwaysSendEvent { + if (index < numberOfPages && index >= 0) || alwaysSendAction { isAnimated = false + previousIndex = currentIndex currentIndex = index - sendActions(for: .valueChanged) - indicatorTouchAction?(index) + performAction() } } diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 074b0570..0cff8f2e 100644 --- a/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -54,7 +54,6 @@ public class CarouselIndicatorModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - currentBarColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentBarColor) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) barsColor = try typeContainer.decodeIfPresent(Color.self, forKey: .barsColor) diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift index e20b5489..53ebd1a8 100644 --- a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/BarsIndicatorView.swift @@ -77,8 +77,6 @@ open class BarsIndicatorView: View, IndicatorViewProtocol { open override func setupView() { super.setupView() - guard subviews.isEmpty else { return } - addSubview(stackView) isUserInteractionEnabled = false diff --git a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift index 95ea16d1..ed215632 100644 --- a/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift +++ b/MVMCoreUI/Atoms/Views/CarouselIndicator/IndicatorViews/NumericIndicatorView.swift @@ -11,7 +11,7 @@ import UIKit open class NumericIndicatorView: View, IndicatorViewProtocol { //-------------------------------------------------- - // MARK: - Properties + // MARK: - Outlets //-------------------------------------------------- /// Text to display the current count of total pages for viewing. @@ -22,7 +22,41 @@ open class NumericIndicatorView: View, IndicatorViewProtocol { 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? { return superview as? CarouselIndicator @@ -74,23 +108,18 @@ open class NumericIndicatorView: View, IndicatorViewProtocol { open override func setupView() { super.setupView() - guard subviews.isEmpty else { return } - isUserInteractionEnabled = false + leftArrow.tintColor = isEnabled ? enabledColor : disabledColor + rightArrow.tintColor = isEnabled ? enabledColor : disabledColor addSubview(titleLabel) titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 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) NSLayoutConstraint.constraintPinView(leftArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - let rightArrow = UIImageView(image: UIImage(named: "peakingRightArrow")) addSubview(rightArrow) NSLayoutConstraint.constraintPinView(rightArrow, heightConstraint: true, heightConstant: PaddingTwo, widthConstraint: true, widthConstant: PaddingTwo) diff --git a/MVMCoreUI/BaseClasses/ImageView.swift b/MVMCoreUI/BaseClasses/ImageView.swift new file mode 100644 index 00000000..788be939 --- /dev/null +++ b/MVMCoreUI/BaseClasses/ImageView.swift @@ -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?) -> [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() { } +} + diff --git a/MVMCoreUI/BaseClasses/View.swift b/MVMCoreUI/BaseClasses/View.swift index a117d60a..19e139b9 100644 --- a/MVMCoreUI/BaseClasses/View.swift +++ b/MVMCoreUI/BaseClasses/View.swift @@ -9,6 +9,10 @@ import UIKit @objcMembers open class View: UIView, ModelMoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + open var json: [AnyHashable: Any]? open var model: MoleculeModelProtocol? diff --git a/MVMCoreUI/Organisms/Carousel.swift b/MVMCoreUI/Organisms/Carousel.swift index 46c6f269..f332a6be 100644 --- a/MVMCoreUI/Organisms/Carousel.swift +++ b/MVMCoreUI/Organisms/Carousel.swift @@ -9,6 +9,9 @@ import UIKit open class Carousel: View { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) @@ -45,13 +48,15 @@ open class Carousel: View { var loop = false private var dragging = false - // For adding pager + /// For adding pager private var bottomPin: NSLayoutConstraint? + //-------------------------------------------------- // MARK: - MVMCoreViewProtocol + //-------------------------------------------------- + open override func setupView() { super.setupView() - guard collectionView.superview == nil else { return } collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.dataSource = self @@ -68,6 +73,7 @@ open class Carousel: View { open override func updateView(_ size: CGFloat) { super.updateView(size) + collectionView.collectionViewLayout.invalidateLayout() showPeaking(false) @@ -79,7 +85,9 @@ open class Carousel: View { } } + //-------------------------------------------------- // MARK: - MVMCoreUIMoleculeViewProtocol + //-------------------------------------------------- public override func setWithModel(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.setWithModel(model, delegateObject, additionalData) @@ -106,10 +114,13 @@ open class Carousel: View { collectionView.reloadData() } + //-------------------------------------------------- // MARK: - JSON Setters - /// Updates the layout being used + //-------------------------------------------------- + /// Updates the layout being used func setupLayout(with carouselModel: CarouselModel?) { + let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1) @@ -126,6 +137,7 @@ open class Carousel: View { numberOfPages = newMolecules.count molecules = newMolecules + 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). loop = true @@ -134,6 +146,7 @@ open class Carousel: View { molecules?.append(newMolecules.first!) molecules?.append(newMolecules[1]) } + pageIndex = 0 } @@ -144,6 +157,7 @@ open class Carousel: View { if let molecule = molecule { pagingView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(molecule, delegateObject, false) as? (UIView & MVMCoreUIPagingProtocol) } + addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20))) } @@ -156,7 +170,10 @@ open class Carousel: View { } } + //-------------------------------------------------- // MARK: - Convenience + //-------------------------------------------------- + /// Returns the (identifier, class) of the molecule for the given map. func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? { 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. open func addPaging(view: (UIView & MVMCoreUIPagingProtocol)?, position: CGFloat) { + pagingView?.removeFromSuperview() guard let pagingView = view else { bottomPin?.isActive = false @@ -189,6 +207,7 @@ open class Carousel: View { bottomPin?.isActive = true return } + pagingView.translatesAutoresizingMaskIntoConstraints = false addSubview(pagingView) pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true @@ -201,15 +220,13 @@ open class Carousel: View { pagingView.setNumberOfPages(numberOfPages) (pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill) - pagingView.setPagingTouch { [weak self] (pager) in - MVMCoreDispatchUtility.performBlock(onMainThread: { - guard let localSelf = self else { - return - } + pagingView.setPagingTouch { [weak self] pager in + DispatchQueue.main.async { + guard let self = self else { return } let currentPage = pager.currentPage() - localSelf.pageIndex = currentPage - localSelf.goTo(localSelf.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) - }) + self.pageIndex = currentPage + self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning) + } } self.pagingView = pagingView } @@ -246,7 +263,7 @@ open class Carousel: View { array?.append(pagingView!) } - self.accessibilityElements = array + accessibilityElements = array } else { cell.accessibilityElementsHidden = true } @@ -254,6 +271,7 @@ open class Carousel: View { } extension Carousel: UICollectionViewDelegateFlowLayout { + public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) return CGSize(width: itemWidth, height: collectionView.bounds.height) @@ -265,6 +283,7 @@ extension Carousel: UICollectionViewDelegateFlowLayout { } extension Carousel: UICollectionViewDataSource { + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return molecules?.count ?? 0 } @@ -280,6 +299,7 @@ extension Carousel: UICollectionViewDataSource { protocolCell.setWithModel(moleculeInfo.molecule, nil, nil) protocolCell.updateView(collectionView.bounds.width) } + setAccessiblity(cell, index: indexPath.row) return cell } @@ -290,20 +310,22 @@ extension Carousel: UIScrollViewDelegate { func goTo(_ index: Int, animated: Bool) { showPeaking(false) - setAccessiblity(collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)), index: index) - self.currentIndex = index - self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.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) + setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index) + currentIndex = index + collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: animated) + + 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) } } func handleUserOnBufferCell() { + guard loop else { return } let lastPageIndex = numberOfPages + 1 - let goToIndex = {(index: Int) in + let goToIndex = { (index: Int) in self.goTo(index, animated: false) self.collectionView.layoutIfNeeded() self.pagingView?.setPage(self.pageIndex) @@ -319,6 +341,7 @@ extension Carousel: UIScrollViewDelegate { } 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. @@ -326,10 +349,12 @@ extension Carousel: UIScrollViewDelegate { let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent) let index = scrollView.contentOffset.x / (itemWidth + separatorWidth) let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1 + if index < 1 { - self.currentIndex = 0 + currentIndex = 0 + } else if index > CGFloat(lastCellIndex - 1) { - self.currentIndex = lastCellIndex + currentIndex = lastCellIndex } } @@ -379,9 +404,7 @@ extension Carousel: UIScrollViewDelegate { public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { // Cycle to other end if on buffer cell. handleUserOnBufferCell() - pagingView?.setPage(pageIndex) - showPeaking(true) } }