Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create datasource for cells to make lazy-loading possible for collections #216

Merged
merged 3 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The changelog for `ReactiveLists`. Also see the [releases](https://github.com/pl

NEXT
----
0.8.1.8
-----
- Added support for CollectionView cells lazy-loading.

0.8.1
-----
- Added support for deselect and willDispaly cells.
Expand Down
2 changes: 1 addition & 1 deletion ReactiveLists.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "ReactiveLists"
s.version = "0.8.1.beta.7"
s.version = "0.8.1.beta.8"

s.summary = "React-like API for UITableView and UICollectionView"
s.homepage = "https://github.com/plangrid/ReactiveLists"
Expand Down
19 changes: 15 additions & 4 deletions ReactiveLists.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
357B96DC2019374E0000443F /* CollectionToolCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 357B96D9201934C50000443F /* CollectionToolCell.xib */; };
357B96E9201956760000443F /* CollectionViewHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 357B96E8201956760000443F /* CollectionViewHeaderView.xib */; };
357B96EA2019599C0000443F /* CollectionViewHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 357B96E8201956760000443F /* CollectionViewHeaderView.xib */; };
39A211882A5342EE00288547 /* CollectionCellViewModelDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A211872A5342EE00288547 /* CollectionCellViewModelDataSource.swift */; };
39D64C592A54E4DB0071A7F3 /* CollectionViewLazyDiffingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D64C582A54E4DB0071A7F3 /* CollectionViewLazyDiffingTests.swift */; };
7C24B1408A8B3A147C254BCA /* Pods_ReactiveLists.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 978763204EC113AFD1F7EB54 /* Pods_ReactiveLists.framework */; };
A8069332250046AD0036CA11 /* TableViewLazyDiffingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8069330250042700036CA11 /* TableViewLazyDiffingTest.swift */; };
A8D93CAA24FD9BDF00459EBB /* TableCellViewModelDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D93CA924FD9BDF00459EBB /* TableCellViewModelDataSource.swift */; };
Expand Down Expand Up @@ -132,6 +134,8 @@
351B0BB420168D2E0034569D /* CollectionViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewCells.swift; sourceTree = "<group>"; };
357B96D9201934C50000443F /* CollectionToolCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionToolCell.xib; sourceTree = "<group>"; };
357B96E8201956760000443F /* CollectionViewHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionViewHeaderView.xib; sourceTree = "<group>"; };
39A211872A5342EE00288547 /* CollectionCellViewModelDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCellViewModelDataSource.swift; sourceTree = "<group>"; };
39D64C582A54E4DB0071A7F3 /* CollectionViewLazyDiffingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewLazyDiffingTests.swift; sourceTree = "<group>"; };
42A9724B45F9E14DD1B210D1 /* Pods-ReactiveLists.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveLists.release.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveLists/Pods-ReactiveLists.release.xcconfig"; sourceTree = "<group>"; };
45132CB591F9F96746AF8E96 /* Pods-ReactiveListsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactiveListsTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReactiveListsTests/Pods-ReactiveListsTests.debug.xcconfig"; sourceTree = "<group>"; };
932473A2DAECE0F923C4B570 /* Pods_ReactiveListsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReactiveListsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -226,6 +230,7 @@
257A97DB2017AA3500164403 /* CollectionViewMocks.swift */,
257A97CA2017A80B00164403 /* CollectionViewModelTests.swift */,
257A97BC2017A5AA00164403 /* TestCollectionViewModels.swift */,
39D64C582A54E4DB0071A7F3 /* CollectionViewLazyDiffingTests.swift */,
);
path = CollectionView;
sourceTree = "<group>";
Expand Down Expand Up @@ -279,6 +284,7 @@
32753F8C201BB8310084DCB1 /* UICollectionView+Extensions.swift */,
32753F8E201BB8470084DCB1 /* UITableView+Extensions.swift */,
32E7A1F7201BADE800B90EBC /* ViewRegistrationInfo.swift */,
39A211872A5342EE00288547 /* CollectionCellViewModelDataSource.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -395,6 +401,7 @@
TargetAttributes = {
2576640A1F29075C00C037E3 = {
CreatedOnToolsVersion = 8.3.3;
DevelopmentTeam = 29VP5K5AJ7;
LastSwiftMigration = 1000;
ProvisioningStyle = Automatic;
};
Expand Down Expand Up @@ -612,6 +619,7 @@
32E7A1F8201BADE800B90EBC /* ViewRegistrationInfo.swift in Sources */,
258E31B11F0D8D9C00D6F324 /* CollectionViewDriver.swift in Sources */,
258E31B41F0D8D9C00D6F324 /* TableViewModel.swift in Sources */,
39A211882A5342EE00288547 /* CollectionCellViewModelDataSource.swift in Sources */,
258E31D31F0D8F3100D6F324 /* AccessibilityFormats.swift in Sources */,
32753F8F201BB8470084DCB1 /* UITableView+Extensions.swift in Sources */,
A8D93CAA24FD9BDF00459EBB /* TableCellViewModelDataSource.swift in Sources */,
Expand All @@ -631,6 +639,7 @@
257A97D82017A82F00164403 /* CollectionViewDriverTests.swift in Sources */,
257A97C02017A5D300164403 /* TestCollectionViewModels.swift in Sources */,
257A97D92017A82F00164403 /* CollectionViewModelTests.swift in Sources */,
39D64C592A54E4DB0071A7F3 /* CollectionViewLazyDiffingTests.swift in Sources */,
25B1B0B920195F1C0036545F /* CollectionViewDriverDiffingTests.swift in Sources */,
257A97DA2017A83400164403 /* XCTest+Parameterized.swift in Sources */,
257A97D42017A82900164403 /* TableViewMocks.swift in Sources */,
Expand Down Expand Up @@ -685,7 +694,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 29VP5K5AJ7;
INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.plangrid.ReactiveListsExample;
Expand All @@ -701,7 +710,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 29VP5K5AJ7;
INFOPLIST_FILE = "$(SRCROOT)/Example/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.plangrid.ReactiveListsExample;
Expand Down Expand Up @@ -881,7 +890,8 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
INFOPLIST_FILE = "$(SRCROOT)/Tests/Info.plist";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.plangrid.ReactiveListsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -898,7 +908,8 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = Tests/Info.plist;
INFOPLIST_FILE = "$(SRCROOT)/Tests/Info.plist";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.plangrid.ReactiveListsTests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
126 changes: 126 additions & 0 deletions Sources/CollectionCellViewModelDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// PlanGrid
// https://www.plangrid.com
// https://medium.com/plangrid-technology
//
// Documentation
// https://plangrid.github.io/ReactiveLists
//
// GitHub
// https://github.com/plangrid/ReactiveLists
//
// License
// Copyright © 2018-present PlanGrid, Inc.
// Released under an MIT license: https://opensource.org/licenses/MIT
//

