Skip to content

Commit

Permalink
Remove the Equatable requirement on components.
Browse files Browse the repository at this point in the history
  • Loading branch information
andersio committed Dec 11, 2018
1 parent ac32365 commit 26c30bc
Show file tree
Hide file tree
Showing 15 changed files with 95 additions and 328 deletions.
18 changes: 6 additions & 12 deletions Bento.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
58FC4427207CF2BB00DA3614 /* AnyRenderableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A3EF77F205D866F00D043AC /* AnyRenderableTests.swift */; };
58FC4428207CF2BB00DA3614 /* TestId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A784702205EB61D00FA597E /* TestId.swift */; };
58FC4429207CF2BB00DA3614 /* TestRenderable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A784700205EB5E000FA597E /* TestRenderable.swift */; };
58FC442A207CF2BB00DA3614 /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7846FC205EAF7C00FA597E /* NodeTests.swift */; };
58FC442B207CF2BB00DA3614 /* SectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A7846FE205EAF8400FA597E /* SectionTests.swift */; };
58FC442F207CF37700DA3614 /* script in Resources */ = {isa = PBXBuildFile; fileRef = 58FC442E207CF37700DA3614 /* script */; };
58FC4430207CF3CD00DA3614 /* Bento.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58BA755C2016303B0050D5F1 /* Bento.framework */; };
58FC4431207CF3CD00DA3614 /* Bento.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 58BA755C2016303B0050D5F1 /* Bento.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -76,7 +74,7 @@
740921B820ACE5EC00B59F5C /* ConcatenationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740921B720ACE5EC00B59F5C /* ConcatenationTests.swift */; };
74208FA12083B1F00062CC8D /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74208FA22083B1F00062CC8D /* Nimble.framework */; };
747523C72083A6660030CCAA /* FlexibleDiff.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 747523C82083A6660030CCAA /* FlexibleDiff.framework */; };
9A4DB3E1212CB3710079D5AE /* Changeset+MutationIndexPairs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4DB3E0212CB3710079D5AE /* Changeset+MutationIndexPairs.swift */; };
9A4DB3E1212CB3710079D5AE /* ChangesetExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A4DB3E0212CB3710079D5AE /* ChangesetExtensions.swift */; };
9AF4786A21205E3100F87E21 /* Supplement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF4786921205E3100F87E21 /* Supplement.swift */; };
9AF4786C2120CA7500F87E21 /* BentoReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF4786B2120CA7500F87E21 /* BentoReusableView.swift */; };
A67A873520B5D2D100BC2BAC /* RenderableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67A873420B5D2D100BC2BAC /* RenderableTests.swift */; };
Expand Down Expand Up @@ -170,6 +168,7 @@
58FC4440207CFBD700DA3614 /* MovieComponentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MovieComponentView.xib; sourceTree = "<group>"; };
58FC444B207CFBE100DA3614 /* BookAppointmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookAppointmentViewController.swift; sourceTree = "<group>"; };
58FC444C207CFBE200DA3614 /* MoviesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoviesListViewController.swift; sourceTree = "<group>"; };
5B175A6421C0634800590F34 /* ComponentContract.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ComponentContract.md; sourceTree = "<group>"; };
61B40916208523F50063DE25 /* IntroViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroViewModel.swift; sourceTree = "<group>"; };
61B40917208523F50063DE25 /* IntroViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntroViewController.swift; sourceTree = "<group>"; };
61B64BB02086561B0092082C /* IntroRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroRenderer.swift; sourceTree = "<group>"; };
Expand All @@ -192,10 +191,8 @@
74208FA22083B1F00062CC8D /* Nimble.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Nimble.framework; sourceTree = BUILT_PRODUCTS_DIR; };
747523C82083A6660030CCAA /* FlexibleDiff.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FlexibleDiff.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9A3EF77F205D866F00D043AC /* AnyRenderableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRenderableTests.swift; sourceTree = "<group>"; };
9A4DB3E0212CB3710079D5AE /* Changeset+MutationIndexPairs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Changeset+MutationIndexPairs.swift"; sourceTree = "<group>"; };
9A4DB3E0212CB3710079D5AE /* ChangesetExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangesetExtensions.swift; sourceTree = "<group>"; };
9A7846FA205D89C000FA597E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
9A7846FC205EAF7C00FA597E /* NodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = "<group>"; };
9A7846FE205EAF8400FA597E /* SectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTests.swift; sourceTree = "<group>"; };
9A784700205EB5E000FA597E /* TestRenderable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestRenderable.swift; sourceTree = "<group>"; };
9A784702205EB61D00FA597E /* TestId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestId.swift; sourceTree = "<group>"; };
9AF4786921205E3100F87E21 /* Supplement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Supplement.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -251,9 +248,10 @@
581ED77E2022041C00EC9584 /* Diff */ = {
isa = PBXGroup;
children = (
5B175A6421C0634800590F34 /* ComponentContract.md */,
58467B02202B33F200577C77 /* TableViewSectionDiff.swift */,
61B64BB320873DA10092082C /* CollectionViewSectionDiff.swift */,
9A4DB3E0212CB3710079D5AE /* Changeset+MutationIndexPairs.swift */,
9A4DB3E0212CB3710079D5AE /* ChangesetExtensions.swift */,
);
path = Diff;
sourceTree = "<group>";
Expand Down Expand Up @@ -374,8 +372,6 @@
children = (
A67A873420B5D2D100BC2BAC /* RenderableTests.swift */,
9A3EF77F205D866F00D043AC /* AnyRenderableTests.swift */,
9A7846FC205EAF7C00FA597E /* NodeTests.swift */,
9A7846FE205EAF8400FA597E /* SectionTests.swift */,
740921B520ACDDDA00B59F5C /* IfTests.swift */,
740921B720ACE5EC00B59F5C /* ConcatenationTests.swift */,
9A784700205EB5E000FA597E /* TestRenderable.swift */,
Expand Down Expand Up @@ -660,7 +656,7 @@
65E3ECAC2113591500869DF3 /* FocusableView.swift in Sources */,
A9509880661501C40B50E453 /* TableViewHeaderFooterView.swift in Sources */,
515F94097C1F82B7E5C2DE0C /* Section.swift in Sources */,
9A4DB3E1212CB3710079D5AE /* Changeset+MutationIndexPairs.swift in Sources */,
9A4DB3E1212CB3710079D5AE /* ChangesetExtensions.swift in Sources */,
65E3ECA421133BF700869DF3 /* FocusEligibilitySource.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -670,11 +666,9 @@
buildActionMask = 2147483647;
files = (
58FC4427207CF2BB00DA3614 /* AnyRenderableTests.swift in Sources */,
58FC442A207CF2BB00DA3614 /* NodeTests.swift in Sources */,
58FC4428207CF2BB00DA3614 /* TestId.swift in Sources */,
58FC4429207CF2BB00DA3614 /* TestRenderable.swift in Sources */,
740921B620ACDDDA00B59F5C /* IfTests.swift in Sources */,
58FC442B207CF2BB00DA3614 /* SectionTests.swift in Sources */,
740921B820ACE5EC00B59F5C /* ConcatenationTests.swift in Sources */,
A67A873520B5D2D100BC2BAC /* RenderableTests.swift in Sources */,
);
Expand Down
6 changes: 1 addition & 5 deletions Bento/Bento/Node.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import UIKit

public struct Node<Identifier: Hashable>: Equatable {
public struct Node<Identifier: Hashable> {
public let id: Identifier
let component: AnyRenderable

Expand All @@ -13,10 +13,6 @@ public struct Node<Identifier: Hashable>: Equatable {
self.init(id: id, component: AnyRenderable(component))
}

public static func == (lhs: Node, rhs: Node) -> Bool {
return lhs.id == rhs.id && lhs.component == rhs.component
}

public func component<T>(as type: T.Type) -> T? {
return component.cast(to: type)
}
Expand Down
12 changes: 1 addition & 11 deletions Bento/Bento/Section.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import UIKit

public struct Section<SectionID: Hashable, ItemID: Hashable>: Equatable {
public struct Section<SectionID: Hashable, ItemID: Hashable> {
public typealias Item = Node<ItemID>

public let id: SectionID
Expand Down Expand Up @@ -70,16 +70,6 @@ public struct Section<SectionID: Hashable, ItemID: Hashable>: Equatable {
return supplements[supplement]?.sizeBoundTo(size: size, inheritedMargins: inheritedMargins)
}

public static func hasEqualMetadata(_ lhs: Section, _ rhs: Section) -> Bool {
return lhs.supplements == rhs.supplements
}

public static func == (lhs: Section, rhs: Section) -> Bool {
return lhs.id == rhs.id
&& hasEqualMetadata(lhs, rhs)
&& lhs.items == rhs.items
}

public static func |---+ (lhs: Section, rhs: Item) -> Section {
return Section(id: lhs.id, items: lhs.items + [rhs], supplements: lhs.supplements)
}
Expand Down
13 changes: 13 additions & 0 deletions Bento/Bento/UITableViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,16 @@ extension UITableView {
return objc_getAssociatedObject(self, AssociatedKey.adapter) as AnyObject?
}
}

extension UITableView {
var visibleSections: Set<Int> {
let isVisible = (0 ..< numberOfSections)
.map(rect(forSection:))
.map(bounds.intersects)

return Set(
zip(isVisible, 0 ..< numberOfSections)
.compactMap { $0 ? $1 : nil }
)
}
}
10 changes: 0 additions & 10 deletions Bento/Diff/Changeset+MutationIndexPairs.swift

This file was deleted.

19 changes: 19 additions & 0 deletions Bento/Diff/ChangesetExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import FlexibleDiff

extension Changeset {
/// Retrieve the positions of all mutated items, in place or moved. The changes are filtered
/// by the specified collection of pre-removal visible item indices.
///
/// - parameters:
/// - visibleItems: The pre-removal indices of the visible items.
func positionsOfMutations<Indices: Collection>(amongVisible visibleItems: Indices) -> FlattenCollection<[[(source: Int, destination: Int)]]> where Indices.Element == Int {
return [
mutations.lazy
.filter(visibleItems.contains)
.map { (source: $0, destination: $0) },
moves.lazy
.filter { visibleItems.contains($0.source) }
.map { (source: $0.source, destination: $0.destination) }
].joined()
}
}
16 changes: 12 additions & 4 deletions Bento/Diff/CollectionViewSectionDiff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ struct CollectionViewSectionDiff<SectionID: Hashable, ItemID: Hashable> {
}

func apply(to collectionView: UICollectionView, completion: (() -> Void)? = nil) {
/// Since we are going to always rebind visible components, there is no point to evaluate
/// component equation. However, we still force all instances of components to be treated as
/// unequal, so as to preserve all positional information for in-place updates to visible cells.
let diff = SectionedChangeset(previous: oldSections,
current: newSections,
sectionIdentifier: { $0.id },
areMetadataEqual: Section.hasEqualMetadata,
areMetadataEqual: const(false),
items: { $0.items },
itemIdentifier: { $0.id },
areItemsEqual: ==)
areItemsEqual: const(false))
apply(diff: diff, to: collectionView, completion: completion)
}

Expand All @@ -40,7 +43,7 @@ struct CollectionViewSectionDiff<SectionID: Hashable, ItemID: Hashable> {
by: { $0.section }
)

for (source, destination) in diff.sections.mutationIndexPairs {
for (source, destination) in diff.sections.positionsOfMutations(amongVisible: groups.keys) {
if let indexPaths = groups[source] {
for indexPath in indexPaths {
let view = collectionView.supplementaryView(forElementKind: elementKind, at: indexPath)
Expand Down Expand Up @@ -95,8 +98,13 @@ extension UICollectionView {
deleteItems(at: sectionMutation.deletedIndexPaths)
insertItems(at: sectionMutation.insertedIndexPaths)
perform(moves: sectionMutation.movedCollectionIndexPaths)

let visibleItems = Set(
indexPathsForVisibleItems
.compactMap { $0.section == sectionMutation.source ? $0.row : nil }
)

for (source, destination) in sectionMutation.changeset.mutationIndexPairs {
for (source, destination) in sectionMutation.changeset.positionsOfMutations(amongVisible: visibleItems) {
if let cell = cellForItem(at: [sectionMutation.source, source]) as? CollectionViewContainerCell {
let component = newSections[sectionMutation.destination]
.items[destination]
Expand Down
17 changes: 17 additions & 0 deletions Bento/Diff/ComponentContract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Bento Component Contract
Applicable to both UICollectionView and UITableView.

### Components must conform to `Renderable`.
`Renderable` represents a set of requirements which a component must satisfy for Bento to integrate it.

### Components need not be evaluable for equation.
Bento enforces no restriction in what types of properties the component can comprise of. Moreover, there are also prevalent uses of non-equatable properties, for example, user interaction callbacks. Therefore, Bento does not require any component to be evaluable for equation.

### Any given ID path may be bound with any kind and instance of components at any time.
Bento is designed to be very flexible in terms of component types. Users should require no extra precaution in populating their `Box`es with different permutations of ID paths and components.

### Visible ID paths may be aggressively rebound.
[As Bento cannot rely on component equation](#components-need-not-be-evaluable-for-equation), Bento aggressively rebinds all visible ID paths with the new component at the same ID path in any new `Box` being applied.

### Components should avoid deriving animations from rebinding occurrences.
[Given potential occurrences of aggressive rebinding](#visible-id-paths-may-be-aggressively-rebound), rebinding is generally not a good indiction of when animations should happen. Components might want to instead have data-driven, fine-grained animations, or rely on mechanisms like `ViewLifecycleAware`.
27 changes: 21 additions & 6 deletions Bento/Diff/TableViewSectionDiff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ struct TableViewSectionDiff<SectionId: Hashable, RowId: Hashable> {
}

func apply(to tableView: UITableView) {
/// Since we are going to always rebind visible components, there is no point to evaluate
/// component equation. However, we still force all instances of components to be treated as
/// unequal, so as to preserve all positional information for in-place updates to visible cells.
let diff = SectionedChangeset(previous: oldSections,
current: newSections,
sectionIdentifier: { $0.id },
areMetadataEqual: Section.hasEqualMetadata,
areMetadataEqual: const(false),
items: { $0.items },
itemIdentifier: { $0.id },
areItemsEqual: ==)
areItemsEqual: const(false))
apply(diff: diff, to: tableView)
}

private func apply(diff: SectionedChangeset, to tableView: UITableView) {
tableView.beginUpdates()
for (source, destination) in diff.sections.mutationIndexPairs {

let visibleSections = tableView.visibleSections

for (source, destination) in diff.sections.positionsOfMutations(amongVisible: visibleSections) {
if let headerView = tableView.headerView(forSection: source) {
let component = newSections[destination].supplements[.header]
(headerView as? BentoReusableView)?.bind(component)
Expand All @@ -38,6 +44,7 @@ struct TableViewSectionDiff<SectionId: Hashable, RowId: Hashable> {
(footerView as? BentoReusableView)?.bind(component)
}
}

tableView.insertSections(diff.sections.inserts, with: animation.sectionInsertion)
tableView.deleteSections(diff.sections.removals, with: animation.sectionDeletion)
tableView.moveSections(diff.sections.moves, animation: animation)
Expand All @@ -48,14 +55,22 @@ struct TableViewSectionDiff<SectionId: Hashable, RowId: Hashable> {
private func apply(sectionMutations: [SectionedChangeset.MutatedSection],
to tableView: UITableView,
with animation: TableViewAnimation) {
let visibleIndexPaths = tableView.indexPathsForVisibleRows ?? []

for sectionMutation in sectionMutations {
tableView.deleteRows(at: sectionMutation.deletedIndexPaths, with: animation.rowDeletion)
tableView.insertRows(at: sectionMutation.insertedIndexPaths, with: animation.rowInsertion)
tableView.perform(moves: sectionMutation.movedIndexPaths)

for (source, destination) in sectionMutation.changeset.mutationIndexPairs {
if let cell = tableView.cellForRow(at: [sectionMutation.source, source]) as? BentoReusableView {
let node = newSections[sectionMutation.destination].items[destination]
let visibleRows = Set(
visibleIndexPaths.lazy
.filter { $0.section == sectionMutation.source }
.map { $0.item }
)

for (source, destination) in sectionMutation.changeset.positionsOfMutations(amongVisible: visibleRows) {
if let cell = tableView.cellForRow(at: IndexPath(item: source, section: sectionMutation.source)) as? BentoReusableView {
let node = newSections[sectionMutation.destination].items[destination]
cell.bind(node.component)
}
}
Expand Down
4 changes: 4 additions & 0 deletions Bento/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ internal func search<T>(from leaf: UIView, type: T.Type) -> T? {
}
return cell as! T?
}

func const<T, U, Value>(_ value: Value) -> (_: T, _: U) -> Value {
return { _, _ in value }
}
11 changes: 0 additions & 11 deletions Bento/Renderable/AnyRenderable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ struct AnyRenderable: Renderable {

return view
}

static func ==(lhs: AnyRenderable, rhs: AnyRenderable) -> Bool {
return lhs.base.equals(to: rhs.base)
}
}

private class AnyRenderableBox<Base: Renderable>: AnyRenderableBoxBase where Base.View: UIView {
Expand Down Expand Up @@ -91,12 +87,6 @@ private class AnyRenderableBox<Base: Renderable>: AnyRenderableBoxBase where Bas
override func cast<T>(to type: T.Type) -> T? {
return base as? T
}

override func equals(to other: AnyRenderableBoxBase) -> Bool {
guard let other = other as? AnyRenderableBox<Base>
else { return false }
return self.base == other.base
}
}

private class AnyRenderableBoxBase {
Expand All @@ -108,6 +98,5 @@ private class AnyRenderableBoxBase {

func render(in view: UIView) { fatalError() }
func generate() -> UIView { fatalError() }
func equals(to other: AnyRenderableBoxBase) -> Bool { fatalError() }
func cast<T>(to type: T.Type) -> T? { fatalError() }
}
Loading

0 comments on commit 26c30bc

Please sign in to comment.