import UIKit enum Section: CaseIterable { case main } // MARK: Controller class CollectionViewController : UICollectionViewController { let presenter: CollectionPresenter // MARK: Init init(presenter: CollectionPresenter) { self.presenter = presenter super.init(collectionViewLayout: UICollectionViewFlowLayout()) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: Life cycle override func viewDidLoad() { super.viewDidLoad() setupCollection() setupPagination() presenter.fetchPresentationData() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // presenter.fetchPresentationData() } // MARK: CollectionView lazy var dataSource = makeDataSource() private lazy var delegate = makeDelegate() private lazy var prefetchDataSource = makePrefetchDataSource() func setupCollection() { collectionView?.backgroundColor = .white collectionView?.register(Cell.self, forCellWithReuseIdentifier: "PlayCell") collectionView?.dataSource = dataSource collectionView?.prefetchDataSource = prefetchDataSource collectionView?.delegate = delegate } // MARK: Pagination private lazy var paginationManager: VerticalPaginationManager = { let manager = VerticalPaginationManager(scrollView: collectionView) manager.delegate = self return manager }() } extension CollectionViewController { func makeDataSource() -> UICollectionViewDiffableDataSource<Section, Model.Word> { return UICollectionViewDiffableDataSource<Section, Model.Word>(collectionView: collectionView) { collectionView, indexPath, item -> Cell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlayCell", for: indexPath) as? Cell cell?.backgroundColor = indexPath.row % 2 == 0 ? .systemBlue : .systemGray cell?.contentLabel.text = item.name return cell } } func makePrefetchDataSource() -> UICollectionViewDataSourcePrefetching { let dataSource = CollectionViewDataSource() dataSource.presenter = presenter return dataSource } func makeDelegate() -> UICollectionViewDelegate { let delegate = CollectionViewDelegate() delegate.presenter = presenter return delegate } } private class CollectionViewDelegate: NSObject, UICollectionViewDelegate { weak var presenter: CollectionPresenter? func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { // let scrollVelocity = collectionView.panGestureRecognizer.velocity(in: collectionView.superview) // if scrollVelocity.y > 0.0 { // // print("going down") // } else if scrollVelocity.y < 0.0 { // print("going up, \(collectionView.collectionViewLayout.collectionViewContentSize.height) && \(cell.frame.origin.y)") // let size = cell.frame.origin.y / collectionView.collectionViewLayout.collectionViewContentSize.height // print(size) // if size > 0.3 { // presenter?.fetchNextPageIfNeeded(whileIsPreloadingCellAtIndex: indexPath.row) // } // } // // print("\(cell.frame.origin.y) \(indexPath)") // // presenter?.fetchNextPageIfNeeded(whileIsPreloadingCellAtIndex: indexPath.row) } } private class CollectionViewDataSource: NSObject, UICollectionViewDataSourcePrefetching { weak var presenter: CollectionPresenter? func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { // let scrollVelocity = collectionView.panGestureRecognizer.velocity(in: collectionView.superview) // if scrollVelocity.y > 0.0 { // // print("going down") // } else if scrollVelocity.y < 0.0 { // print("going up, \(collectionView.collectionViewLayout.collectionViewContentSize.height)") // presenter?.fetchNextPageIfNeeded(whileIsPreloadingCells: indexPaths.map({ $0.row })) // } } } // MARK: Pagination extension CollectionViewController: VerticalPaginationManagerDelegate { private func setupPagination() { self.paginationManager.refreshViewColor = .clear self.paginationManager.loaderColor = .white } func refreshAll(completion: @escaping (Bool) -> Void) { } func loadMore(completion: @escaping (Bool) -> Void) { presenter.fetchNextPageIfNeeded() completion(true) } } // MARK: Presenter class CollectionPresenter { weak var presentingView: CollectionViewController? let dataProvider: CollectionDataProvider private var currentOffset: Int = 0 init(dataProvider: CollectionDataProvider) { self.dataProvider = dataProvider } func fetchPresentationData() { fetchFirstPage() } func fetchFirstPage() { dataProvider.retrieveData { result in switch result { case .success(let data): self.updateOffset(data.next) var snapshot = NSDiffableDataSourceSnapshot<Section, Model.Word>() snapshot.appendSections([.main]) snapshot.appendItems(data.words, toSection: .main) self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil) case .failure(_): break } } } func fetchNextPageIfNeeded() { dataProvider.retrieveData(offset: currentOffset, limit: 10) { result in switch result { case .success(let data): self.updateOffset(data.next) if var snapshot = self.presentingView?.dataSource.snapshot() { snapshot.appendItems(data.words, toSection: .main) self.presentingView?.dataSource.apply(snapshot, animatingDifferences: true, completion: nil) } case .failure(_): break } } } private func updateOffset(_ next: String?) { guard let next = next else { return } guard let offset = extractParam(from: next, paramName: "offset") else { return } if let newOffset = Int(offset) { currentOffset = newOffset print(currentOffset) } } fileprivate func extractParam(from query: String, paramName: String) -> String? { let params = query.components(separatedBy: "&") for param in params { let paramKeyValue = param.components(separatedBy: "=") if let name = paramKeyValue.first { if name == paramName { return paramKeyValue.last } } } return nil } } // MARK: DataProvider final class CollectionDataProvider { private lazy var poetry = "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? I. Quae res in civitate duae plurimum possunt, eae contra nos ambae faciunt in hoc tempore, summa gratia et eloquentia; quarum alterum, C. Aquili, vereor, alteram metuo. Eloquentia Q. Hortensi ne me in dicendo impediat, non nihil commoveor, gratia ***. Naevi ne P. Quinctio noceat, id vero non mediocriter pertimesco. Neque hoc tanto opere querendum videretur, haec summa in illis esse, si in nobis essent saltem mediocria; verum ita se res habet, ut ego, qui neque usu satis et ingenio parum possum, *** patrono disertissimo comparer, P. Quinctius, cui tenues opes, nullae facultates, exiguae amicorum copiae sunt, *** adversario gratiosissimo contendat. Illud quoque nobis accedit incommodum, quod M. Iunius, qui hanc causam aliquotiens apud te egit, **** et in aliis causis exercitatus et in hac multum ac saepe versatus, hoc tempore abest nova legatione impeditus, et ad me ventum est qui, ut summa haberem cetera, temporis quidem certe vix satis habui ut rem tantam, tot controversiis implicatam, possem cognoscere. Ita quod mihi consuevit in ceteris causis esse adiumento, id quoque in hac causa deficit. Nam, quod ingenio minus possum, subsidium mihi diligentia comparavi; quae quanta sit, nisi tempus et spatium datum sit, intellegi non potest. Quae quo plura sunt, C. Aquili, eo te et hos qui tibi in consilio sunt meliore mente nostra verba audire oportebit, ut multis incommodis veritas debilitata tandem aequitate talium virorum recreetur. Quod si tu iudex nullo praesidio fuisse videbere contra vim et gratiam solitudini atque inopiae, si apud hoc consilium ex opibus, non ex veritate causa pendetur, profecto nihil est iam sanctum atque sincerum in civitate, nihil est quod humilitatem cuiusquam gravitas et virtus iudicis consoletur. Certe aut apud te et hos qui tibi adsunt veritas valebit, aut ex hoc loco repulsa vi et gratia locum ubi consistat reperire non poterit. II. Non eo dico, C. Aquili, quo mihi veniat in dubium tua fides et constantia, aut quo non in his quos tibi advocavisti viris lectissimis civitatis spem summam habere P. Quinctius, debeat. Quid ergo est? Primum magnitudo periculi summo timore hominem adficit, quod uno iudicio de fortunis omnibus decernit, idque dum cogitat, non minus saepe ei venit in mentem potestatis quam aequitatis tuae, propterea quod omnes quorum in alterius manu vita posita est saepius illud cogitant, quid possit is cuius in dicione ac potestate sunt quam quid debeat facere. Deinde habet adversarium P. Quinctius verbo ***. Naevium, re ura huiusce aetatis homines disertissimos, fortissimos, florentissimos nostrae civitatis, qui communi studio summis opibus *** Naevium defendunt, si id est defendere, cupiditati alterius obtemperare quo is facilius quem velit iniquo iudicio opprimere possit. Nam quid hoc iniquius aut indignius, C. Aquili, dici aut commemorari potest, quam me qui caput alterius, famam fortunasque defendam priore loco causam dicere? *** praesertim Q. Hortensius qui in hoc iudicio partis accusatoris obtinet contra me sit dicturus, cui summam copiam facultatemque dicendi natura largita est. Ita fit ut ego qui tela depellere et volneribus mederi debeam tum id facere cogar *** etiam telum adversarius nullum iecerit, illis autem id tempus impugnandi detur *** et vitandi illorum impetus potestas adempta nobis erit et, si qua in re, id quod parati sunt facere, falsum crimen quasi venenatum aliquod telum iecerint, medicinae faciendae locus non erit. Id accidit praetoris iniquitate et iniuria, primum quod contra omnium consuetudinem iudicium prius de probro quam de re maluit fieri, deinde quod ita constituit id ipsum iudicium ut reus, ante quam verbum accusatoris audisset, causam dicere cogeretur. Quod eorum gratia et potentia factum ao est qui, quasi sua res aut honos agatur, ita diligenter ***. Naevi studio et cupiditati morem gerunt et in eius modi rebus opes suas experiuntur, in quibus, quo plus propter virtutem nobilitatemque possunt, eo minus quantum possint debent ostendere." func retrieveData(offset: Int = 0, limit: Int = 256, completion: @escaping (Result<Model, Error>) -> Void) { let count = poetry.count let words = makeWords(offset: offset, limit: limit, count: count) let next = makeNext(offset: offset, limit: limit, count: count) let previous = makePrevious(offset: offset, limit: limit, count: count) let model = Model(words: words, count: count, next: next, previous: previous) completion(Result.success(model)) } private func makeWords(offset: Int = 0, limit: Int = 256, count: Int) -> [Model.Word] { let components = poetry.components(separatedBy: " ") print(components) if limit < 1 || limit > count { return [] } let newOffset = offset + limit // let newLimit = min(count - limit, limit) if newOffset > count { return [] } return components[offset..<offset + limit].map({ Model.Word(name: $0) }) } private func makeNext(offset: Int = 0, limit: Int = 256, count: Int) -> String? { if limit < 1 || limit > count { return nil } let newOffset = offset + limit let newLimit = min(count - limit, limit) if newOffset > count { return nil } else { return "offset=\(newOffset)&limit=\(newLimit)" } } private func makePrevious(offset: Int = 0, limit: Int = 256, count: Int) -> String? { if limit < 1 || limit > count { return nil } let newOffset = offset - limit let newLimit = min(count - limit, limit) if newOffset < 0 { return nil } else { return "offset=\(newOffset)&limit=\(newLimit)" } } } // MARK: Model struct Model: Codable { struct Word: Codable { let name: String } let words: [Word] // Pagination let count: Int let next: String? let previous: String? } extension Model: Hashable { var identifier: UUID { UUID() } func hash(into hasher: inout Hasher) { hasher.combine(identifier) } } extension Model: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { return lhs.identifier == rhs.identifier } } extension Model.Word: Hashable { var identifier: UUID { UUID() } func hash(into hasher: inout Hasher) { hasher.combine(identifier) } } extension Model.Word: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { return lhs.identifier == rhs.identifier } } // MARK: Cell class Cell: UICollectionViewCell { lazy var contentLabel: UILabel = { let label = UILabel(frame: .zero) label.textColor = .systemRed label.textAlignment = .center label.adjustsFontSizeToFitWidth = true label.translatesAutoresizingMaskIntoConstraints = false self.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: self.centerXAnchor), label.centerYAnchor.constraint(equalTo: self.centerYAnchor), label.topAnchor.constraint(equalTo: self.topAnchor, constant: 8), label.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8) ]) return label }() }