import DifferenceKit
import Foundation

/// Protocol for providing `CollectionViewModel`s to a `CollectionSectionViewModel`
///
/// It is itself the `Collection` of `CollectionCellViewModel` and
/// also provides hooks for pre-fetching data
///
/// - Note: `[TableCellViewModel]` has a default implementation
public protocol CollectionCellViewModelDataSourceProtocol: RandomAccessCollection where Element == CollectionCellViewModel, Index == Int {

/// Called by the equivalent `UITableViewDataSourcePrefetching` method
/// - Parameter indices: The indices in the section, for which to prefetch the models
func prefetchRowsAt<S: Sequence>(indices: S) where S.Element == Int

/// Called by the equivalent `UITableViewDataSourcePrefetching` method
/// - Parameter indices: The indices in the section, for which to cancel prefetchign the models
func cancelPrefetchingRowsAt<S: Sequence>(indices: S) where S.Element == Int

/// The `ViewRegistrationInfo` for the cells represented by this datasource
var cellRegistrationInfo: [ViewRegistrationInfo] { get }
}

/// The concrete data source that wraps a provided `TableCellViewModelDataSourceProtocol` implementation
public struct CollectionCellViewModelDataSource: RandomAccessCollection {

// MARK: `CollectionCellViewModelDataSourceProtocol` wrapper blocks for type erasure

/// :nodoc:
private let _subscriptBlock: (Int) -> CollectionCellViewModel

/// :nodoc:
private let _prefetchBlock: (AnySequence<Int>) -> Void

/// :nodoc:
private let _prefetchCancelBlock: (AnySequence<Int>) -> Void

/// Initializes the `CollectionCellViewModelDataSource` with the provided `CollectionCellViewModelDataSourceProtocol` implementation
public init<DataSource: CollectionCellViewModelDataSourceProtocol>(_ dataSource: DataSource) {
self.init(dataSource, cellRegistrationInfo: dataSource.cellRegistrationInfo)
}

/// Used internally by the public init and during diffing
/// when cached ``ViewRegistrationInfo` is available
init<DataSource: CollectionCellViewModelDataSourceProtocol>(_ dataSource: DataSource, cellRegistrationInfo: [ViewRegistrationInfo]) {
self._prefetchBlock = dataSource.prefetchRowsAt
self._prefetchCancelBlock = dataSource.cancelPrefetchingRowsAt
self._subscriptBlock = { dataSource[$0] }
self.startIndex = dataSource.startIndex
self.endIndex = dataSource.endIndex
self.cellRegistrationInfo = cellRegistrationInfo
}

// MARK: - Protocol Implementation

/// :nodoc:
public let cellRegistrationInfo: [ViewRegistrationInfo]

/// :nodoc:
func prefetchRowsAt<S: Sequence>(indices: S) where S.Element == Int {
self._prefetchBlock(AnySequence(indices))
}

/// :nodoc:
func cancelPrefetchingRowsAt<S: Sequence>(indices: S) where S.Element == Int { self._prefetchCancelBlock(AnySequence(indices)) }

/// :nodoc:
public typealias Element = CollectionCellViewModel

/// :nodoc:
public typealias Index = Int

/// :nodoc:
public subscript(position: Int) -> CollectionCellViewModel {
self._subscriptBlock(position)
}

/// :nodoc:
public let startIndex: Int

/// :nodoc:
public let endIndex: Int
}

