import Foundation import UIKit protocol VerticalPaginationManagerDelegate: class { func refreshAll(completion: @escaping (Bool) -> Void) func loadMore(completion: @escaping (Bool) -> Void) } class VerticalPaginationManager: NSObject { private var isLoading = false private var isObservingKeyPath: Bool = false private var scrollView: UIScrollView! private var bottomMostLoader: UIView? var refreshViewColor: UIColor = .white var loaderColor: UIColor = .white weak var delegate: VerticalPaginationManagerDelegate? init(scrollView: UIScrollView) { super.init() self.scrollView = scrollView self.addScrollViewOffsetObserver() } deinit { self.removeScrollViewOffsetObserver() } func initialLoad() { self.delegate?.refreshAll(completion: { _ in }) } } // MARK: BOTTOM LOADER extension VerticalPaginationManager { private func addBottomMostControl() { let view = UIView() view.backgroundColor = self.refreshViewColor let activity = UIActivityIndicatorView(style: .medium) activity.color = self.loaderColor activity.frame = view.bounds activity.startAnimating() view.addSubview(activity) activity.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ activity.centerXAnchor.constraint(equalTo: view.centerXAnchor), activity.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) self.bottomMostLoader = view self.scrollView.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: scrollView.bottomAnchor), view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), view.heightAnchor.constraint(equalToConstant: 60) ]) } func removeBottomLoader() { self.bottomMostLoader?.removeFromSuperview() self.bottomMostLoader = nil } } // MARK: OFFSET OBSERVER extension VerticalPaginationManager { private func addScrollViewOffsetObserver() { if self.isObservingKeyPath { return } self.scrollView.addObserver( self, forKeyPath: "contentOffset", options: [.new], context: nil ) self.isObservingKeyPath = true } private func removeScrollViewOffsetObserver() { if self.isObservingKeyPath { self.scrollView.removeObserver(self, forKeyPath: "contentOffset") } self.isObservingKeyPath = false } override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let object = object as? UIScrollView, let keyPath = keyPath, let newValue = change?[.newKey] as? CGPoint, object == self.scrollView, keyPath == "contentOffset" else { return } self.setContentOffSet(newValue) } private func setContentOffSet(_ offset: CGPoint) { let offsetY = offset.y let contentHeight = self.scrollView.contentSize.height let frameHeight = self.scrollView.bounds.size.height let diffY = contentHeight - frameHeight if contentHeight > frameHeight, offsetY > (diffY + 130) && !self.isLoading { self.isLoading = true self.addBottomMostControl() self.delegate?.loadMore { success in self.isLoading = false self.removeBottomLoader() } } } }