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/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/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/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..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?
@@ -72,7 +71,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 +84,7 @@ private extension PaginatableTableViewController {
self?.activityIndicator?.isHidden = true
// show footer
- self?.paginatableInput?.updatePagination(canIterate: true)
+ self?.paginatableInput?.updatePaginationEnabled(true, at: .forward(.bottom))
}
}
@@ -130,24 +129,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..48a6f99d8 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: .topPaginatable(progressView: topProgressView, output: self))
+ .add(plugin: .bottomPaginatable(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,52 +177,34 @@ private extension TwoDirectionPaginatableTableViewController {
extension TwoDirectionPaginatableTableViewController: PaginatableOutput {
- func onPaginationInitialized(with input: PaginatableInput) {
- bottomPaginatableInput = input
- }
-
- func loadNextPage(with input: PaginatableInput) {
- input.updateProgress(isLoading: true)
-
- delay(.now() + .seconds(2)) { [weak self, weak input] in
- let canFillPages = self?.canFillPages() ?? false
-
- if canFillPages {
- let canIterate = self?.fillNext() ?? false
-
- input?.updateProgress(isLoading: false)
- input?.updatePagination(canIterate: canIterate)
- } else {
- input?.updateProgress(isLoading: false)
- input?.updateError(SampleError.sample)
- }
+ func onPaginationInitialized(with input: PaginatableInput, at direction: PagingDirection) {
+ switch direction {
+ case .backward:
+ topPaginatableInput = input
+ case .forward:
+ bottomPaginatableInput = input
}
}
-}
-
-// MARK: - TopPaginatableOutput
+ func loadNextPage(with input: PaginatableInput, at direction: PagingDirection) {
-extension TwoDirectionPaginatableTableViewController: TopPaginatableOutput {
+ input.updatePaginationState(.loading, at: direction)
- func onTopPaginationInitialized(with input: TopPaginatableInput) {
- topPaginatableInput = input
- }
-
- func loadPrevPage(with input: TopPaginatableInput) {
- input.updateProgress(isLoading: true)
+ 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
+ }
- 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..0ef6166f6 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 strategy: 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
+ strategy?.addPafinationView()
} else {
guard progressView.superview != nil else {
return
}
-
- progressView.removeFromSuperview()
- collectionView?.contentInset.bottom -= progressView.frame.height
+ strategy?.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
+ strategy?.scrollView = manager?.view
+ strategy?.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()
- }
- guard let sections = manager?.sections, !isErrorWasReceived else {
- return
+ strategy?.setProgressViewFinalFrame()
}
- 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 == strategy?.getIndexPath(with: manager?.sections), 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
+ strategy?.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
+ strategy?.saveCurrentState()
+ case .error(let error):
+ isLoading = false
+ isErrorWasReceived = true
+ progressView.showError(error)
+ }
+ progressView.showProgress(isLoading)
}
}
@@ -141,23 +135,53 @@ 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.strategy = 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.strategy = BottomPaginationStrategy()
+ 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 {
+ 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/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/Protocols/Plugins/CommonPaginatablePlugin/PaginationState.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/PaginationState.swift
new file mode 100644
index 000000000..fe46b83d4
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/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/Protocols/Plugins/CommonPaginatablePlugin/PagingDirection.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/PagingDirection.swift
new file mode 100644
index 000000000..7115a158f
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/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/Protocols/Plugins/CommonPaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/ContentOffsetStateKeeper.swift
new file mode 100644
index 000000000..04a3a1572
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/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/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PageIndexPathComparator.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PageIndexPathComparator.swift
new file mode 100644
index 000000000..041287f86
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/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/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift
new file mode 100644
index 000000000..ccc2d89de
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Protocols/PaginationViewManager.swift
@@ -0,0 +1,18 @@
+//
+// PaginationViewManager.swift
+// ReactiveDataDisplayManager
+//
+// Created by Konstantin Porokhov on 25.07.2023.
+//
+
+import UIKit
+
+protocol PaginationStrategy: ContentOffsetStateKeeper {
+
+ func getIndexPath(
+ with sections: [Section]?
+ ) -> IndexPath?
+ func addPafinationView()
+ func removePafinationView()
+ func setProgressViewFinalFrame()
+}
diff --git a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift
new file mode 100644
index 000000000..960728163
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/BottomPaginationStrategy.swift
@@ -0,0 +1,53 @@
+//
+// 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 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 else {
+ return
+ }
+ // 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/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
new file mode 100644
index 000000000..4131cd482
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/RightPaginationStrategy.swift
@@ -0,0 +1,53 @@
+//
+// 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 else {
+ return
+ }
+ // Hack: Update progressView position.
+ progressView?.frame = .init(origin: .init(x: scrollView?.contentSize.width ?? 0, y: .zero),
+ size: progressViewFrame.size)
+ }
+
+}
diff --git a/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift
new file mode 100644
index 000000000..d6006f2e4
--- /dev/null
+++ b/Source/Protocols/Plugins/CommonPaginatablePlugin/Strategy/TopPaginationStrategy.swift
@@ -0,0 +1,68 @@
+//
+// 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 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.origin.x, y: -progressViewFrame.height),
+ size: progressViewFrame.size)
+ }
+
+}
diff --git a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift b/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift
index 99050c022..6e8582111 100644
--- a/Source/Table/Plugins/PluginAction/TablePaginatablePlugin.swift
+++ b/Source/Table/Plugins/PluginAction/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,16 +119,26 @@ 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 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()
}
}
}
@@ -136,23 +147,26 @@ 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
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)
+ 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)
}
}
@@ -160,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)
+ guard indexPath == strategy?.getIndexPath(with: manager?.sections), canIterate, !isLoading, !isErrorWasReceived else {
+ return
}
+ output?.loadNextPage(with: self, at: direction)
default:
break
}
@@ -181,18 +192,26 @@ public class TablePaginatablePlugin: BaseTablePlugin {
extension TablePaginatablePlugin: 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
+ strategy?.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
+ strategy?.saveCurrentState()
+ case .error(let error):
+ isLoading = false
+ isErrorWasReceived = true
+ progressView.showError(error)
+ }
+ progressView.showProgress(isLoading)
}
}
@@ -208,25 +227,21 @@ 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
- static func paginatable(progressView: TablePaginatablePlugin.ProgressView,
- output: PaginatableOutput) -> TablePaginatablePlugin {
- return TablePaginatablePlugin(progressView: progressView, with: output)
-
+ /// - parameter direction: direction of pagination
+ 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
}
}