From c3ca6c706e89b765b8728eb611acafd1e5530060 Mon Sep 17 00:00:00 2001 From: Rudd Fawcett Date: Wed, 2 Oct 2024 19:25:14 -0400 Subject: [PATCH] Exposes `UIScrollViewDelegate` to clients (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Have you read the [Contributing Guidelines](https://github.com/jessesquires/.github/blob/master/CONTRIBUTING.md)?* 🖤 Closes #131 ## Describe your changes - Allows clients to set a `weak var scrollViewDelegate: UIScrollViewDelegate?` on `CollectionViewDriver` - Implement the `UIScrollViewDelegate` methods in `CollectionViewDriver` and forwards them to this delegate - Keeps the method signatures in the same order and groupings as `UIScrollViewDelegate` ### Example - Adds example to `ListViewController`, demonstrating an override of `scrollViewShouldScrollToTop(scrollView:)` | Before | After | | ------ | ----- | | ![rf_131_before](https://github.com/user-attachments/assets/7d0dfd8b-1be2-4eef-8899-65503b783ed0) | ![rf_131_after](https://github.com/user-attachments/assets/3ec172c8-ad56-47ce-8e23-89cf0828ab66) | ## Todo - Add unit tests. --------- Co-authored-by: Jesse Squires --- CHANGELOG.md | 1 + Example/Sources/List/ListViewController.swift | 27 ++++-- Sources/CollectionViewDriver.swift | 96 +++++++++++++++++++ 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a4da9c..099eb0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ NEXT - Reverted back to Swift 5 language mode because of issues in UIKit. ([@jessesquires](https://github.com/jessesquires), [#116](https://github.com/jessesquires/ReactiveCollectionsKit/pull/116)) - Applying a snapshot using `reloadData` now always occurs on the main thread. ([@jessesquires](https://github.com/jessesquires), [#116](https://github.com/jessesquires/ReactiveCollectionsKit/pull/116)) - Implemented additional selection APIs for `CellViewModel`: `shouldSelect`, `shouldDeselect`, `didDeselect()`. ([@nuomi1](https://github.com/nuomi1), [#127](https://github.com/jessesquires/ReactiveCollectionsKit/pull/127)) +- Allow setting a `UIScrollViewDelegate` object to receive scroll view events from the collection view. ([@ruddfawcett](https://github.com/ruddfawcett), [#131](https://github.com/jessesquires/ReactiveCollectionsKit/pull/131), [#133](https://github.com/jessesquires/ReactiveCollectionsKit/pull/133)) 0.1.6 ----- diff --git a/Example/Sources/List/ListViewController.swift b/Example/Sources/List/ListViewController.swift index a0fbc92..76e1acc 100644 --- a/Example/Sources/List/ListViewController.swift +++ b/Example/Sources/List/ListViewController.swift @@ -17,12 +17,19 @@ import UIKit final class ListViewController: ExampleViewController, CellEventCoordinator { - lazy var driver = CollectionViewDriver( - view: self.collectionView, - options: .init(diffOnBackgroundQueue: true), - emptyViewProvider: sharedEmptyViewProvider, - cellEventCoordinator: self - ) + lazy var driver: CollectionViewDriver = { + let driver = CollectionViewDriver( + view: self.collectionView, + options: .init(diffOnBackgroundQueue: true), + emptyViewProvider: sharedEmptyViewProvider, + cellEventCoordinator: self + ) + + // Access `UIScrollViewDelegate` and handle protocol + driver.scrollViewDelegate = self + + return driver + }() override var model: Model { didSet { @@ -131,3 +138,11 @@ final class ListViewController: ExampleViewController, CellEventCoordinator { return CollectionViewModel(id: "list_view", sections: [peopleSection, colorSection]) } } + +extension ListViewController: UIScrollViewDelegate { + + // Demonstrate delegate override; tapping status bar does not scroll to top + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + false + } +} diff --git a/Sources/CollectionViewDriver.swift b/Sources/CollectionViewDriver.swift index 5d57ece..a27878c 100644 --- a/Sources/CollectionViewDriver.swift +++ b/Sources/CollectionViewDriver.swift @@ -31,6 +31,9 @@ public final class CollectionViewDriver: NSObject { /// The collection view model. @Published public private(set) var viewModel: CollectionViewModel + + /// The scroll view delegate to forward. + public weak var scrollViewDelegate: UIScrollViewDelegate? private let _emptyViewProvider: EmptyViewProvider? @@ -373,3 +376,96 @@ extension CollectionViewDriver: UICollectionViewDelegate { self.viewModel._safeSupplementaryViewModel(ofKind: elementKind, at: indexPath)?.didEndDisplaying() } } + +// MARK: UIScrollViewDelegate + +extension CollectionViewDriver: UIScrollViewDelegate { + // MARK: Managing offset and zoom scale changes + + /// :nodoc: + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidScroll?(scrollView) + } + + /// :nodoc: + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidZoom?(scrollView) + } + + // MARK: Tracking dragging + + /// :nodoc: + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) + } + + /// :nodoc: + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, + withVelocity velocity: CGPoint, + targetContentOffset: UnsafeMutablePointer) { + self.scrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) + } + + /// :nodoc: + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, + willDecelerate decelerate: Bool) { + self.scrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) + } + + // MARK: Tracking deceleration and scrolling animation + + /// :nodoc: + public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) + } + + /// :nodoc: + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView) + } + + /// :nodoc: + public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) + } + + // MARK: Managing and tracking zooming more granularly + + /// :nodoc: + public func viewForZooming(in scrollView: UIScrollView) -> UIView? { + self.scrollViewDelegate?.viewForZooming?(in: scrollView) + } + + /// :nodoc: + public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, + with view: UIView?) { + self.scrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view) + } + + /// :nodoc: + public func scrollViewDidEndZooming(_ scrollView: UIScrollView, + with view: UIView?, + atScale scale: CGFloat) { + self.scrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) + } + + // MARK: Managing if should scroll to top and tracking if done so + + /// :nodoc: + public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + self.scrollViewDelegate?.scrollViewShouldScrollToTop?(scrollView) ?? true + } + + /// :nodoc: + public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidScrollToTop?(scrollView) + } + + + // MARK: Tracking adjusted content insets on scroll view + + /// :nodoc: + public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { + self.scrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView) + } +}