Skip to content

Commit

Permalink
Merge pull request #23 from traderepublic/develop
Browse files Browse the repository at this point in the history
Release 0.7
  • Loading branch information
Niclas Kristek authored Oct 20, 2020
2 parents 7f19bd1 + 9a98aeb commit f6dae13
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 16 deletions.
89 changes: 89 additions & 0 deletions DiffingSectionKit/Sources/ManualDiffingListSectionController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import DifferenceKit
import Foundation
import SectionKit

/**
A `SectionController` that contains a list of items and calculates the difference whenever there is an update.

This `SectionController` is typically used when there are multiple semantically similar items
of a model to be displayed and the list of items may dynamically change.

- Note: Compared to `DiffingListSectionController` this doesn't have a `Differentiable` constraint on the generic
`Item` type, instead it requires closures to get diffing information for an item.
*/
open class ManualDiffingListSectionController<
Model: SectionModel,
Item,
ItemId: Hashable
>: ListSectionController<Model, Item> {
private let itemId: (Item) -> ItemId
private let itemContentIsEqual: (Item, Item) -> Bool

/**
Initialize an instance of `ManualDiffingListSectionController`.

- Parameter model: The model of this `SectionController`.

- Parameter itemId: A closure that returns the identifier for a given item.

- Parameter itemContentIsEqual: A closure that checks two items for equality.
*/
public init(model: Model, itemId: @escaping (Item) -> ItemId, itemContentIsEqual: @escaping (Item, Item) -> Bool) {
self.itemId = itemId
self.itemContentIsEqual = itemContentIsEqual
super.init(model: model)
}

override open func calculateUpdate(from oldData: [Item],
to newData: [Item]) -> CollectionViewSectionUpdate<[Item]>? {
let changeSet = StagedChangeset(
source: oldData.map { DifferentiableBox(value: $0, id: itemId, isContentEqual: itemContentIsEqual) },
target: newData.map { DifferentiableBox(value: $0, id: itemId, isContentEqual: itemContentIsEqual) }
)
return CollectionViewSectionUpdate(sectionId: model.sectionId,
batchOperations: changeSet.mapData(\.value).map(\.sectionBatchOperation),
setData: { [weak self] in self?.collectionViewItems = $0 },
shouldReload: { $0.changes.count > 100 })
}
}

extension ManualDiffingListSectionController where Item: Equatable {
/**
Initialize an instance of `ManualDiffingListSectionController`.

- Parameter model: The model of this `SectionController`.

- Parameter itemId: A closure that returns the identifier for a given item.
*/
public convenience init(model: Model, itemId: @escaping (Item) -> ItemId) {
self.init(model: model, itemId: itemId, itemContentIsEqual: ==)
}
}

@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
extension ManualDiffingListSectionController where Item: Identifiable, Item.ID == ItemId {
/**
Initialize an instance of `ManualDiffingListSectionController`.

- Parameter model: The model of this `SectionController`.

- Parameter itemContentIsEqual: A closure that checks two items for equality.
*/
public convenience init(model: Model, itemContentIsEqual: @escaping (Item, Item) -> Bool) {
self.init(model: model, itemId: \.id, itemContentIsEqual: itemContentIsEqual)
}
}

// NOTE: the following unfortunately doesn't compile since declaring `init(model: Model)` would require
// to override the parent initialiser
//@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
//extension ManualDiffingListSectionController where Item: Identifiable & Equatable, Item.ID == ItemId {
// /**
// Initialize an instance of `ManualDiffingListSectionController`.
//
// - Parameter model: The model of this `SectionController`.
// */
// public convenience init(model: Model) {
// self.init(model: model, itemId: \.id, itemContentIsEqual: ==)
// }
//}
26 changes: 26 additions & 0 deletions DiffingSectionKit/Sources/Utility/DifferentiableBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import DifferenceKit
import Foundation

