Skip to content

Instantly share code, notes, and snippets.

@KaQuMiQ
Last active May 13, 2020 07:55
Show Gist options
  • Select an option

  • Save KaQuMiQ/9170cb7b668c39cba5455cfb8c33dc6a to your computer and use it in GitHub Desktop.

Select an option

Save KaQuMiQ/9170cb7b668c39cba5455cfb8c33dc6a to your computer and use it in GitHub Desktop.
Cards with pagination
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