Skip to content

Instantly share code, notes, and snippets.

@audrl1010
Created November 7, 2018 13:59
Show Gist options
  • Save audrl1010/2fb94d84111b46d5621ec2d60ffcd840 to your computer and use it in GitHub Desktop.
Save audrl1010/2fb94d84111b46d5621ec2d60ffcd840 to your computer and use it in GitHub Desktop.
UITableView 비슷하게 구현해봄..
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
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment