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 } }