extension Array: CollectionCellViewModelDataSourceProtocol where Element == CollectionCellViewModel {

/// :nodoc:
public func prefetchRowsAt<S: Sequence>(indices: S) where S.Element == Int {}

/// :nodoc:
public func cancelPrefetchingRowsAt<S: Sequence>(indices: S) where S.Element == Int {}

/// :nodoc:
public var cellRegistrationInfo: [ViewRegistrationInfo] {
self.map {
$0.registrationInfo
}
}
}

//extension Array where Element == IndexPath {j
//
// /// Helper that transforms `[IndexPath]` to sequence of pairs of sections and row sequences
// func indicesBySection() -> AnySequence<(Int, AnySequence<Int>)> {
// let indexPathsBySection = [Int: [IndexPath]](grouping: self) { $0.section }
// return AnySequence(indexPathsBySection.lazy.map { section, indexPaths in
// return (section, AnySequence(indexPaths.lazy.map { $0.row }))
// })
// }
//}
73 changes: 66 additions & 7 deletions Sources/CollectionViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public class CollectionViewDriver: NSObject {
private var _shouldDeselectUponSelection: Bool

private let _automaticDiffingEnabled: Bool

private let useDataSource: Bool

// MARK: Initialization

Expand All @@ -72,14 +74,17 @@ public class CollectionViewDriver: NSObject {
collectionView: UICollectionView,
collectionViewModel: CollectionViewModel? = nil,
shouldDeselectUponSelection: Bool = true,
useDataSource: Bool = false,
automaticDiffingEnabled: Bool = true) {
self._collectionViewModel = collectionViewModel
self.collectionView = collectionView
self._automaticDiffingEnabled = automaticDiffingEnabled
self.useDataSource = useDataSource
self._shouldDeselectUponSelection = shouldDeselectUponSelection
super.init()
collectionView.dataSource = self
collectionView.delegate = self
collectionView.prefetchDataSource = self
self._updateCollectionViewModel(from: nil, to: collectionViewModel)
}

Expand Down Expand Up @@ -154,14 +159,31 @@ public class CollectionViewDriver: NSObject {
guard let newModel = newModel else { return }

if self._automaticDiffingEnabled {

let old: [CollectionSectionViewModel] = oldModel?.sectionModels ?? []
let changeset = StagedChangeset(source: old, target: newModel.sectionModels)
if changeset.isEmpty {
self._collectionViewModel = newModel
if self.useDataSource {
let visibleIndexPaths = self.collectionView.indexPathsForVisibleItems
let old: [DiffableCollectionSectionViewModel] = oldModel?.sectionModelsForDiffing(inVisibleIndexPaths: visibleIndexPaths) ?? []
let changeset = StagedChangeset<[DiffableCollectionSectionViewModel]>(
source: old,
target: newModel.sectionModelsForDiffing(inVisibleIndexPaths: visibleIndexPaths)
)

if changeset.isEmpty {
self._collectionViewModel = newModel
} else {
self.collectionView.reload(using: changeset) {
self._collectionViewModel = $0.makeCollectionViewModel()
}
self._collectionViewModel = newModel
}
} else {
self.collectionView.reload(using: changeset) {
self._collectionViewModel = CollectionViewModel(sectionModels: $0)
let old: [CollectionSectionViewModel] = oldModel?.sectionModels ?? []
let changeset = StagedChangeset(source: old, target: newModel.sectionModels)
if changeset.isEmpty {
self._collectionViewModel = newModel
} else {
self.collectionView.reload(using: changeset) {
self._collectionViewModel = CollectionViewModel(sectionModels: $0)
}
}
}
self.refreshViews()
Expand Down Expand Up @@ -195,6 +217,9 @@ extension CollectionViewDriver: UICollectionViewDataSource {
/// :nodoc:
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let sectionModel = self.collectionViewModel?[ifExists: section] else { return 0 }
if let datasource = sectionModel.cellViewModelDataSource {
return datasource.count
}
return sectionModel.cellViewModels.count
}

Expand Down Expand Up @@ -226,6 +251,40 @@ extension CollectionViewDriver: UICollectionViewDataSource {
}
}

extension CollectionViewDriver: UICollectionViewDataSourcePrefetching {

private func _enumerateCellDataSourcesForPrefetch(
indexPaths: [IndexPath],
enumerationBlock: (CollectionCellViewModelDataSource, AnySequence<Int>) -> Void
) {
guard let sectionModels = self.collectionViewModel?.sectionModels else { return }
// if this is called during a batch update, sections can shift
// around, which can lead to accessing a bad section
let indexIsValid = sectionModels.indices.contains
for (section, indices) in indexPaths.indicesBySection() where indexIsValid(section) {
guard let dataSource = sectionModels[section].cellViewModelDataSource else { return }
enumerationBlock(dataSource, indices)
}
}


public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
self._enumerateCellDataSourcesForPrefetch(
indexPaths: indexPaths
) { datasource, indices in
datasource.prefetchRowsAt(indices: indices)
}
}

public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
self._enumerateCellDataSourcesForPrefetch(
indexPaths: indexPaths
) { datasource, indices in
datasource.cancelPrefetchingRowsAt(indices: indices)
}
}
}

extension CollectionViewDriver: UICollectionViewDelegate {

/// :nodoc:
Expand Down
Loading
Loading