public class DifferentiableBox<Value, Id: Hashable>: Differentiable {
public let value: Value

@usableFromInline
internal let id: (Value) -> Id

@usableFromInline
internal let isContentEqual: (Value, Value) -> Bool

public init(value: Value, id: @escaping (Value) -> Id, isContentEqual: @escaping (Value, Value) -> Bool) {
self.value = value
self.id = id
self.isContentEqual = isContentEqual
}

@inlinable
public var differenceIdentifier: Id { id(value) }

@inlinable
public func isContentEqual(to source: DifferentiableBox<Value, Id>) -> Bool {
isContentEqual(value, source.value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ extension Section: Differentiable {

@inlinable
public func isContentEqual(to source: Section) -> Bool {
// only check for section id since we do not want to reload the section in the collection view
// changes to the section will instead be handled by the sectioncontroller
model.sectionId == source.model.sectionId
}
}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ The functionality is split into two packages:
This `SectionController` is typically used when there are multiple semantically similar items
of a model to be displayed and the list of items may dynamically change.

- `ManualDiffingListSectionController<Model: SectionModel, Item>` (DiffingSectionKit):

A `SectionController` that contains a list of items and calculates the difference whenever there is an update.

This `SectionController` is typically used when there are multiple semantically similar items
of a model to be displayed and the list of items may dynamically change.

> Note: Compared to `DiffingListSectionController` this doesn't have a `Differentiable` constraint on the generic
`Item` type, instead it requires closures to get diffing information for an item.

## Concept

![Diagram](./Resources/SectionKit.svg)
20 changes: 20 additions & 0 deletions SectionKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
5A7C9FD02509250900A32BE6 /* DifferenceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A7C9FCF2509250900A32BE6 /* DifferenceKit.framework */; };
5A7C9FD6250A2ECC00A32BE6 /* DiffingListCollectionViewAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A7C9FD5250A2ECC00A32BE6 /* DiffingListCollectionViewAdapterTests.swift */; };
5AD17C77252B6DF5009DEF3F /* IndexPath+IsValid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD17C76252B6DF5009DEF3F /* IndexPath+IsValid.swift */; };
5AD17C7E252C5174009DEF3F /* SectionDataSourcePrefetchingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD17C7D252C5174009DEF3F /* SectionDataSourcePrefetchingDelegate.swift */; };
5AD17C8A252C575D009DEF3F /* Sequence+Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD17C89252C575D009DEF3F /* Sequence+Group.swift */; };
5AD17C91252C5780009DEF3F /* ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD17C90252C5780009DEF3F /* ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift */; };
5AED5914250A34B600E97796 /* DifferenceKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5A7C9FCF2509250900A32BE6 /* DifferenceKit.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5AF1A3E7253F13E4000058C6 /* ManualDiffingListSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF1A3E6253F13E4000058C6 /* ManualDiffingListSectionController.swift */; };
5AF1A401253F33F8000058C6 /* DifferentiableBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF1A400253F33F8000058C6 /* DifferentiableBox.swift */; };
5AFC546C250BBB250099E3BD /* CollectionViewContext+CollectionViewSectionUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFC546B250BBB250099E3BD /* CollectionViewContext+CollectionViewSectionUpdate.swift */; };
5AFC546E250BBF3A0099E3BD /* CollectionViewContext+CollectionViewUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AFC546D250BBF3A0099E3BD /* CollectionViewContext+CollectionViewUpdate.swift */; };
OBJ_131 /* DiffingListCollectionViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* DiffingListCollectionViewAdapter.swift */; };
Expand Down Expand Up @@ -144,6 +149,11 @@
5A7C9FCF2509250900A32BE6 /* DifferenceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DifferenceKit.framework; path = Carthage/Build/iOS/DifferenceKit.framework; sourceTree = "<group>"; };
5A7C9FD5250A2ECC00A32BE6 /* DiffingListCollectionViewAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffingListCollectionViewAdapterTests.swift; sourceTree = "<group>"; };
5AD17C76252B6DF5009DEF3F /* IndexPath+IsValid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IndexPath+IsValid.swift"; sourceTree = "<group>"; };
5AD17C7D252C5174009DEF3F /* SectionDataSourcePrefetchingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionDataSourcePrefetchingDelegate.swift; sourceTree = "<group>"; };
5AD17C89252C575D009DEF3F /* Sequence+Group.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Group.swift"; sourceTree = "<group>"; };
5AD17C90252C5780009DEF3F /* ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift"; sourceTree = "<group>"; };
5AF1A3E6253F13E4000058C6 /* ManualDiffingListSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualDiffingListSectionController.swift; sourceTree = "<group>"; };
5AF1A400253F33F8000058C6 /* DifferentiableBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DifferentiableBox.swift; sourceTree = "<group>"; };
5AFC546B250BBB250099E3BD /* CollectionViewContext+CollectionViewSectionUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewContext+CollectionViewSectionUpdate.swift"; sourceTree = "<group>"; };
5AFC546D250BBF3A0099E3BD /* CollectionViewContext+CollectionViewUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewContext+CollectionViewUpdate.swift"; sourceTree = "<group>"; };
OBJ_10 /* DiffingListSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffingListSectionController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -260,6 +270,7 @@
OBJ_13 /* ContentIdentifiable+Identifiable.swift */,
OBJ_14 /* Section+Differentiable.swift */,
OBJ_15 /* StagedChangeset+MapData.swift */,
5AF1A400253F33F8000058C6 /* DifferentiableBox.swift */,
);
path = Utility;
sourceTree = "<group>";
Expand Down Expand Up @@ -308,6 +319,7 @@
children = (
OBJ_26 /* FoundationDiffingListCollectionViewAdapter.swift */,
OBJ_27 /* ListCollectionViewAdapter+UICollectionViewDataSource.swift */,
5AD17C90252C5780009DEF3F /* ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift */,
OBJ_28 /* ListCollectionViewAdapter+UICollectionViewDelegate.swift */,
OBJ_29 /* ListCollectionViewAdapter+UICollectionViewDragDelegate.swift */,
OBJ_30 /* ListCollectionViewAdapter+UICollectionViewDropDelegate.swift */,
Expand All @@ -324,6 +336,7 @@
OBJ_40 /* Generic */,
OBJ_46 /* SectionController.swift */,
OBJ_47 /* SectionDataSource.swift */,
5AD17C7D252C5174009DEF3F /* SectionDataSourcePrefetchingDelegate.swift */,
OBJ_48 /* SectionDelegate.swift */,
OBJ_49 /* SectionDragDelegate.swift */,
OBJ_50 /* SectionDropDelegate.swift */,
Expand Down Expand Up @@ -385,6 +398,7 @@
OBJ_60 /* Move.swift */,
OBJ_61 /* UICollectionView+Apply.swift */,
5AD17C76252B6DF5009DEF3F /* IndexPath+IsValid.swift */,
5AD17C89252C575D009DEF3F /* Sequence+Group.swift */,
);
path = Utility;
sourceTree = "<group>";
Expand Down Expand Up @@ -455,6 +469,7 @@
children = (
OBJ_9 /* DiffingListCollectionViewAdapter.swift */,
OBJ_10 /* DiffingListSectionController.swift */,
5AF1A3E6253F13E4000058C6 /* ManualDiffingListSectionController.swift */,
OBJ_11 /* Utility */,
);
name = DiffingSectionKit;
Expand Down Expand Up @@ -638,9 +653,11 @@
OBJ_131 /* DiffingListCollectionViewAdapter.swift in Sources */,
OBJ_132 /* DiffingListSectionController.swift in Sources */,
OBJ_133 /* Changeset+BatchOperations.swift in Sources */,
5AF1A3E7253F13E4000058C6 /* ManualDiffingListSectionController.swift in Sources */,
OBJ_134 /* ContentIdentifiable+Identifiable.swift in Sources */,
OBJ_135 /* Section+Differentiable.swift in Sources */,
OBJ_136 /* StagedChangeset+MapData.swift in Sources */,
5AF1A401253F33F8000058C6 /* DifferentiableBox.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -659,6 +676,7 @@
OBJ_160 /* CollectionViewBatchOperation.swift in Sources */,
OBJ_161 /* CollectionViewChange.swift in Sources */,
OBJ_162 /* CollectionViewUpdate.swift in Sources */,
5AD17C8A252C575D009DEF3F /* Sequence+Group.swift in Sources */,
OBJ_163 /* CollectionViewAdapter.swift in Sources */,
OBJ_164 /* CollectionViewAdapterDataSource.swift in Sources */,
OBJ_165 /* CollectionViewContext.swift in Sources */,
Expand All @@ -668,6 +686,7 @@
OBJ_169 /* ListCollectionViewAdapter+UICollectionViewDragDelegate.swift in Sources */,
OBJ_170 /* ListCollectionViewAdapter+UICollectionViewDropDelegate.swift in Sources */,
OBJ_171 /* ListCollectionViewAdapter+UICollectionViewFlowLayout.swift in Sources */,
5AD17C7E252C5174009DEF3F /* SectionDataSourcePrefetchingDelegate.swift in Sources */,
5AFC546E250BBF3A0099E3BD /* CollectionViewContext+CollectionViewUpdate.swift in Sources */,
OBJ_172 /* ListCollectionViewAdapter+UIScrollViewDelegate.swift in Sources */,
OBJ_173 /* ListCollectionViewAdapter.swift in Sources */,
Expand All @@ -684,6 +703,7 @@
OBJ_184 /* SectionController.swift in Sources */,
OBJ_185 /* SectionDataSource.swift in Sources */,
OBJ_186 /* SectionDelegate.swift in Sources */,
5AD17C91252C5780009DEF3F /* ListCollectionViewAdapter+UICollectionViewDataSourcePrefetching.swift in Sources */,
OBJ_187 /* SectionDragDelegate.swift in Sources */,
OBJ_188 /* SectionDropDelegate.swift in Sources */,
5AFC546C250BBB250099E3BD /* CollectionViewContext+CollectionViewSectionUpdate.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import UIKit

@available(iOS 10.0, *)
extension ListCollectionViewAdapter: UICollectionViewDataSourcePrefetching {
open func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
let validIndexPaths = indexPaths.filter { $0.isSectionIndexValid(for: sections) }
validIndexPaths.group(by: \.section).forEach { sectionIndex, indexPaths in
guard let prefetchingDelegate = sections[sectionIndex].controller.dataSourcePrefetchingDelegate else {
return
}
let sectionIndexPaths = indexPaths.map(SectionIndexPath.init)
prefetchingDelegate.prefetchItems(at: sectionIndexPaths)
}
}

open func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
let validIndexPaths = indexPaths.filter { $0.isSectionIndexValid(for: sections) }
validIndexPaths.group(by: \.section).forEach { sectionIndex, indexPaths in
guard let prefetchingDelegate = sections[sectionIndex].controller.dataSourcePrefetchingDelegate else {
return
}
let sectionIndexPaths = indexPaths.map(SectionIndexPath.init)
prefetchingDelegate.cancelPrefetchingForItems(at: sectionIndexPaths)
}
}
}
24 changes: 18 additions & 6 deletions SectionKit/Sources/Section/Generic/BaseSectionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import UIKit
Every declaration is marked `open` and can be overriden.
*/
open class BaseSectionController: SectionController,
SectionDataSource,
SectionDelegate,
SectionFlowDelegate,
SectionDragDelegate,
SectionDropDelegate {
// MARK: - BaseSectionController
SectionDataSource,
SectionDataSourcePrefetchingDelegate,
SectionDelegate,
SectionFlowDelegate,
SectionDragDelegate,
SectionDropDelegate {
// MARK: - Init

public init () { }

Expand All @@ -21,6 +22,9 @@ open class BaseSectionController: SectionController,

open var dataSource: SectionDataSource { self }

@available(iOS 10.0, *)
open var dataSourcePrefetchingDelegate: SectionDataSourcePrefetchingDelegate? { self }

open var delegate: SectionDelegate? { self }

open var flowDelegate: SectionFlowDelegate? { self }
Expand Down Expand Up @@ -71,6 +75,14 @@ open class BaseSectionController: SectionController,
return 0
}

// MARK: - SectionDataSourcePrefetchingDelegate

open func prefetchItems(at indexPaths: [SectionIndexPath]) {
}

open func cancelPrefetchingForItems(at indexPaths: [SectionIndexPath]) {
}

// MARK: - SectionDelegate

open func shouldHighlightItem(at indexPath: SectionIndexPath) -> Bool {
Expand Down
7 changes: 7 additions & 0 deletions SectionKit/Sources/Section/SectionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public protocol SectionController: AnyObject {
/// The datasource of this section.
var dataSource: SectionDataSource { get }

/// The prefetching delegate of the datasource of this section.
@available(iOS 10.0, *)
var dataSourcePrefetchingDelegate: SectionDataSourcePrefetchingDelegate? { get }

/// The delegate of this section.
var delegate: SectionDelegate? { get }

Expand All @@ -27,6 +31,9 @@ public protocol SectionController: AnyObject {
}

extension SectionController {
@available(iOS 10.0, *)
public var dataSourcePrefetchingDelegate: SectionDataSourcePrefetchingDelegate? { nil }

public var delegate: SectionDelegate? { nil }

public var flowDelegate: SectionFlowDelegate? { nil }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import UIKit

/// The delegate for datasource prefetching.
@available(iOS 10.0, *)
public protocol SectionDataSourcePrefetchingDelegate: AnyObject {
/**
Tells the delegate to start prefetching items at the specified indexPaths.

- Parameter indexPaths: The index paths of the items to be prefetched.
*/
func prefetchItems(at indexPaths: [SectionIndexPath])

/**
Tells the delegate to cancel prefetching items at the specified indexPaths.

- Parameter indexPaths: The index paths of the items that previously were considered as candidates for pre-fetching, but were not actually used.
*/
func cancelPrefetchingForItems(at indexPaths: [SectionIndexPath])
}

@available(iOS 10.0, *)
extension SectionDataSourcePrefetchingDelegate {
public func cancelPrefetchingForItems(at indexPaths: [SectionIndexPath]) {
}
}
5 changes: 0 additions & 5 deletions SectionKit/Sources/Section/SectionDragDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ public protocol SectionDragDelegate: AnyObject {

@available(iOS 11.0, *)
extension SectionDragDelegate {
public func dragItems(forBeginning session: UIDragSession,
at indexPath: SectionIndexPath) -> [UIDragItem] {
return []
}

public func dragItems(forAddingTo session: UIDragSession,
at indexPath: SectionIndexPath,
point: CGPoint) -> [UIDragItem] {
Expand Down
4 changes: 0 additions & 4 deletions SectionKit/Sources/Section/SectionDropDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ extension SectionDropDelegate {
return UICollectionViewDropProposal(operation: .forbidden)
}

public func performDrop(at indexPath: SectionIndexPath,
with coordinator: UICollectionViewDropCoordinator) {
}

public func dropPreviewParametersForItem(at indexPath: SectionIndexPath) -> UIDragPreviewParameters? {
return nil
}
Expand Down
Loading

0 comments on commit f6dae13

Please sign in to comment.