Created
February 19, 2018 22:56
-
-
Save alexmx/27879b80c1556e0824976f1c0474bded to your computer and use it in GitHub Desktop.
Sample carousel implementation which supports spacing, content mode and custom pagination.
This file contains 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
import UIKit | |
@objc | |
public protocol CarouselViewDataSource: class { | |
func numberOfPages(in carouselView: CarouselView) -> Int | |
func carouselView(_ carouselView: CarouselView, pageAt index: Int) -> UIView | |
} | |
@objc | |
public protocol CarouselViewDelegate: class { | |
func carouselView(_ carouselView: CarouselView, didSelectPage page: Int) | |
func carouselViewSpacingBetweenPages(_ carouselView: CarouselView) -> CGFloat | |
} | |
@objc | |
public class CarouselView: UIView { | |
public weak var dataSource: CarouselViewDataSource? | |
public weak var delegate: CarouselViewDelegate? | |
let scrollView = UIScrollView() | |
var pagesCount = 0 | |
var spacingBetweenPages: CGFloat = 0 | |
var contentOffset: CGPoint { | |
return scrollView.contentOffset | |
} | |
var pageWidth: CGFloat { | |
return scrollView.bounds.width + spacingBetweenPages | |
} | |
var currentPage: Int { | |
return Int(contentOffset.x / pageWidth) | |
} | |
// MARK: Public API | |
public override init(frame: CGRect) { | |
super.init(frame: frame) | |
setup() | |
} | |
public required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setup() | |
} | |
public override func layoutSubviews() { | |
super.layoutSubviews() | |
layoutPages() | |
} | |
public override func didMoveToWindow() { | |
super.didMoveToWindow() | |
scrollView.contentInset = .zero | |
} | |
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { | |
if let hitTest = super.hitTest(point, with: event) { | |
if hitTest === self { | |
return scrollView | |
} else { | |
return hitTest | |
} | |
} | |
return nil | |
} | |
public override func updateConstraints() { | |
super.updateConstraints() | |
scrollView.removeConstraints(scrollView.constraints) | |
switch contentMode { | |
case .center: | |
scrollView.centerInSuperview() | |
case .top: | |
scrollView.pinToSuperviewTop() | |
scrollView.centerXInSuperview() | |
case .right: | |
scrollView.pinToSuperviewRight() | |
scrollView.centerYInSuperview() | |
case .bottom: | |
scrollView.pinToSuperviewBottom() | |
scrollView.centerXInSuperview() | |
case .left: | |
scrollView.pinToSuperviewLeft() | |
scrollView.centerYInSuperview() | |
case .topLeft: | |
scrollView.pinToSuperviewCorners(.topLeft) | |
case .topRight: | |
scrollView.pinToSuperviewCorners(.topRight) | |
case .bottomLeft: | |
scrollView.pinToSuperviewCorners(.bottomLeft) | |
case .bottomRight: | |
scrollView.pinToSuperviewCorners(.bottomRight) | |
case .scaleAspectFill, | |
.scaleToFill, | |
.scaleAspectFit: | |
scrollView.matchSuperview() | |
default: | |
scrollView.matchSuperview() | |
} | |
} | |
public func reloadData() { | |
layoutPages() | |
} | |
// MARK: Internal API | |
func setup() { | |
clipsToBounds = true | |
scrollView.translatesAutoresizingMaskIntoConstraints = false | |
scrollView.clipsToBounds = false | |
scrollView.showsVerticalScrollIndicator = false | |
scrollView.showsHorizontalScrollIndicator = false | |
scrollView.decelerationRate = UIScrollViewDecelerationRateFast | |
scrollView.delegate = self | |
addSubview(scrollView) | |
} | |
func removePages() { | |
for page in scrollView.subviews { | |
page.removeFromSuperview() | |
} | |
} | |
func layoutPages() { | |
removePages() | |
pagesCount = dataSource?.numberOfPages(in: self) ?? 0 | |
var lastPage: UIView? | |
var constraints: [NSLayoutConstraint] = [] | |
spacingBetweenPages = delegate?.carouselViewSpacingBetweenPages(self) ?? 0 | |
for i in 0 ..< pagesCount { | |
guard let page = dataSource?.carouselView(self, pageAt: i) else { | |
break | |
} | |
scrollView.addSubview(page) | |
let views = ["page": page, "scrollView": scrollView, "lastPage": lastPage ?? page] | |
let metrics = ["spacing": spacingBetweenPages] | |
if i == 0 { | |
constraints += NSLayoutConstraint.constraints( | |
withVisualFormat: "|[page(scrollView)]", | |
options: [], | |
metrics: metrics, | |
views: views | |
) | |
} else if i == pagesCount - 1 { | |
constraints += NSLayoutConstraint.constraints( | |
withVisualFormat: "[lastPage]-(spacing)-[page(scrollView)]|", | |
options: [], | |
metrics: metrics, | |
views: views | |
) | |
} else { | |
constraints += NSLayoutConstraint.constraints( | |
withVisualFormat: "[lastPage]-(spacing)-[page(scrollView)]", | |
options: [], | |
metrics: metrics, | |
views: views | |
) | |
} | |
constraints += NSLayoutConstraint.constraints( | |
withVisualFormat: "V:|[page(scrollView)]|", | |
options: [], | |
metrics: metrics, | |
views: views | |
) | |
lastPage = page | |
} | |
addConstraints(constraints) | |
setNeedsUpdateConstraints() | |
setNeedsLayout() | |
} | |
} | |
extension CarouselView: UIScrollViewDelegate { | |
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { | |
delegate?.carouselView(self, didSelectPage: currentPage) | |
} | |
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { | |
var index = contentOffset.x / pageWidth | |
if velocity.x > 0 { | |
index = ceil(index) | |
} else if velocity.x < 0 { | |
index = floor(index) | |
} else { | |
index = round(index) | |
} | |
targetContentOffset.pointee.x = index * pageWidth | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment