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

Remove the Equatable requirement on components. #87

Merged
merged 1 commit into from
Jan 9, 2019
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
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