Skip to content

Commit

Permalink
Create datasource for cells to make lazy-loading possible for collect…
Browse files Browse the repository at this point in the history
…ions (#216)

* Create datasource for cells to make lazy-loading possible for collections

* Add collection test

* Changelog
  • Loading branch information
Daniel-Juarez committed Jul 6, 2023
1 parent 30cab90 commit 1d535d5
Show file tree
Hide file tree
Showing 9 changed files with 520 additions and 15 deletions.
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

0 comments on commit 1d535d5

Please sign in to comment.