Last active
May 13, 2020 07:55
-
-
Save KaQuMiQ/9170cb7b668c39cba5455cfb8c33dc6a to your computer and use it in GitHub Desktop.
Cards with pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public final class CardsView: UIView { | |
| private enum Constants { | |
| static let margins = UIEdgeInsets(top: 8, left: spacing / 2, bottom: 8, right: spacing / 2) | |
| static let spacing = CGFloat(16) | |
| } | |
| private lazy var scrollView = UIScrollView() | |
| private lazy var stackView = UIStackView() | |
| public var previousPageHandler: (() -> Array<CardViewModel>)? { | |
| didSet { | |
| if oldValue == nil && previousPageHandler != nil { | |
| addCard(leftFiller, at: 0) | |
| addCard(leftPlaceholder, at: 1) | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } else if oldValue != nil && previousPageHandler == nil { | |
| removeCard(leftPlaceholder) | |
| removeCard(leftFiller) | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } else { /**/ } | |
| } | |
| } | |
| private lazy var leftPlaceholder = CardViewPlaceholder() | |
| private lazy var leftFiller = CardViewPlaceholder() | |
| public var nextPageHandler: (() -> Array<CardViewModel>)? { | |
| didSet { | |
| if oldValue == nil && previousPageHandler != nil { | |
| addCard(rightPlaceholder) | |
| addCard(rightFiller) | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } else if oldValue != nil && previousPageHandler == nil { | |
| removeCard(rightFiller) | |
| removeCard(rightPlaceholder) | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } else { /**/ } | |
| } | |
| } | |
| private lazy var rightPlaceholder = CardViewPlaceholder() | |
| private lazy var rightFiller = CardViewPlaceholder() | |
| public var cards: Array<CardViewModel> = [] { | |
| didSet { | |
| stackView.arrangedSubviews.forEach { removeCard($0) } | |
| previousPageHandler.map { _ in addCard(leftFiller); addCard(leftPlaceholder) } | |
| cards.map { CardView($0) }.forEach { addCard($0) } | |
| nextPageHandler.map { _ in addCard(rightPlaceholder); addCard(rightFiller) } | |
| if currentCard == nil || !cards.contains(where: { $0.id == currentCard?.id }) { | |
| currentCard = cards.first | |
| } else { /**/ } | |
| setNeedsLayout() | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } | |
| } | |
| override public init(frame: CGRect) { | |
| super.init(frame: frame) | |
| setup() | |
| } | |
| public override func didMoveToWindow() { | |
| super.didMoveToWindow() | |
| guard window != nil else { return } | |
| currentCard.map { scrollTo(card: $0, animated: false) } | |
| } | |
| @available(*, unavailable) | |
| required public init?(coder: NSCoder) { fatalError() } | |
| private func setup() { | |
| addSubview(scrollView) | |
| scrollView.translatesAutoresizingMaskIntoConstraints = false | |
| scrollView.clipsToBounds = false | |
| scrollView.backgroundColor = .clear | |
| scrollView.bounces = true | |
| scrollView.alwaysBounceHorizontal = true | |
| scrollView.showsVerticalScrollIndicator = false | |
| scrollView.showsHorizontalScrollIndicator = false | |
| scrollView.isPagingEnabled = true | |
| scrollView.delegate = self | |
| scrollView.addSubview(stackView) | |
| stackView.translatesAutoresizingMaskIntoConstraints = false | |
| stackView.distribution = .fill | |
| stackView.axis = .horizontal | |
| stackView.isLayoutMarginsRelativeArrangement = true | |
| stackView.spacing = Constants.spacing | |
| stackView.backgroundColor = .clear | |
| stackView.layoutMargins = Constants.margins | |
| NSLayoutConstraint.activate([ | |
| scrollView.topAnchor.constraint(equalTo: topAnchor), | |
| scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), | |
| scrollView.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.spacing), | |
| scrollView.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constants.spacing), | |
| stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), | |
| stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), | |
| stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), | |
| stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), | |
| stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor), | |
| stackView.widthAnchor.constraint(equalTo: scrollView.safeAreaLayoutGuide.widthAnchor).withPriority(.defaultLow), | |
| stackView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.widthAnchor) | |
| ]) | |
| } | |
| private func addCard(_ view: UIView, at index: Int? = nil) { | |
| if let index = index { | |
| stackView.insertArrangedSubview(view, at: index) | |
| } else { | |
| stackView.addArrangedSubview(view) | |
| } | |
| NSLayoutConstraint.activate([ | |
| view.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -(Constants.spacing)), | |
| ]) | |
| } | |
| private func removeCard(_ view: UIView) { | |
| stackView.removeArrangedSubview(view) | |
| } | |
| public func scrollTo(card: CardViewModel, animated: Bool) { | |
| guard let cardView = stackView.arrangedSubviews.first(where: { ($0 as? CardView)?.modelID == card.id }) else { return } | |
| layoutIfNeeded() | |
| scrollView.scrollRectToVisible(cardView.frame.inset(by: UIEdgeInsets(top: 0, left: -Constants.spacing / 2, bottom: 0, right: -Constants.spacing / 2)), animated: animated) | |
| } | |
| public var currentCardChangeHandler: ((CardViewModel?) -> Void)? | |
| public private(set) var currentCard: CardViewModel? { | |
| didSet { | |
| guard oldValue?.id != currentCard?.id else { return } | |
| currentCardChangeHandler?(currentCard) | |
| } | |
| } | |
| } | |
| extension CardsView: UIScrollViewDelegate { | |
| public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { | |
| guard let currentView = stackView.arrangedSubviews.first(where: { scrollView.bounds.contains($0.center) }) else { return } | |
| if let cardView = currentView as? CardView { | |
| currentCard = cards.first(where: { $0.id == cardView.modelID} ) | |
| } else if scrollView.bounds.contains(leftPlaceholder.center) { | |
| guard let newPage = previousPageHandler?() else { return } | |
| cards = newPage | |
| newPage.last.map { scrollTo(card: $0, animated: false) } | |
| } else if scrollView.bounds.contains(rightPlaceholder.center) { | |
| guard let newPage = nextPageHandler?() else { return } | |
| cards = newPage | |
| newPage.first.map { scrollTo(card: $0, animated: false) } | |
| } | |
| } | |
| public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { | |
| guard let currentView = stackView.arrangedSubviews.first(where: { scrollView.bounds.contains($0.center) }) else { return } | |
| if let cardView = currentView as? CardView { | |
| currentCard = cards.first(where: { $0.id == cardView.modelID} ) | |
| } else if scrollView.bounds.contains(leftPlaceholder.center) { | |
| guard let newPage = previousPageHandler?() else { return } | |
| cards = newPage | |
| newPage.last.map { scrollTo(card: $0, animated: false) } | |
| } else if scrollView.bounds.contains(rightPlaceholder.center) { | |
| guard let newPage = nextPageHandler?() else { return } | |
| cards = newPage | |
| newPage.first.map { scrollTo(card: $0, animated: false) } | |
| } | |
| } | |
| } | |
| public struct CardViewModel { | |
| public var id: Int | |
| public var color: UIColor | |
| } | |
| public final class CardView: UIView { | |
| public var modelID: Int | |
| public init(_ model: CardViewModel) { | |
| modelID = model.id | |
| super.init(frame: .zero) | |
| backgroundColor = model.color | |
| layer.cornerRadius = 16 | |
| } | |
| @available(*, unavailable) | |
| required public init?(coder: NSCoder) { fatalError() } | |
| } | |
| public final class CardViewPlaceholder: UIView { | |
| override public init(frame: CGRect) { | |
| super.init(frame: frame) | |
| backgroundColor = .gray | |
| layer.cornerRadius = 16 | |
| } | |
| @available(*, unavailable) | |
| required public init?(coder: NSCoder) { fatalError() } | |
| } | |
| // -- | |
| extension NSLayoutConstraint { | |
| func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint{ | |
| self.priority = priority | |
| return self | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment