class PageReusableView: UIView {
fileprivate var index: Int = 0
func prepareForReuse() {}
}
protocol PagingViewDataSource: class {
func numberOfPages(in pagingView: PagingView) -> Int
func pagingView(_ pagingView: PagingView, pageForAt index: Int) -> PageReusableView
}
@objc protocol PagingViewDelegate: UIScrollViewDelegate {
@objc optional func pagingView(_ pagingView: PagingView, didDisplay page: PageReusableView, forAt index: Int)
}
class PagingView: UIScrollView {
var currentPageIndex = 0
var previousPageIndex = Int.max
var dataSource: PagingViewDataSource!
private var isLayouting = false
private var isViewLoaded = false
private var visiblePages = Set<PageReusableView>()
private var recycledPages = Set<PageReusableView>()
private var observationContentOffset: NSKeyValueObservation!
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
// self.contentInsetAdjustmentBehavior = .never
self.isPagingEnabled = true
self.showsHorizontalScrollIndicator = true
self.showsVerticalScrollIndicator = true
self.backgroundColor = .white //.black
NotificationCenter.default.addObserver(
forName: NSNotification.Name.UIDeviceOrientationDidChange,
object: self,
queue: OperationQueue.main) { [weak self] _ in
self?.layoutVisiblePages()
}
self.observationContentOffset = self.observe(\PagingView.contentOffset) { [weak self] _, _ in
guard let `self` = self else { return }
if !self.isViewLoaded || self.isLayouting { return }
// Tile pages
self.tilePages()
// Calculate current page
let numberOfPages = self.dataSource.numberOfPages(in: self)
let visibleBounds = self.bounds
var index = Int(visibleBounds.midX / visibleBounds.width)
index = (index < 0) ? 0 : index
index = (index > numberOfPages - 1) ? numberOfPages - 1 : index
let previousCurrentPage = self.currentPageIndex
self.currentPageIndex = index
if self.currentPageIndex != previousCurrentPage {
self.displayPageView(at: index)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
if !self.isViewLoaded {
self.isViewLoaded = true
let numberOfPages = self.dataSource.numberOfPages(in: self)
self.contentSize = CGSize(
width: self.bounds.size.width * numberOfPages.f,
height: self.bounds.size.height
)
self.reloadData()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
self.observationContentOffset.invalidate()
}
// MARK: Public Methods
func reloadData() {
let numberOfPages = dataSource.numberOfPages(in: self)
self.visiblePages.removeAll()
self.recycledPages.removeAll()
// Update current page index
self.currentPageIndex =
(self.currentPageIndex > 0) ? max(0, self.currentPageIndex, numberOfPages - 1) : 0
// Update Layout
if self.isViewLoaded {
while self.subviews.count > 0 {
self.subviews.last?.removeFromSuperview()
}
self.tilePages()
self.setNeedsLayout()
}
}
func dequeue(at index: Int) -> PageReusableView {
if let page = self.recycledPages.first,
let recycledPage = self.recycledPages.remove(page) {
return recycledPage
}
return PageReusableView(frame: .zero)
}
func contentOffsetForPage(at index: Int) -> CGPoint {
let pageCount = dataSource.numberOfPages(in: self)
let pageWidth = self.bounds.size.width * self.bounds.size.width * pageCount.f
return CGPoint(x: pageWidth * index.f, y: 0)
}
func frameForPageView(at index: Int) -> CGRect {
let bounds = self.bounds
let pageFrame = CGRect(
x: bounds.size.width * index.f,
y: bounds.y,
width: bounds.size.width,
height: bounds.size.height
)
return pageFrame
}
func visiblePageView(at index: Int) -> PageReusableView? {
for visiblePage in self.visiblePages {
if visiblePage.index == index {
return visiblePage
}
}
return nil
}
func visiblePageViews() -> [PageReusableView] {
return self.visiblePages
.map { $0 }
.sorted { $0.index < $1.index }
}
// MARK: Private Methods
private func layoutVisiblePages() {
defer { self.isLayouting = false }
self.isLayouting = true
let numberOfPages = self.dataSource.numberOfPages(in: self)
self.contentSize = CGSize(width: self.bounds.size.width * numberOfPages.f, height: self.bounds.size.height)
for page in self.visiblePages {
let index = page.index
page.frame = self.frameForPageView(at: index)
}
self.contentOffset = self.contentOffsetForPage(at: self.currentPageIndex)
self.displayPageView(at: self.currentPageIndex)
}
private func displayPageView(at index: Int) {
let numberOfPages = dataSource.numberOfPages(in: self)
guard numberOfPages > 0 else { return }
if index != self.previousPageIndex {
if let visiblePageView = self.visiblePageView(at: index),
let delegate = self.delegate as? PagingViewDelegate {
delegate.pagingView?(self, didDisplay: visiblePageView, forAt: index)
}
self.previousPageIndex = index
}
}
fileprivate func tilePages() {
let numberOfPages = self.dataSource.numberOfPages(in: self)
let visibleBounds = self.bounds
var visibleFirstIndex = Int(floorf(Float(visibleBounds.minX / visibleBounds.width)))
visibleFirstIndex = (visibleFirstIndex < 0) ? 0 : visibleFirstIndex
visibleFirstIndex = (visibleFirstIndex > numberOfPages - 1) ? numberOfPages - 1 : visibleFirstIndex
var visibleLastIndex = Int(floorf(Float(visibleBounds.maxX / visibleBounds.width)))
visibleLastIndex = (visibleLastIndex < 0) ? 0 : visibleLastIndex
visibleLastIndex = (visibleLastIndex > numberOfPages - 1) ? numberOfPages - 1 : visibleLastIndex
var pageIndex = 0
for page in self.visiblePages {
pageIndex = page.index
if pageIndex < visibleFirstIndex || pageIndex > visibleLastIndex {
self.recycledPages.insert(page)
page.prepareForReuse()
page.removeFromSuperview()
}
}
self.visiblePages = self.visiblePages.subtracting(self.recycledPages)
// Only keep 2 recycled pages
while self.recycledPages.count > 2 {
self.recycledPages.remove(recycledPages.first!)
}
for index in visibleFirstIndex ... visibleLastIndex {
if self.visiblePageView(at: index) == nil {
let page = self.dataSource.pagingView(self, pageForAt: index)
page.frame = self.frameForPageView(at: index)
page.index = index
self.visiblePages.insert(page)
self.addSubview(page)
}
}
}
}
class ViewController: BaseViewController {
var pagingScrollView = PagingView()
override func viewDidLoad() {
super.viewDidLoad()
self.pagingScrollView.dataSource = self
self.view.addSubview(self.pagingScrollView)
}
override func setupConstraints() {
super.setupConstraints()
self.pagingScrollView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
extension ViewController: PagingViewDataSource {
func pagingView(_ pagingView: PagingView, pageForAt index: Int) -> PageReusableView {
let pageView = pagingView.dequeue(at: index)
return pageView
}
func numberOfPages(in pagingView: PagingView) -> Int {
return 4
}
}
Created
November 7, 2018 13:59
-
-
Save audrl1010/2fb94d84111b46d5621ec2d60ffcd840 to your computer and use it in GitHub Desktop.
UITableView 비슷하게 구현해봄..
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment