From 93a6deefd45abf4ff397f1f4fabdaa2ca3973329 Mon Sep 17 00:00:00 2001 From: Konstantin Porokhov Date: Tue, 25 Jul 2023 15:25:57 +0400 Subject: [PATCH 1/3] created strategies --- .../PaginatableCollectionViewController.swift | 21 ++- ...nPaginatableCollectionViewController.swift | 78 ++++------ .../PaginatableTableViewController.swift | 17 ++- ...ectionPaginatableTableViewController.swift | 65 +++------ Gemfile.lock | 13 +- .../CollectionPaginatablePlugin.swift | 100 +++++++------ .../CollectionTopPaginatablePlugin.swift | 134 ------------------ .../PaginationState.swift | 14 ++ .../PagingDirection.swift | 32 +++++ .../Protocols/ContentOffsetStateKeeper.swift | 28 ++++ .../Protocols/PageIndexPathComparator.swift | 18 +++ .../Protocols/PaginationViewManager.swift | 16 +++ .../Strategy/BottomPaginationStrategy.swift | 51 +++++++ .../Strategy/TopPaginationStrategy.swift | 66 +++++++++ .../TablePaginatablePlugin.swift | 105 ++++++++------ 15 files changed, 405 insertions(+), 353 deletions(-) delete mode 100644 Source/Collection/Plugins/PluginAction/CollectionTopPaginatablePlugin.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift create mode 100644 Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift rename Source/Table/Plugins/PluginAction/{ => TablePaginatablePlugin}/TablePaginatablePlugin.swift (67%) diff --git a/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift index 3471153f1..94ed43efc 100644 --- a/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift @@ -28,7 +28,7 @@ final class PaginatableCollectionViewController: UIViewController { private lazy var progressView = PaginatorView(frame: .init(x: 0, y: 0, width: collectionView.frame.width, height: 80)) private lazy var adapter = collectionView.rddm.baseBuilder - .add(plugin: .paginatable(progressView: progressView, output: self)) + .add(plugin: .bottomPaginatable(progressView: progressView, output: self)) .build() private weak var paginatableInput: PaginatableInput? @@ -72,8 +72,8 @@ private extension PaginatableCollectionViewController { activityIndicator.startAnimating() // hide footer - paginatableInput?.updatePagination(canIterate: false) - paginatableInput?.updateProgress(isLoading: false) + paginatableInput?.updatePaginationEnabled(false, at: .forward(.bottom)) + paginatableInput?.updatePaginationState(.idle, at: .forward(.bottom)) // imitation of loading first page delay(.now() + .seconds(3)) { [weak self] in @@ -84,7 +84,7 @@ private extension PaginatableCollectionViewController { self?.activityIndicator?.stopAnimating() // show footer - self?.paginatableInput?.updatePagination(canIterate: true) + self?.paginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom)) } } @@ -137,24 +137,23 @@ private extension PaginatableCollectionViewController { extension PaginatableCollectionViewController: PaginatableOutput { - func onPaginationInitialized(with input: PaginatableInput) { + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) { paginatableInput = input } - func loadNextPage(with input: PaginatableInput) { + func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) { - input.updateProgress(isLoading: true) + input.updatePaginationState(.loading, at: direction) delay(.now() + .seconds(3)) { [weak self, weak input] in let canFillNext = self?.canFillNext() ?? false if canFillNext { let canIterate = self?.fillNext() ?? false - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) + input?.updatePaginationState(.idle, at: direction) + input?.updatePaginationEnabled(canIterate, at: direction) } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) + input?.updatePaginationState(.error(SampleError.sample), at: direction) } } } diff --git a/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift index ee7d180c6..35a4d14c4 100644 --- a/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift @@ -44,14 +44,12 @@ final class TwoDirectionPaginatableCollectionViewController: UIViewController { height: Constants.paginatorHeight)) private lazy var adapter = collectionView.rddm.baseBuilder - .add(plugin: .paginatable(progressView: bottomProgressView, output: self)) - .add(plugin: .topPaginatable(progressView: topProgressView, - output: self, - isSaveScrollPositionNeeded: true)) + .add(plugin: .bottomPaginatable(progressView: bottomProgressView, output: self)) + .add(plugin: .topPaginatable(progressView: topProgressView, output: self)) .build() private weak var bottomPaginatableInput: PaginatableInput? - private weak var topPaginatableInput: TopPaginatableInput? + private weak var topPaginatableInput: PaginatableInput? private var isFirstPageLoading = true private var currentTopPage = 0 @@ -95,10 +93,10 @@ private extension TwoDirectionPaginatableCollectionViewController { activityIndicator.startAnimating() // hide footer - bottomPaginatableInput?.updatePagination(canIterate: false) - topPaginatableInput?.updatePagination(canIterate: false) - bottomPaginatableInput?.updateProgress(isLoading: false) - topPaginatableInput?.updateProgress(isLoading: false) + bottomPaginatableInput?.updatePaginationEnabled(false, at: .forward(.bottom)) + topPaginatableInput?.updatePaginationEnabled(false, at: .backward(.top)) + bottomPaginatableInput?.updatePaginationState(.idle, at: .forward(.bottom)) + topPaginatableInput?.updatePaginationState(.loading, at: .backward(.top)) // imitation of loading first page delay(.now() + .seconds(3)) { [weak self] in @@ -112,8 +110,8 @@ private extension TwoDirectionPaginatableCollectionViewController { self?.collectionView.scrollToItem(at: Constants.firstPageMiddleIndexPath, at: .centeredVertically, animated: false) // show pagination loader if update is needed - self?.bottomPaginatableInput?.updatePagination(canIterate: true) - self?.topPaginatableInput?.updatePagination(canIterate: true) + self?.bottomPaginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom)) + self?.topPaginatableInput?.updatePaginationEnabled(true, at: .backward(.top)) } } @@ -188,52 +186,34 @@ private extension TwoDirectionPaginatableCollectionViewController { extension TwoDirectionPaginatableCollectionViewController: PaginatableOutput { - func onPaginationInitialized(with input: PaginatableInput) { - bottomPaginatableInput = input + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) { + switch direction { + case .backward: + topPaginatableInput = input + case .forward: + bottomPaginatableInput = input + } } - func loadNextPage(with input: PaginatableInput) { + func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) { - input.updateProgress(isLoading: true) + input.updatePaginationState(.loading, at: direction) delay(.now() + .seconds(3)) { [weak self, weak input] in let canFillNext = self?.canFillPages() ?? false if canFillNext { - let canIterate = self?.fillNext() ?? false - - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) - } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) - } - } - } - -} - -// MARK: - TopPaginatableOutput - -extension TwoDirectionPaginatableCollectionViewController: TopPaginatableOutput { - - func onTopPaginationInitialized(with input: ReactiveDataDisplayManager.TopPaginatableInput) { - topPaginatableInput = input - } - - func loadPrevPage(with input: ReactiveDataDisplayManager.TopPaginatableInput) { - input.updateProgress(isLoading: true) - - delay(.now() + .seconds(2)) { [weak self, weak input] in - guard let self = self else { - return - } - if self.canFillPages() { - let canIterate = self.fillPrev() - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) + let canIterate: Bool + switch direction { + case .backward: + canIterate = self?.fillPrev() ?? false + case .forward: + canIterate = self?.fillNext() ?? false + } + + input?.updatePaginationState(.idle, at: direction) + input?.updatePaginationEnabled(canIterate, at: direction) } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) + input?.updatePaginationState(.error(SampleError.sample), at: direction) } } } diff --git a/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift b/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift index d8e2878bf..7ed232694 100644 --- a/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift +++ b/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift @@ -72,7 +72,7 @@ private extension PaginatableTableViewController { activityIndicator.startAnimating() // hide footer - paginatableInput?.updatePagination(canIterate: false) + paginatableInput?.updatePaginationEnabled(false, at: .forward(.bottom)) // imitation of loading first page delay(.now() + .seconds(3)) { [weak self] in @@ -85,7 +85,7 @@ private extension PaginatableTableViewController { self?.activityIndicator?.isHidden = true // show footer - self?.paginatableInput?.updatePagination(canIterate: true) + self?.paginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom)) } } @@ -130,24 +130,23 @@ private extension PaginatableTableViewController { extension PaginatableTableViewController: PaginatableOutput { - func onPaginationInitialized(with input: PaginatableInput) { + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) { paginatableInput = input } - func loadNextPage(with input: PaginatableInput) { + func loadNextPage(with input: PaginatableInput, at direction: ReactiveDataDisplayManager.PagingDirection) { - input.updateProgress(isLoading: true) + input.updatePaginationState(.loading, at: direction) delay(.now() + .seconds(3)) { [weak self, weak input] in let canFillNext = self?.canFillNext() ?? false if canFillNext { let canIterate = self?.fillNext() ?? false - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) + input?.updatePaginationState(.idle, at: direction) + input?.updatePaginationEnabled(canIterate, at: direction) } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) + input?.updatePaginationState(.error(SampleError.sample), at: direction) } } } diff --git a/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift b/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift index 453cc312b..eaed68750 100644 --- a/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift +++ b/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift @@ -43,15 +43,12 @@ final class TwoDirectionPaginatableTableViewController: UIViewController { height: Constants.paginatorViewHeight)) private lazy var adapter = tableView.rddm.manualBuilder - .add(plugin: .paginatable(progressView: bottomProgressView, - output: self)) - .add(plugin: .topPaginatable(progressView: topProgressView, - output: self, - isSaveScrollPositionNeeded: true)) + .add(plugin: .paginatable(progressView: topProgressView, output: self, direction: .backward(.top))) +// .add(plugin: .paginatable(progressView: bottomProgressView, output: self)) .build() private weak var bottomPaginatableInput: PaginatableInput? - private weak var topPaginatableInput: TopPaginatableInput? + private weak var topPaginatableInput: PaginatableInput? private var isFirstPageLoading = true private var currentTopPage = 0 @@ -94,8 +91,8 @@ private extension TwoDirectionPaginatableTableViewController { activityIndicator.startAnimating() // hide footer and header - bottomPaginatableInput?.updatePagination(canIterate: false) - topPaginatableInput?.updatePagination(canIterate: false) + bottomPaginatableInput?.updatePaginationEnabled(false, at: .forward(.bottom)) + topPaginatableInput?.updatePaginationEnabled(false, at: .backward(.top)) // imitation of loading first page delay(.now() + .seconds(1)) { [weak self] in @@ -111,8 +108,8 @@ private extension TwoDirectionPaginatableTableViewController { self?.activityIndicator?.isHidden = true // show pagination loader if update is needed - self?.bottomPaginatableInput?.updatePagination(canIterate: true) - self?.topPaginatableInput?.updatePagination(canIterate: true) + self?.bottomPaginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom)) + self?.topPaginatableInput?.updatePaginationEnabled(true, at: .backward(.top)) } } @@ -180,12 +177,17 @@ private extension TwoDirectionPaginatableTableViewController { extension TwoDirectionPaginatableTableViewController: PaginatableOutput { - func onPaginationInitialized(with input: PaginatableInput) { - bottomPaginatableInput = input + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) { + switch direction { + case .backward: + topPaginatableInput = input + case .forward: + bottomPaginatableInput = input + } } - func loadNextPage(with input: PaginatableInput) { - input.updateProgress(isLoading: true) + func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) { + input.updatePaginationState(.loading, at: direction) delay(.now() + .seconds(2)) { [weak self, weak input] in let canFillPages = self?.canFillPages() ?? false @@ -193,39 +195,10 @@ extension TwoDirectionPaginatableTableViewController: PaginatableOutput { if canFillPages { let canIterate = self?.fillNext() ?? false - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) - } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) - } - } - } - -} - -// MARK: - TopPaginatableOutput - -extension TwoDirectionPaginatableTableViewController: TopPaginatableOutput { - - func onTopPaginationInitialized(with input: TopPaginatableInput) { - topPaginatableInput = input - } - - func loadPrevPage(with input: TopPaginatableInput) { - input.updateProgress(isLoading: true) - - delay(.now() + .seconds(2)) { [weak self, weak input] in - guard let self = self else { - return - } - if self.canFillPages() { - let canIterate = self.fillPrev() - input?.updateProgress(isLoading: false) - input?.updatePagination(canIterate: canIterate) + input?.updatePaginationState(.idle, at: direction) + input?.updatePaginationEnabled(canIterate, at: direction) } else { - input?.updateProgress(isLoading: false) - input?.updateError(SampleError.sample) + input?.updatePaginationState(.error(SampleError.sample), at: direction) } } } diff --git a/Gemfile.lock b/Gemfile.lock index 2de8cb52b..fde5de546 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (6.1.7.3) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -67,16 +67,16 @@ GEM i18n (1.13.0) concurrent-ruby (~> 1.0) json (2.6.3) - minitest (5.18.0) + minitest (5.18.1) molinillo (0.8.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - nokogiri (1.15.2-arm64-darwin) + nokogiri (1.13.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-darwin) + nokogiri (1.13.10-x86_64-darwin) racc (~> 1.4) - nokogiri (1.15.2-x86_64-linux) + nokogiri (1.13.10-x86_64-linux) racc (~> 1.4) public_suffix (4.0.7) racc (1.6.2) @@ -105,6 +105,7 @@ GEM zeitwerk (2.6.8) PLATFORMS + arm64-darwin-21 arm64-darwin-22 x86_64-darwin-21 x86_64-linux @@ -115,4 +116,4 @@ DEPENDENCIES xcpretty (~> 0.3.0) BUNDLED WITH - 2.4.6 + 2.3.26 diff --git a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift index 7326321f0..f47f14c50 100644 --- a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift +++ b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift @@ -27,8 +27,12 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin private var isLoading = false private var isErrorWasReceived = false + private var direction: PagingDirection - private weak var collectionView: UICollectionView? + // MARK: - Properties + + weak var collectionView: UICollectionView? + var paginationStrategy: PaginationStrategy? /// Property which indicating availability of pages public private(set) var canIterate = false { @@ -37,16 +41,12 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin guard progressView.superview == nil else { return } - - collectionView?.addSubview(progressView) - collectionView?.contentInset.bottom += progressView.frame.height + paginationStrategy?.addPafinationView() } else { guard progressView.superview != nil else { return } - - progressView.removeFromSuperview() - collectionView?.contentInset.bottom -= progressView.frame.height + paginationStrategy?.removePafinationView() } } } @@ -55,23 +55,26 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. /// - parameter output: output signals to hide `progressView` from footer - init(progressView: ProgressView, with output: PaginatableOutput) { + init(progressView: ProgressView, with output: PaginatableOutput, direction: PagingDirection = .forward(.bottom)) { self.progressView = progressView self.output = output + self.direction = direction } // MARK: - BaseTablePlugin public override func setup(with manager: BaseCollectionManager?) { collectionView = manager?.view + paginationStrategy?.scrollView = manager?.view + paginationStrategy?.progressView = progressView canIterate = false - output?.onPaginationInitialized(with: self) + output?.onPaginationInitialized(with: self, at: direction) self.progressView.setOnRetry { [weak self] in - guard let input = self, let output = self?.output else { + guard let input = self, let output = self?.output, let direction = self?.direction else { return } self?.isErrorWasReceived = false - output.loadNextPage(with: input) + output.loadNextPage(with: input, at: direction) } } @@ -80,52 +83,43 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin switch event { case .willDisplayCell(let indexPath): if progressView.frame.minY != collectionView?.contentSize.height { - setProgressViewFinalFrame() + paginationStrategy?.setProgressViewFinalFrame() } - guard let sections = manager?.sections, !isErrorWasReceived else { - return - } - let lastSectionIndex = sections.count - 1 - let lastCellInLastSectionIndex = sections[lastSectionIndex].generators.count - 1 - - let lastCellIndexPath = IndexPath(row: lastCellInLastSectionIndex, section: lastSectionIndex) - guard indexPath == lastCellIndexPath, canIterate, !isLoading else { + guard indexPath == paginationStrategy?.getIndexPath(with: manager), canIterate, !isLoading, !isErrorWasReceived else { return } - - output?.loadNextPage(with: self) + output?.loadNextPage(with: self, at: self.direction) default: break } } - // MARK: - Private methods - - func setProgressViewFinalFrame() { - // Hack: Update progressView position. Imitation of global footer view like `tableFooterView` - progressView.frame = .init(origin: .init(x: progressView.frame.origin.x, - y: collectionView?.contentSize.height ?? 0), - size: progressView.frame.size) - } - } // MARK: - PaginatableInput extension CollectionPaginatablePlugin: PaginatableInput { - public func updateProgress(isLoading: Bool) { - self.isLoading = isLoading - progressView.showProgress(isLoading) - } + public func updatePaginationEnabled(_ canIterate: Bool, at direction: PagingDirection) { + self.canIterate = canIterate + self.direction = direction - public func updateError(_ error: Error?) { - progressView.showError(error) - isErrorWasReceived = true + paginationStrategy?.resetOffset(canIterate: canIterate) } - public func updatePagination(canIterate: Bool) { - self.canIterate = canIterate + public func updatePaginationState(_ state: PaginationState, at direction: PagingDirection) { + switch state { + case .idle: + isLoading = false + case .loading: + isLoading = true + paginationStrategy?.saveCurrentState() + case .error(let error): + isLoading = false + isErrorWasReceived = true + progressView.showError(error) + } + progressView.showProgress(isLoading) } } @@ -141,23 +135,25 @@ public extension BaseCollectionPlugin { /// /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. /// - parameter output: output signals to hide `progressView` from footer - static func paginatable(progressView: CollectionPaginatablePlugin.ProgressView, - output: PaginatableOutput) -> CollectionPaginatablePlugin { - .init(progressView: progressView, with: output) + static func topPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, + output: PaginatableOutput) -> CollectionPaginatablePlugin { + let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .backward(.top)) + plugin.paginationStrategy = TopPaginationStrategy() + return plugin } - /// Plugin to display `progressView` while previous page is loading + /// Plugin to display `progressView` while next page is loading /// - /// Show `progressView` on `willDisplay` first cell. + /// Show `progressView` on `willDisplay` last cell. /// Hide `progressView` when finish loading request /// - /// - parameter progressView: indicator view to add inside header. Do not forget to init this view with valid frame size. - /// - parameter output: output signals to hide `progressView` from header - static func topPaginatable(progressView: CollectionTopPaginatablePlugin.ProgressView, - output: TopPaginatableOutput, - isSaveScrollPositionNeeded: Bool = true) -> CollectionTopPaginatablePlugin { - return CollectionTopPaginatablePlugin(progressView: progressView, with: output, isSaveScrollPositionNeeded: isSaveScrollPositionNeeded) - + /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. + /// - parameter output: output signals to hide `progressView` from footer + static func bottomPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, + output: PaginatableOutput) -> CollectionPaginatablePlugin { + let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .forward(.bottom)) + plugin.paginationStrategy = BottomPaginationStrategy() + return plugin } } diff --git a/Source/Collection/Plugins/PluginAction/CollectionTopPaginatablePlugin.swift b/Source/Collection/Plugins/PluginAction/CollectionTopPaginatablePlugin.swift deleted file mode 100644 index 8f8ea9263..000000000 --- a/Source/Collection/Plugins/PluginAction/CollectionTopPaginatablePlugin.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// CollectionTopPaginatablePlugin.swift -// ReactiveDataDisplayManager -// -// Created by Антон Голубейков on 23.06.2023. -// - -import UIKit - -/// Plugin to display `progressView` while prevous page is loading -/// -/// Show `progressView` on `willDisplay` first cell. -/// Hide `progressView` when finish loading request -/// -/// - Warning: Specify itemSize of your layout to proper `willDisplay` calls and correct `contentSize` -public class CollectionTopPaginatablePlugin: BaseCollectionPlugin { - - // MARK: - Nested types - - public typealias ProgressView = UIView & ProgressDisplayableItem - - // MARK: - Private Properties - - private let progressView: ProgressView - private weak var output: TopPaginatableOutput? - private let isSaveScrollPositionNeeded: Bool - - private var isLoading = false - private var isErrorWasReceived = false - - private weak var collectionView: UICollectionView? - - private var currentContentHeight: CGFloat? - - /// Property which indicating availability of pages - public private(set) var canIterate = false { - didSet { - if canIterate { - guard progressView.superview == nil else { - return - } - - collectionView?.addSubview(progressView) - collectionView?.contentInset.top += progressView.frame.height - } else { - guard progressView.superview != nil else { - return - } - - progressView.removeFromSuperview() - collectionView?.contentInset.top -= progressView.frame.height - } - } - } - - // MARK: - Initialization - - /// - parameter progressView: indicator view to add inside header. Do not forget to init this view with valid frame size. - /// - parameter output: output signals to hide `progressView` from header - init(progressView: ProgressView, with output: TopPaginatableOutput, isSaveScrollPositionNeeded: Bool) { - self.progressView = progressView - self.output = output - self.isSaveScrollPositionNeeded = isSaveScrollPositionNeeded - } - - // MARK: - BaseTablePlugin - - public override func setup(with manager: BaseCollectionManager?) { - collectionView = manager?.view - canIterate = false - output?.onTopPaginationInitialized(with: self) - self.progressView.setOnRetry { [weak self] in - guard let input = self, let output = self?.output else { - return - } - self?.isErrorWasReceived = false - output.loadPrevPage(with: input) - } - } - - public override func process(event: CollectionEvent, with manager: BaseCollectionManager?) { - - switch event { - case .willDisplayCell(let indexPath): - let firstCellIndexPath = IndexPath(row: 0, section: 0) - guard indexPath == firstCellIndexPath, canIterate, !isLoading, !isErrorWasReceived else { - return - } - - // Hack: Update progressView position. Imitation of global header view like `tableHeaderView` - - progressView.frame = .init(origin: .init(x: progressView.frame.origin.x, y: -progressView.frame.height), - size: progressView.frame.size) - - output?.loadPrevPage(with: self) - default: - break - } - } - -} - -// MARK: - PaginatableInput - -extension CollectionTopPaginatablePlugin: TopPaginatableInput { - - public func updateProgress(isLoading: Bool) { - self.isLoading = isLoading - progressView.showProgress(isLoading) - if isLoading { - currentContentHeight = collectionView?.contentSize.height - } - } - - public func updateError(_ error: Error?) { - progressView.showError(error) - isErrorWasReceived = true - } - - public func updatePagination(canIterate: Bool) { - self.canIterate = canIterate - if - canIterate, - isSaveScrollPositionNeeded, - let currentContentHeight = currentContentHeight, - let newContentHeight = collectionView?.contentSize.height - { - let finalOffset = CGPoint(x: 0, y: newContentHeight - currentContentHeight - progressView.frame.height) - collectionView?.setContentOffset(finalOffset, animated: false) - self.currentContentHeight = nil - } - } - -} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift new file mode 100644 index 000000000..fe46b83d4 --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift @@ -0,0 +1,14 @@ +// +// PaginationState.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 20.07.2023. +// + +import Foundation + +public enum PaginationState { + case idle + case loading + case error(Error?) +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift new file mode 100644 index 000000000..7115a158f --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift @@ -0,0 +1,32 @@ +// +// PagingDirection.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 20.07.2023. +// + +import Foundation + +public enum PagingDirection { + + public enum ForwardDirection { + case bottom, right + } + + public enum BackwardDirection { + case top, left + } + + case backward(BackwardDirection) + case forward(ForwardDirection) + + var isBackward: Bool { + switch self { + case .backward: + return true + case .forward: + return false + } + } + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift new file mode 100644 index 000000000..04a3a1572 --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift @@ -0,0 +1,28 @@ +// +// ContentOffsetStateKeeper.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 20.07.2023. +// + +import UIKit + +protocol ContentOffsetStateKeeper { + + var scrollView: UIScrollView? { get set } + var progressView: UIView? { get set } + + // Сохраняет contentSize.height + func saveCurrentState() + + // Вычисляет finalOffset и устанавливает его используя setContentOffset + func resetOffset(canIterate: Bool) + +} + +extension ContentOffsetStateKeeper { + + func saveCurrentState() { } + func resetOffset(canIterate: Bool) { } + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift new file mode 100644 index 000000000..041287f86 --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift @@ -0,0 +1,18 @@ +// +// PageIndexPathComparator.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 20.07.2023. +// + +import Foundation + +protocol PageIndexPathComparator { + + // Реализация хранит weak референс на SectionsProvider для доступа к массиву генераторов. + var sectionProvider: (any SectionsProvider)? { get set } + + // Сравнивает текущий индекс из willDisplay с последним/первым индексом. + func compare(currentIndexPath: IndexPath) -> Bool + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift new file mode 100644 index 000000000..6de80692e --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift @@ -0,0 +1,16 @@ +// +// PaginationViewManager.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 25.07.2023. +// + +import UIKit + +protocol PaginationStrategy: ContentOffsetStateKeeper { + + func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? + func addPafinationView() + func removePafinationView() + func setProgressViewFinalFrame() +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift new file mode 100644 index 000000000..57ee499db --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift @@ -0,0 +1,51 @@ +// +// BottomPaginationStrategy.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 25.07.2023. +// + +import UIKit + +final class BottomPaginationStrategy: PaginationStrategy { + + // MARK: - Properties + + weak var scrollView: UIScrollView? + weak var progressView: UIView? + + // MARK: - PaginationStrategy + + func addPafinationView() { + guard let progressView = progressView else { + return + } + scrollView?.addSubview(progressView) + scrollView?.contentInset.bottom += progressView.frame.height + } + + func removePafinationView() { + progressView?.removeFromSuperview() + scrollView?.contentInset.bottom -= progressView?.frame.height ?? .zero + } + + func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? { + guard let sections = manager?.sections else { + return nil + } + let lastSectionIndex = sections.count - 1 + let lastCellInLastSectionIndex = sections[lastSectionIndex].generators.count - 1 + + return IndexPath(row: lastCellInLastSectionIndex, section: lastSectionIndex) + } + + func setProgressViewFinalFrame() { + guard let progressViewFrame = progressView?.frame else { + return + } + // Hack: Update progressView position. Imitation of global footer view like `tableFooterView` + progressView?.frame = .init(origin: .init(x: progressViewFrame.origin.x, y: scrollView?.contentSize.height ?? 0), + size: progressViewFrame.size) + } + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift new file mode 100644 index 000000000..9e4cd77f3 --- /dev/null +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift @@ -0,0 +1,66 @@ +// +// TopPaginationStrategy.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 25.07.2023. +// + +import UIKit + +final class TopPaginationStrategy: PaginationStrategy { + + // MARK: - Properties + + weak var scrollView: UIScrollView? + weak var progressView: UIView? + + // MARK: - Private properties + + private var currentContentHeight: CGFloat? + + // MARK: - PaginationStrategy + + func saveCurrentState() { + currentContentHeight = scrollView?.contentSize.height + } + + func resetOffset(canIterate: Bool) { + guard + canIterate, + let currentContentHeight = currentContentHeight, + let newContentHeight = scrollView?.contentSize.height, + let progressViewHeight = progressView?.frame.height + else { return } + + let finalOffset = CGPoint(x: 0, y: newContentHeight - currentContentHeight - progressViewHeight) + scrollView?.setContentOffset(finalOffset, animated: false) + self.currentContentHeight = nil + } + + func addPafinationView() { + guard let progressView = progressView else { + return + } + scrollView?.addSubview(progressView) + scrollView?.contentInset.top += progressView.frame.height + } + + func removePafinationView() { + progressView?.removeFromSuperview() + scrollView?.contentInset.top -= progressView?.frame.height ?? .zero + } + + func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? { + IndexPath(row: 0, section: 0) + } + + func setProgressViewFinalFrame() { + guard let progressViewFrame = progressView?.frame else { + return + } + // Hack: Update progressView position. Imitation of global footer view like `tableFooterView` + progressView?.frame = .init(origin: .init(x: progressViewFrame.origin.x, y: -progressViewFrame.height), + size: progressViewFrame.size) + } + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift similarity index 67% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift rename to Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift index 99050c022..498c6d45e 100644 --- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift @@ -40,17 +40,16 @@ public protocol PaginatableInput: AnyObject { /// Call this method to control availability of **loadNextPage** action /// - /// - parameter canIterate: `true` if want to use last cell will display event to execute **loadNextPage** action - func updatePagination(canIterate: Bool) - - /// Call this method to control visibility of progressView in header/footer - /// - /// - parameter isLoading: `true` if want to show `progressView` in header/footer - func updateProgress(isLoading: Bool) - - /// - parameter error: some error got while loading of next/previous page. - /// You should transfer this error into UI representation. - func updateError(_ error: Error?) + /// - Parameters: + /// - canIterate: `true` if want to use last cell will display event to execute **loadNextPage** action + /// - direction: direction of pagination + func updatePaginationEnabled(_ canIterate: Bool, at direction: PagingDirection) + + /// Call this method to control visibility of progressView in header/footer, loading/error state + /// - Parameters: + /// - state: state of pagination + /// - direction: direction of pagination + func updatePaginationState(_ state: PaginationState, at direction: PagingDirection) } /// Input signals to control visibility of progressView in header @@ -77,12 +76,14 @@ public protocol PaginatableOutput: AnyObject { /// Called when collection has setup `TablePaginatablePlugin` /// /// - parameter input: input signals to hide `progressView` from footer - func onPaginationInitialized(with input: PaginatableInput) + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) /// Called when collection scrolled to last cell /// - /// - parameter input: input signals to hide `progressView` from footer - func loadNextPage(with input: PaginatableInput) + /// - Parameters: + /// - input: input signals to hide `progressView` from footer + /// - direction: direction of pagination + func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) } /// Output signals for loading previous page of content @@ -118,8 +119,12 @@ public class TablePaginatablePlugin: BaseTablePlugin { private var isLoading = false private var isErrorWasReceived = false + private var direction: PagingDirection - private weak var tableView: UITableView? + // MARK: - Properties + + weak var tableView: UITableView? + var paginationStrategy: PaginationStrategy? /// Property which indicating availability of pages public private(set) var canIterate = false { @@ -136,9 +141,10 @@ public class TablePaginatablePlugin: BaseTablePlugin { /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. /// - parameter output: output signals to hide `progressView` from footer - init(progressView: ProgressView, with output: PaginatableOutput) { + init(progressView: ProgressView, with output: PaginatableOutput, direction: PagingDirection = .forward(.bottom)) { self.progressView = progressView self.output = output + self.direction = direction } // MARK: - BaseTablePlugin @@ -146,13 +152,13 @@ public class TablePaginatablePlugin: BaseTablePlugin { public override func setup(with manager: BaseTableManager?) { self.tableView = manager?.view self.canIterate = false - self.output?.onPaginationInitialized(with: self) + self.output?.onPaginationInitialized(with: self, at: direction) self.progressView.setOnRetry { [weak self] in - guard let input = self, let output = self?.output else { + guard let input = self, let output = self?.output, let direction = self?.direction else { return } self?.isErrorWasReceived = false - output.loadNextPage(with: input) + output.loadNextPage(with: input, at: direction) } } @@ -168,7 +174,7 @@ public class TablePaginatablePlugin: BaseTablePlugin { let lastCellIndexPath = IndexPath(row: lastCellInLastSectionIndex, section: lastSectionIndex) if indexPath == lastCellIndexPath && canIterate && !isLoading { - output?.loadNextPage(with: self) + output?.loadNextPage(with: self, at: direction) } default: break @@ -181,18 +187,23 @@ public class TablePaginatablePlugin: BaseTablePlugin { extension TablePaginatablePlugin: PaginatableInput { - public func updateProgress(isLoading: Bool) { - self.isLoading = isLoading - progressView.showProgress(isLoading) - } - - public func updateError(_ error: Error?) { - progressView.showError(error) - isErrorWasReceived = true + public func updatePaginationEnabled(_ canIterate: Bool, at direction: PagingDirection) { + self.canIterate = canIterate + self.direction = direction } - public func updatePagination(canIterate: Bool) { - self.canIterate = canIterate + public func updatePaginationState(_ state: PaginationState, at direction: PagingDirection) { + switch state { + case .idle: + isLoading = false + case .loading: + isLoading = true + case .error(let error): + isLoading = false + isErrorWasReceived = true + progressView.showError(error) + } + progressView.showProgress(isLoading) } } @@ -208,25 +219,27 @@ public extension BaseTablePlugin { /// /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. /// - parameter output: output signals to hide `progressView` from footer + /// - parameter direction: direction of pagination static func paginatable(progressView: TablePaginatablePlugin.ProgressView, - output: PaginatableOutput) -> TablePaginatablePlugin { - return TablePaginatablePlugin(progressView: progressView, with: output) + output: PaginatableOutput, + direction: PagingDirection = .forward(.bottom)) -> TablePaginatablePlugin { + return TablePaginatablePlugin(progressView: progressView, with: output, direction: direction) } - /// Plugin to display `progressView` while previous page is loading - /// - /// Show `progressView` on `willDisplay` first cell. - /// Hide `progressView` when finish loading request - /// - /// - parameter progressView: indicator view to add inside header. Do not forget to init this view with valid frame size. - /// - parameter output: output signals to hide `progressView` from header - /// - Warning: UITableView.style must be plain style for keeping scroll position - static func topPaginatable(progressView: TableTopPaginatablePlugin.ProgressView, - output: TopPaginatableOutput, - isSaveScrollPositionNeeded: Bool) -> TableTopPaginatablePlugin { - return TableTopPaginatablePlugin(progressView: progressView, with: output, isSaveScrollPositionNeeded: isSaveScrollPositionNeeded) - - } +// /// Plugin to display `progressView` while previous page is loading +// /// +// /// Show `progressView` on `willDisplay` first cell. +// /// Hide `progressView` when finish loading request +// /// +// /// - parameter progressView: indicator view to add inside header. Do not forget to init this view with valid frame size. +// /// - parameter output: output signals to hide `progressView` from header +// /// - Warning: UITableView.style must be plain style for keeping scroll position +// static func topPaginatable(progressView: TableTopPaginatablePlugin.ProgressView, +// output: TopPaginatableOutput, +// isSaveScrollPositionNeeded: Bool) -> TableTopPaginatablePlugin { +// return TableTopPaginatablePlugin(progressView: progressView, with: output, isSaveScrollPositionNeeded: isSaveScrollPositionNeeded) +// +// } } From 4391ad9f03b4908027c5a537743332950224b4af Mon Sep 17 00:00:00 2001 From: Konstantin Porokhov Date: Tue, 25 Jul 2023 16:54:19 +0400 Subject: [PATCH 2/3] created RightPagination strategy --- .../PaginatableTableViewController.swift | 3 +- ...ectionPaginatableTableViewController.swift | 20 +++--- .../CollectionPaginatablePlugin.swift | 29 +++++---- .../PaginationState.swift | 0 .../PagingDirection.swift | 0 .../Protocols/ContentOffsetStateKeeper.swift | 0 .../Protocols/PageIndexPathComparator.swift | 0 .../Protocols/PaginationViewManager.swift | 4 +- .../Strategy/BottomPaginationStrategy.swift | 8 ++- .../Strategy/RightPaginationStrategy.swift | 54 ++++++++++++++++ .../Strategy/TopPaginationStrategy.swift | 6 +- .../TablePaginatablePlugin.swift | 62 ++++++++++--------- 12 files changed, 130 insertions(+), 56 deletions(-) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/PaginationState.swift (100%) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/PagingDirection.swift (100%) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/Protocols/ContentOffsetStateKeeper.swift (100%) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/Protocols/PageIndexPathComparator.swift (100%) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/Protocols/PaginationViewManager.swift (60%) rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/Strategy/BottomPaginationStrategy.swift (82%) create mode 100644 Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift rename Source/{Table/Plugins/PluginAction/TablePaginatablePlugin => Protocols/Plugins/CommonPaginatablePlugin}/Strategy/TopPaginationStrategy.swift (88%) rename Source/Table/Plugins/PluginAction/{TablePaginatablePlugin => }/TablePaginatablePlugin.swift (79%) diff --git a/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift b/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift index 7ed232694..a1bf8a10a 100644 --- a/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift +++ b/Example/ReactiveDataDisplayManager/Table/PaginatableTableViewController/PaginatableTableViewController.swift @@ -28,8 +28,7 @@ final class PaginatableTableViewController: UIViewController { private lazy var progressView = PaginatorView(frame: .init(x: 0, y: 0, width: tableView.frame.width, height: 80)) private lazy var adapter = tableView.rddm.baseBuilder - .add(plugin: .paginatable(progressView: progressView, - output: self)) + .add(plugin: .bottomPaginatable(progressView: progressView, output: self)) .build() private weak var paginatableInput: PaginatableInput? diff --git a/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift b/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift index eaed68750..48a6f99d8 100644 --- a/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift +++ b/Example/ReactiveDataDisplayManager/Table/TwoDirectionPaginatableTableViewController/TwoDirectionPaginatableTableViewController.swift @@ -43,8 +43,8 @@ final class TwoDirectionPaginatableTableViewController: UIViewController { height: Constants.paginatorViewHeight)) private lazy var adapter = tableView.rddm.manualBuilder - .add(plugin: .paginatable(progressView: topProgressView, output: self, direction: .backward(.top))) -// .add(plugin: .paginatable(progressView: bottomProgressView, output: self)) + .add(plugin: .topPaginatable(progressView: topProgressView, output: self)) + .add(plugin: .bottomPaginatable(progressView: bottomProgressView, output: self)) .build() private weak var bottomPaginatableInput: PaginatableInput? @@ -187,13 +187,19 @@ extension TwoDirectionPaginatableTableViewController: PaginatableOutput { } func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) { - input.updatePaginationState(.loading, at: direction) - delay(.now() + .seconds(2)) { [weak self, weak input] in - let canFillPages = self?.canFillPages() ?? false + input.updatePaginationState(.loading, at: direction) - if canFillPages { - let canIterate = self?.fillNext() ?? false + delay(.now() + .seconds(3)) { [weak self, weak input] in + let canFillNext = self?.canFillPages() ?? false + if canFillNext { + let canIterate: Bool + switch direction { + case .backward: + canIterate = self?.fillPrev() ?? false + case .forward: + canIterate = self?.fillNext() ?? false + } input?.updatePaginationState(.idle, at: direction) input?.updatePaginationEnabled(canIterate, at: direction) diff --git a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift index f47f14c50..42ec4c462 100644 --- a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift +++ b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift @@ -32,7 +32,7 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin // MARK: - Properties weak var collectionView: UICollectionView? - var paginationStrategy: PaginationStrategy? + var strategy: PaginationStrategy? /// Property which indicating availability of pages public private(set) var canIterate = false { @@ -41,12 +41,12 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin guard progressView.superview == nil else { return } - paginationStrategy?.addPafinationView() + strategy?.addPafinationView() } else { guard progressView.superview != nil else { return } - paginationStrategy?.removePafinationView() + strategy?.removePafinationView() } } } @@ -65,8 +65,8 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin public override func setup(with manager: BaseCollectionManager?) { collectionView = manager?.view - paginationStrategy?.scrollView = manager?.view - paginationStrategy?.progressView = progressView + strategy?.scrollView = manager?.view + strategy?.progressView = progressView canIterate = false output?.onPaginationInitialized(with: self, at: direction) self.progressView.setOnRetry { [weak self] in @@ -83,9 +83,9 @@ public class CollectionPaginatablePlugin: BaseCollectionPlugin switch event { case .willDisplayCell(let indexPath): if progressView.frame.minY != collectionView?.contentSize.height { - paginationStrategy?.setProgressViewFinalFrame() + strategy?.setProgressViewFinalFrame() } - guard indexPath == paginationStrategy?.getIndexPath(with: manager), canIterate, !isLoading, !isErrorWasReceived else { + guard indexPath == strategy?.getIndexPath(with: manager?.sections), canIterate, !isLoading, !isErrorWasReceived else { return } output?.loadNextPage(with: self, at: self.direction) @@ -104,7 +104,7 @@ extension CollectionPaginatablePlugin: PaginatableInput { self.canIterate = canIterate self.direction = direction - paginationStrategy?.resetOffset(canIterate: canIterate) + strategy?.resetOffset(canIterate: canIterate) } public func updatePaginationState(_ state: PaginationState, at direction: PagingDirection) { @@ -113,7 +113,7 @@ extension CollectionPaginatablePlugin: PaginatableInput { isLoading = false case .loading: isLoading = true - paginationStrategy?.saveCurrentState() + strategy?.saveCurrentState() case .error(let error): isLoading = false isErrorWasReceived = true @@ -138,7 +138,7 @@ public extension BaseCollectionPlugin { static func topPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, output: PaginatableOutput) -> CollectionPaginatablePlugin { let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .backward(.top)) - plugin.paginationStrategy = TopPaginationStrategy() + plugin.strategy = TopPaginationStrategy() return plugin } @@ -152,7 +152,14 @@ public extension BaseCollectionPlugin { static func bottomPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, output: PaginatableOutput) -> CollectionPaginatablePlugin { let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .forward(.bottom)) - plugin.paginationStrategy = BottomPaginationStrategy() + plugin.strategy = BottomPaginationStrategy() + return plugin + } + + static func rightPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, + output: PaginatableOutput) -> CollectionPaginatablePlugin { + let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .forward(.bottom)) + plugin.strategy = RightPaginationStrategy() return plugin } diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/PaginationState.swift similarity index 100% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PaginationState.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/PaginationState.swift diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/PagingDirection.swift similarity index 100% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/PagingDirection.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/PagingDirection.swift diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift similarity index 100% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PageIndexPathComparator.swift similarity index 100% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PageIndexPathComparator.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PageIndexPathComparator.swift diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift similarity index 60% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift index 6de80692e..ccc2d89de 100644 --- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Protocols/PaginationViewManager.swift +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift @@ -9,7 +9,9 @@ import UIKit protocol PaginationStrategy: ContentOffsetStateKeeper { - func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? + func getIndexPath( + with sections: [Section]? + ) -> IndexPath? func addPafinationView() func removePafinationView() func setProgressViewFinalFrame() diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift similarity index 82% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift index 57ee499db..960728163 100644 --- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/BottomPaginationStrategy.swift +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift @@ -29,8 +29,10 @@ final class BottomPaginationStrategy: PaginationStrategy { scrollView?.contentInset.bottom -= progressView?.frame.height ?? .zero } - func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? { - guard let sections = manager?.sections else { + func getIndexPath( + with sections: [Section]? + ) -> IndexPath? { + guard let sections = sections else { return nil } let lastSectionIndex = sections.count - 1 @@ -43,7 +45,7 @@ final class BottomPaginationStrategy: PaginationStrategy { guard let progressViewFrame = progressView?.frame else { return } - // Hack: Update progressView position. Imitation of global footer view like `tableFooterView` + // Hack: Update progressView position. progressView?.frame = .init(origin: .init(x: progressViewFrame.origin.x, y: scrollView?.contentSize.height ?? 0), size: progressViewFrame.size) } diff --git a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift new file mode 100644 index 000000000..55e172aa4 --- /dev/null +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift @@ -0,0 +1,54 @@ +// +// RightPaginationStrategy.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 25.07.2023. +// + +import UIKit + +final class RightPaginationStrategy: PaginationStrategy { + + // MARK: - Properties + + weak var scrollView: UIScrollView? + weak var progressView: UIView? + + // MARK: - PaginationStrategy + + func addPafinationView() { + guard let progressView = progressView else { + return + } + scrollView?.addSubview(progressView) + scrollView?.contentInset.right += progressView.frame.width + } + + func removePafinationView() { + progressView?.removeFromSuperview() + scrollView?.contentInset.right -= progressView?.frame.width ?? .zero + } + + func getIndexPath( + with sections: [Section]? + ) -> IndexPath? { + guard let sections = sections else { + return nil + } + let lastSectionIndex = sections.count - 1 + let lastCellInLastSectionIndex = sections[lastSectionIndex].generators.count - 1 + + return IndexPath(row: lastCellInLastSectionIndex, section: lastSectionIndex) + } + + func setProgressViewFinalFrame() { + guard let progressViewFrame = progressView?.frame, let scrollViewHeight = scrollView?.bounds.height else { + return + } + // Hack: Update progressView position. + progressView?.frame = .init(origin: .init(x: scrollView?.contentSize.width ?? 0, + y: (scrollViewHeight / 2) - progressViewFrame.height), + size: progressViewFrame.size) + } + +} diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift similarity index 88% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift rename to Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift index 9e4cd77f3..d6006f2e4 100644 --- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/Strategy/TopPaginationStrategy.swift +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift @@ -50,7 +50,9 @@ final class TopPaginationStrategy: PaginationStrategy { scrollView?.contentInset.top -= progressView?.frame.height ?? .zero } - func getIndexPath(with manager: BaseCollectionManager?) -> IndexPath? { + func getIndexPath( + with sections: [Section]? + ) -> IndexPath? { IndexPath(row: 0, section: 0) } @@ -58,7 +60,7 @@ final class TopPaginationStrategy: PaginationStrategy { guard let progressViewFrame = progressView?.frame else { return } - // Hack: Update progressView position. Imitation of global footer view like `tableFooterView` + // Hack: Update progressView position. progressView?.frame = .init(origin: .init(x: progressViewFrame.origin.x, y: -progressViewFrame.height), size: progressViewFrame.size) } diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift similarity index 79% rename from Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift rename to Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift index 498c6d45e..6e8582111 100644 --- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin/TablePaginatablePlugin.swift +++ b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift @@ -124,15 +124,21 @@ public class TablePaginatablePlugin: BaseTablePlugin { // MARK: - Properties weak var tableView: UITableView? - var paginationStrategy: PaginationStrategy? + var strategy: PaginationStrategy? /// Property which indicating availability of pages public private(set) var canIterate = false { didSet { if canIterate { - tableView?.tableFooterView = progressView + guard progressView.superview == nil else { + return + } + strategy?.addPafinationView() } else { - tableView?.tableFooterView = nil + guard progressView.superview != nil else { + return + } + strategy?.removePafinationView() } } } @@ -151,6 +157,8 @@ public class TablePaginatablePlugin: BaseTablePlugin { public override func setup(with manager: BaseTableManager?) { self.tableView = manager?.view + self.strategy?.scrollView = manager?.view + self.strategy?.progressView = progressView self.canIterate = false self.output?.onPaginationInitialized(with: self, at: direction) self.progressView.setOnRetry { [weak self] in @@ -166,16 +174,13 @@ public class TablePaginatablePlugin: BaseTablePlugin { switch event { case .willDisplayCell(let indexPath): - guard let sections = manager?.sections, !isErrorWasReceived else { - return + if progressView.frame.minY != tableView?.contentSize.height { + strategy?.setProgressViewFinalFrame() } - let lastSectionIndex = sections.count - 1 - let lastCellInLastSectionIndex = sections[lastSectionIndex].generators.count - 1 - - let lastCellIndexPath = IndexPath(row: lastCellInLastSectionIndex, section: lastSectionIndex) - if indexPath == lastCellIndexPath && canIterate && !isLoading { - output?.loadNextPage(with: self, at: direction) + guard indexPath == strategy?.getIndexPath(with: manager?.sections), canIterate, !isLoading, !isErrorWasReceived else { + return } + output?.loadNextPage(with: self, at: direction) default: break } @@ -190,6 +195,8 @@ extension TablePaginatablePlugin: PaginatableInput { public func updatePaginationEnabled(_ canIterate: Bool, at direction: PagingDirection) { self.canIterate = canIterate self.direction = direction + + strategy?.resetOffset(canIterate: canIterate) } public func updatePaginationState(_ state: PaginationState, at direction: PagingDirection) { @@ -198,6 +205,7 @@ extension TablePaginatablePlugin: PaginatableInput { isLoading = false case .loading: isLoading = true + strategy?.saveCurrentState() case .error(let error): isLoading = false isErrorWasReceived = true @@ -220,26 +228,20 @@ public extension BaseTablePlugin { /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. /// - parameter output: output signals to hide `progressView` from footer /// - parameter direction: direction of pagination - static func paginatable(progressView: TablePaginatablePlugin.ProgressView, - output: PaginatableOutput, - direction: PagingDirection = .forward(.bottom)) -> TablePaginatablePlugin { - return TablePaginatablePlugin(progressView: progressView, with: output, direction: direction) - + static func topPaginatable(progressView: TablePaginatablePlugin.ProgressView, + output: PaginatableOutput, + direction: PagingDirection = .backward(.top)) -> TablePaginatablePlugin { + let plugin = TablePaginatablePlugin(progressView: progressView, with: output, direction: direction) + plugin.strategy = TopPaginationStrategy() + return plugin } -// /// Plugin to display `progressView` while previous page is loading -// /// -// /// Show `progressView` on `willDisplay` first cell. -// /// Hide `progressView` when finish loading request -// /// -// /// - parameter progressView: indicator view to add inside header. Do not forget to init this view with valid frame size. -// /// - parameter output: output signals to hide `progressView` from header -// /// - Warning: UITableView.style must be plain style for keeping scroll position -// static func topPaginatable(progressView: TableTopPaginatablePlugin.ProgressView, -// output: TopPaginatableOutput, -// isSaveScrollPositionNeeded: Bool) -> TableTopPaginatablePlugin { -// return TableTopPaginatablePlugin(progressView: progressView, with: output, isSaveScrollPositionNeeded: isSaveScrollPositionNeeded) -// -// } + static func bottomPaginatable(progressView: TablePaginatablePlugin.ProgressView, + output: PaginatableOutput, + direction: PagingDirection = .forward(.bottom)) -> TablePaginatablePlugin { + let plugin = TablePaginatablePlugin(progressView: progressView, with: output, direction: direction) + plugin.strategy = BottomPaginationStrategy() + return plugin + } } From ef1ffebf9542b3fe5a17ae71f498ca94ab404d85 Mon Sep 17 00:00:00 2001 From: Konstantin Porokhov Date: Wed, 30 Aug 2023 13:44:57 +0400 Subject: [PATCH 3/3] created LeftPaginationStrategy and example --- .../Collection.storyboard | 45 ++++ .../MainCollectionViewController.swift | 2 + ...nPaginatableCollectionViewController.swift | 229 ++++++++++++++++++ .../CollectionPaginatablePlugin.swift | 23 +- .../Strategy/LeftPaginationStrategy.swift | 68 ++++++ .../Strategy/RightPaginationStrategy.swift | 5 +- 6 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/HorizontalTwoDirectionPaginatableCollectionViewController.swift create mode 100644 Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/LeftPaginationStrategy.swift diff --git a/Example/ReactiveDataDisplayManager/Collection.storyboard b/Example/ReactiveDataDisplayManager/Collection.storyboard index b30d2f5a2..5c845327c 100644 --- a/Example/ReactiveDataDisplayManager/Collection.storyboard +++ b/Example/ReactiveDataDisplayManager/Collection.storyboard @@ -55,6 +55,7 @@ + @@ -882,6 +883,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift index 09a50da57..14457279f 100644 --- a/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift @@ -35,6 +35,7 @@ final class MainCollectionViewController: UIViewController { case alignedCollection case dynamicHeightViewController case twoDirectionPaginatableCollection + case horizontalTwoDirectionPaginatableCollection } // MARK: - Constants @@ -53,6 +54,7 @@ final class MainCollectionViewController: UIViewController { ("Collection with diffableDataSource", .diffableCollection), ("Collection with pagination", .paginatableCollection), ("Collection with two direction pagination", .twoDirectionPaginatableCollection), + ("Collection with two direction horizontal pagination", .horizontalTwoDirectionPaginatableCollection), ("Collection with compositional layout", .compositionalCollection), ("Collection with DifferenceKit", .differenceCollection), ("List Appearances with swipeable items", .swipeableListAppearances), diff --git a/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/HorizontalTwoDirectionPaginatableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/HorizontalTwoDirectionPaginatableCollectionViewController.swift new file mode 100644 index 000000000..13b37c50b --- /dev/null +++ b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/HorizontalTwoDirectionPaginatableCollectionViewController.swift @@ -0,0 +1,229 @@ +// +// HorizontalTwoDirectionPaginatableCollectionViewController.swift +// ReactiveDataDisplayManagerExample_iOS +// +// Created by Konstantin Porokhov on 30.08.2023. +// + +import UIKit +import ReactiveDataDisplayManager +import ReactiveDataComponents + +final class HorizontalTwoDirectionPaginatableCollectionViewController: UIViewController { + + // MARK: - Nested types + + private enum ScrollDirection { + case left + case right + } + + // MARK: - Constants + + private enum Constants { + static let maxPagesCount = 5 + static let pageSize = 20 + static let paginatorHeight: CGFloat = 80 + static let firstPageMiddleIndexPath = IndexPath(row: Constants.pageSize / 2, section: 0) + } + + // MARK: - IBOutlet + + @IBOutlet private weak var collectionView: UICollectionView! + @IBOutlet private weak var activityIndicator: UIActivityIndicatorView! + + // MARK: - Private Properties + + private lazy var leftProgressView = PaginatorView(frame: .init(x: 0, + y: 0, + width: 200, + height: collectionView.frame.height)) + private lazy var rightProgressView = PaginatorView(frame: .init(x: 0, + y: 0, + width: 200, + height: collectionView.frame.height)) + + private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .leftPaginatable(progressView: leftProgressView, output: self)) + .add(plugin: .rightPaginatable(progressView: rightProgressView, output: self)) + .build() + + private weak var bottomPaginatableInput: PaginatableInput? + private weak var topPaginatableInput: PaginatableInput? + + private var isFirstPageLoading = true + private var currentLeftPage = 0 + private var currentRightPage = 0 + + private lazy var emptyCell = CollectionSpacerCell.rddm.baseGenerator(with: CollectionSpacerCell.Model(height: 0), and: .class) + + // MARK: - UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + title = "Collection with two direction pagination" + + configureActivityIndicatorIfNeeded() + configureCollectionView() + loadFirstPage() + } + +} + +// MARK: - Configuration + +private extension HorizontalTwoDirectionPaginatableCollectionViewController { + + func configureActivityIndicatorIfNeeded() { + if #available(iOS 13.0, tvOS 13.0, *) { + activityIndicator.style = .medium + } + } + +} + +// MARK: - Private Methods + +private extension HorizontalTwoDirectionPaginatableCollectionViewController { + + func configureCollectionView() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = .init(width: 300, height: 200) + collectionView.setCollectionViewLayout(layout, animated: true) + } + + func loadFirstPage() { + + // show loader + activityIndicator.isHidden = false + activityIndicator.hidesWhenStopped = true + activityIndicator.startAnimating() + + // hide footer + bottomPaginatableInput?.updatePaginationEnabled(false, at: .forward(.bottom)) + topPaginatableInput?.updatePaginationEnabled(false, at: .backward(.top)) + bottomPaginatableInput?.updatePaginationState(.idle, at: .forward(.bottom)) + topPaginatableInput?.updatePaginationState(.loading, at: .backward(.top)) + + // imitation of loading first page + delay(.now() + .seconds(3)) { [weak self] in + // fill table + self?.fillAdapter() + + // hide loader + self?.activityIndicator?.stopAnimating() + + // scroll to middle + self?.collectionView.scrollToItem(at: Constants.firstPageMiddleIndexPath, at: .centeredVertically, animated: false) + + // show pagination loader if update is needed + self?.bottomPaginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom)) + self?.topPaginatableInput?.updatePaginationEnabled(true, at: .backward(.top)) + } + } + + /// This method is used to fill adapter + func fillAdapter() { + adapter += emptyCell + + for _ in 0...Constants.pageSize { + adapter += makeGenerator() + } + + adapter => .reload + } + + private func makeGenerator(for scrollDirection: ScrollDirection? = nil) -> CollectionCellGenerator { + var currentPage = 0 + if let scrollDirection = scrollDirection { + switch scrollDirection { + case .left: + currentPage = currentLeftPage + case .right: + currentPage = currentRightPage + } + } + + let title = "Random cell \(Int.random(in: 0...1000)) from page \(currentPage)" + return TitleCollectionViewCell.rddm.baseGenerator(with: title) + } + + func canFillPages() -> Bool { + if isFirstPageLoading { + isFirstPageLoading.toggle() + return false + } else { + return true + } + } + + func fillNext() -> Bool { + currentRightPage += 1 + + var newGenerators = [CollectionCellGenerator]() + + for _ in 0...Constants.pageSize { + newGenerators.append(makeGenerator(for: .right)) + } + + if let lastGenerator = adapter.sections.last?.generators.last { + adapter.insert(after: lastGenerator, new: newGenerators) + } else { + adapter += newGenerators + adapter => .reload + } + + return currentRightPage != Constants.maxPagesCount + } + + func fillPrev() -> Bool { + currentLeftPage -= 1 + + let newGenerators = (0...Constants.pageSize).map { _ in + return makeGenerator(for: .left) + } + adapter.insert(after: emptyCell, new: newGenerators, with: nil) + + return abs(currentLeftPage) != Constants.maxPagesCount + } + +} + +// MARK: - PaginatableOutput + +extension HorizontalTwoDirectionPaginatableCollectionViewController: PaginatableOutput { + + func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) { + switch direction { + case .backward: + topPaginatableInput = input + case .forward: + bottomPaginatableInput = input + } + } + + func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) { + + input.updatePaginationState(.loading, at: direction) + + delay(.now() + .seconds(3)) { [weak self, weak input] in + let canFillNext = self?.canFillPages() ?? false + if canFillNext { + let canIterate: Bool + switch direction { + case .backward: + canIterate = self?.fillPrev() ?? false + case .forward: + canIterate = self?.fillNext() ?? false + } + + input?.updatePaginationState(.idle, at: direction) + input?.updatePaginationEnabled(canIterate, at: direction) + } else { + input?.updatePaginationState(.error(SampleError.sample), at: direction) + } + } + } + +} diff --git a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift index 42ec4c462..0ef6166f6 100644 --- a/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift +++ b/Source/Collection/Plugins/PluginAction/CollectionPaginatablePlugin.swift @@ -156,11 +156,32 @@ public extension BaseCollectionPlugin { return plugin } + /// Plugin to display `progressView` while next page is loading + /// + /// Show `progressView` on `willDisplay` last cell. + /// Hide `progressView` when finish loading request + /// + /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. + /// - parameter output: output signals to hide `progressView` from footer static func rightPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, - output: PaginatableOutput) -> CollectionPaginatablePlugin { + output: PaginatableOutput) -> CollectionPaginatablePlugin { let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .forward(.bottom)) plugin.strategy = RightPaginationStrategy() return plugin } + /// Plugin to display `progressView` while next page is loading + /// + /// Show `progressView` on `willDisplay` last cell. + /// Hide `progressView` when finish loading request + /// + /// - parameter progressView: indicator view to add inside footer. Do not forget to init this view with valid frame size. + /// - parameter output: output signals to hide `progressView` from footer + static func leftPaginatable(progressView: CollectionPaginatablePlugin.ProgressView, + output: PaginatableOutput) -> CollectionPaginatablePlugin { + let plugin = CollectionPaginatablePlugin(progressView: progressView, with: output, direction: .backward(.top)) + plugin.strategy = LeftPaginationStrategy() + return plugin + } + } diff --git a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/LeftPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/LeftPaginationStrategy.swift new file mode 100644 index 000000000..e793d1077 --- /dev/null +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/LeftPaginationStrategy.swift @@ -0,0 +1,68 @@ +// +// LeftPaginationStrategy.swift +// ReactiveDataDisplayManager +// +// Created by Konstantin Porokhov on 30.08.2023. +// + +import UIKit + +final class LeftPaginationStrategy: PaginationStrategy { + + // MARK: - Properties + + weak var scrollView: UIScrollView? + weak var progressView: UIView? + + // MARK: - Private properties + + private var currentContentWidth: CGFloat? + + // MARK: - PaginationStrategy + + func saveCurrentState() { + currentContentWidth = scrollView?.contentSize.width + } + + func resetOffset(canIterate: Bool) { + guard + canIterate, + let currentContentWidth = currentContentWidth, + let newContentWidth = scrollView?.contentSize.width, + let progressViewWidth = progressView?.frame.width + else { return } + + let finalOffset = CGPoint(x: newContentWidth - currentContentWidth - progressViewWidth, y: 0) + scrollView?.setContentOffset(finalOffset, animated: false) + self.currentContentWidth = nil + } + + func addPafinationView() { + guard let progressView = progressView else { + return + } + scrollView?.addSubview(progressView) + scrollView?.contentInset.left += progressView.frame.width + } + + func removePafinationView() { + progressView?.removeFromSuperview() + scrollView?.contentInset.left -= progressView?.frame.width ?? .zero + } + + func getIndexPath( + with sections: [Section]? + ) -> IndexPath? { + IndexPath(row: 0, section: 0) + } + + func setProgressViewFinalFrame() { + guard let progressViewFrame = progressView?.frame else { + return + } + // Hack: Update progressView position. + progressView?.frame = .init(origin: .init(x: -progressViewFrame.width, y: .zero), + size: progressViewFrame.size) + } + +} diff --git a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift index 55e172aa4..4131cd482 100644 --- a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift +++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift @@ -42,12 +42,11 @@ final class RightPaginationStrategy: PaginationStrategy { } func setProgressViewFinalFrame() { - guard let progressViewFrame = progressView?.frame, let scrollViewHeight = scrollView?.bounds.height else { + guard let progressViewFrame = progressView?.frame else { return } // Hack: Update progressView position. - progressView?.frame = .init(origin: .init(x: scrollView?.contentSize.width ?? 0, - y: (scrollViewHeight / 2) - progressViewFrame.height), + progressView?.frame = .init(origin: .init(x: scrollView?.contentSize.width ?? 0, y: .zero), size: progressViewFrame.size) }