Skip to content

Commit

Permalink
Merge pull request #141 from Babylonpartners/anders/adapter-store
Browse files Browse the repository at this point in the history
Introduce `AdapterStore` and integrate it with the TableView adapters.
  • Loading branch information
andersio authored Apr 9, 2019
2 parents a30f616 + abf3ddc commit d846848
Show file tree
Hide file tree
Showing 9 changed files with 716 additions and 22 deletions.
14 changes: 13 additions & 1 deletion Bento.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,16 @@
58FC444A207CFBD700DA3614 /* MovieComponentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58FC4440207CFBD700DA3614 /* MovieComponentView.xib */; };
58FC444D207CFBE200DA3614 /* BookAppointmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC444B207CFBE100DA3614 /* BookAppointmentViewController.swift */; };
58FC444E207CFBE200DA3614 /* MoviesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC444C207CFBE200DA3614 /* MoviesListViewController.swift */; };
5B02B99322503A370089371A /* AdapterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B02B99122503A370089371A /* AdapterProtocol.swift */; };
5B02B99422503A370089371A /* AdapterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B02B99222503A370089371A /* AdapterStore.swift */; };
61B40919208523F60063DE25 /* FoodListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B40916208523F50063DE25 /* FoodListViewModel.swift */; };
61B4091A208523F60063DE25 /* FoodListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B40917208523F50063DE25 /* FoodListViewController.swift */; };
61B64BB12086561B0092082C /* FoodListRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B64BB02086561B0092082C /* FoodListRenderer.swift */; };
61B64BB420873DA10092082C /* CollectionViewSectionDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B64BB320873DA10092082C /* CollectionViewSectionDiff.swift */; };
61B64BB6208745730092082C /* CollectionViewContainerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B64BB5208745730092082C /* CollectionViewContainerCell.swift */; };
65020C322203186400DC8F42 /* NativeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65020C312203186400DC8F42 /* NativeView.swift */; };
651E75C0221005E300130866 /* UIKitContainerDiffApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651E75BF221005E300130866 /* UIKitContainerDiffApplicationTests.swift */; };
653D460B2256665000CF3E4C /* AdapterStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653D460A2256665000CF3E4C /* AdapterStoreTests.swift */; };
65496FB8211C323F00511D8A /* TableViewContainerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587F0717201B355800ACD219 /* TableViewContainerCell.swift */; };
65496FB9211C323F00511D8A /* TableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9509BC2762C8B4277B973D8 /* TableViewAdapter.swift */; };
65A69EDF218B8892005D90AC /* UICollectionViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65A69EDE218B8891005D90AC /* UICollectionViewExtensions.swift */; };
Expand Down Expand Up @@ -183,6 +186,8 @@
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>"; };
5B02B99122503A370089371A /* AdapterProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterProtocol.swift; sourceTree = "<group>"; };
5B02B99222503A370089371A /* AdapterStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterStore.swift; sourceTree = "<group>"; };
5B175A6421C0634800590F34 /* ComponentContract.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ComponentContract.md; sourceTree = "<group>"; };
61B40916208523F50063DE25 /* FoodListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodListViewModel.swift; sourceTree = "<group>"; };
61B40917208523F50063DE25 /* FoodListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoodListViewController.swift; sourceTree = "<group>"; };
Expand All @@ -191,6 +196,7 @@
61B64BB5208745730092082C /* CollectionViewContainerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewContainerCell.swift; sourceTree = "<group>"; };
65020C312203186400DC8F42 /* NativeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeView.swift; sourceTree = "<group>"; };
651E75BF221005E300130866 /* UIKitContainerDiffApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitContainerDiffApplicationTests.swift; sourceTree = "<group>"; };
653D460A2256665000CF3E4C /* AdapterStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdapterStoreTests.swift; sourceTree = "<group>"; };
65A69EDE218B8891005D90AC /* UICollectionViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewExtensions.swift; sourceTree = "<group>"; };
65A69EE0218B8AB6005D90AC /* CustomCollectionViewAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCollectionViewAdapter.swift; sourceTree = "<group>"; };
65BA922D20AF388F004AEF18 /* UITableViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewExtensions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -432,6 +438,7 @@
58FC441E207CF29F00DA3614 /* BentoTests */ = {
isa = PBXGroup;
children = (
653D460A2256665000CF3E4C /* AdapterStoreTests.swift */,
9A3EF77F205D866F00D043AC /* AnyRenderableTests.swift */,
740921B520ACDDDA00B59F5C /* IfTests.swift */,
740921B720ACE5EC00B59F5C /* ConcatenationTests.swift */,
Expand Down Expand Up @@ -487,6 +494,8 @@
A950945F6360B851C3E87B61 /* Adapters */ = {
isa = PBXGroup;
children = (
5B02B99122503A370089371A /* AdapterProtocol.swift */,
5B02B99222503A370089371A /* AdapterStore.swift */,
A9509BC2762C8B4277B973D8 /* TableViewAdapter.swift */,
5874C80420C9342B004EB5EA /* CollectionViewAdapter.swift */,
58D0F12C207F573B00A24E96 /* TableViewAnimation.swift */,
Expand Down Expand Up @@ -684,7 +693,7 @@
5829D269200FB092001E020D /* AppDelegate.swift in Sources */,
04EBE74F222C031B002D38C4 /* Optional+Extension.swift in Sources */,
04C9A0842211967300C70E09 /* SignUpViewController.swift in Sources */,
61B4091A208523F60063DE25 /* IntroViewController.swift in Sources */,
61B4091A208523F60063DE25 /* FoodListViewController.swift in Sources */,
61B4091A208523F60063DE25 /* FoodListViewController.swift in Sources */,
58FC4449207CFBD700DA3614 /* IconTitleDetailsView.swift in Sources */,
044329EA2229096E004EFB29 /* SignUpRenderer.swift in Sources */,
Expand All @@ -700,6 +709,7 @@
files = (
582D9951217E196E00C67B0D /* MenuItemsResponding.swift in Sources */,
9AF4786A21205E3100F87E21 /* Supplement.swift in Sources */,
5B02B99322503A370089371A /* AdapterProtocol.swift in Sources */,
65496FB8211C323F00511D8A /* TableViewContainerCell.swift in Sources */,
65496FB9211C323F00511D8A /* TableViewAdapter.swift in Sources */,
65E3ECB02113598700869DF3 /* UIKit+BentoCollectionView.swift in Sources */,
Expand All @@ -712,6 +722,7 @@
65A69EDF218B8892005D90AC /* UICollectionViewExtensions.swift in Sources */,
58BA7584201633CC0050D5F1 /* Renderable.swift in Sources */,
65020C322203186400DC8F42 /* NativeView.swift in Sources */,
5B02B99422503A370089371A /* AdapterStore.swift in Sources */,
5857BECA2056F02C0085EB9C /* If.swift in Sources */,
A9509FB12CAD89179FAA03B0 /* Box.swift in Sources */,
582D9987217F87B100C67B0D /* ComponentLifecycleAware.swift in Sources */,
Expand Down Expand Up @@ -740,6 +751,7 @@
58FC4427207CF2BB00DA3614 /* AnyRenderableTests.swift in Sources */,
58FC4428207CF2BB00DA3614 /* TestId.swift in Sources */,
58D27BA721B83B2700DC9600 /* DeletableTests.swift in Sources */,
653D460B2256665000CF3E4C /* AdapterStoreTests.swift in Sources */,
58FC4429207CF2BB00DA3614 /* TestRenderable.swift in Sources */,
5830C5E721F22DDC0029044B /* ComponentLifecycleAware.swift in Sources */,
740921B620ACDDDA00B59F5C /* IfTests.swift in Sources */,
Expand Down
31 changes: 31 additions & 0 deletions Bento/Adapters/AdapterProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/// Provide generic parameter agnostic access to the adapter.
internal protocol AdapterStoreAccessible: AnyObject {
var layoutMargins: UIEdgeInsets { get set }
var boundSize: CGSize { get set }
var cachesSizeInformation: Bool { get set }
}

/// Provide a default implementation for all adapter store owners.
internal protocol AdapterStoreOwner: AdapterStoreAccessible {
associatedtype SectionID: Hashable
associatedtype ItemID: Hashable

var store: AdapterStore<SectionID, ItemID> { get set }
}

extension AdapterStoreOwner {
var layoutMargins: UIEdgeInsets {
get { return store.layoutMargins }
set { store.layoutMargins = newValue }
}

var boundSize: CGSize {
get { return store.boundSize }
set { store.boundSize = newValue }
}

var cachesSizeInformation: Bool {
get { return store.cachesSizeInformation }
set { store.cachesSizeInformation = newValue }
}
}
207 changes: 207 additions & 0 deletions Bento/Adapters/AdapterStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import FlexibleDiff

/// The adapter store which carries the component tree, and also cached sizing information if enabled.
///
/// The adapter computes sizing information lazily upon request, but it adjusts spaces and performs cache invalidation
/// every time a diff is applied.
///
/// - important: While the use-as-you-go Bento uses the adapter, the size caching capability should only be enabled in
/// `SizeCachingTableView` and `SizeCachingCollectionView`, since there are specific messages that Bento
/// need to intercept for size caching to function as expected.
struct AdapterStore<SectionID: Hashable, ItemID: Hashable> {
private(set) var sections: [Section<SectionID, ItemID>] = []

var sizingStrategy: SizingStrategy = .fillHorizontally

var cachesSizeInformation: Bool = false {
didSet {
if cachesSizeInformation && info.isEmpty {
resetCachedInfo()
} else {
info = []
}
}
}

var boundSize: CGSize = .unknown {
didSet {
if cachesSizeInformation && oldValue != boundSize {
resetCachedInfo()
}
}
}

var layoutMargins: UIEdgeInsets = .zero {
didSet {
if cachesSizeInformation && oldValue != layoutMargins {
resetCachedInfo()
}
}
}

/// All cached information. If it is empty, it means either there is no item, or the cache structure hasn't been
/// setup yet.
private var info: [SectionInfo] = []

init() {}

mutating func size(for supplement: Supplement, inSection section: Int) -> SupplementSizingResult {
guard cachesSizeInformation else { return .cachingDisabled }
guard let component = sections[section].supplements[supplement] else { return .doesNotExist }

let knownSize = info[section].supplements[supplement, default: .unknown]

if knownSize == .unknown {
let size = sizingStrategy.size(
of: component,
boundSize: boundSize,
layoutMargins: layoutMargins
)

info[section].supplements[supplement] = size
return .size(size)
} else {
return .size(knownSize)
}
}

mutating func size(forItemAt indexPath: IndexPath) -> CGSize? {
guard cachesSizeInformation else { return nil }

let knownSize = info[indexPath.section].itemSizes[indexPath.item]

if knownSize == .unknown {
let size = sizingStrategy.size(
of: sections[indexPath.section].items[indexPath.item].component,
boundSize: boundSize,
layoutMargins: layoutMargins
)

info[indexPath.section].itemSizes[indexPath.item] = size
return size
} else {
return knownSize
}
}

mutating func removeItem(at indexPath: IndexPath) {
sections[indexPath.section].items.remove(at: indexPath.row)
}

mutating func update(with sections: [Section<SectionID, ItemID>], knownSupplements: Set<Supplement>, changeset: SectionedChangeset? = nil) {
self.sections = sections

guard cachesSizeInformation else { return }

guard let changeset = changeset else {
resetCachedInfo()
return
}

info.applyIgnoringMutation(
changeset.sections,
newElement: SectionInfo(),
whenInserted: { info, index in
info.itemSizes = Array(repeating: .unknown, count: sections[index].items.count)
}
)

// Apply changeset to the old section info for all mutated sections.
for mutatedSection in changeset.mutatedSections {
let index = mutatedSection.destination

// NOTE: When layout equivalence is implemented, we need to update this to avoid not invalidating entries
// when layout is declared not to have changed.
info[index].supplements = [:]

info[index].apply(mutatedSection.changeset)
}
}

mutating func resetCachedInfo() {
info = Array(repeating: SectionInfo(), count: sections.count)
for index in info.indices {
info[index].itemSizes = Array(repeating: .unknown, count: sections[index].items.count)
}
}
}

enum SupplementSizingResult: Equatable {
case doesNotExist
case cachingDisabled
case size(CGSize)
}

enum SizingStrategy {
case fillHorizontally
case fillVertically
case compressed

func size(of component: AnyRenderable, boundSize: CGSize, layoutMargins: UIEdgeInsets) -> CGSize {
switch self {
case .fillHorizontally:
return component.sizeBoundTo(width: boundSize.width, inheritedMargins: layoutMargins)
case .fillVertically:
return component.sizeBoundTo(height: boundSize.height, inheritedMargins: layoutMargins)
case .compressed:
return component.sizeBoundTo(size: UIView.layoutFittingCompressedSize, inheritedMargins: layoutMargins)
}
}
}

private struct SectionInfo {
var supplements: [Supplement: CGSize] = [:]
var itemSizes: [CGSize] = []

init() {}

mutating func apply(_ changeset: Changeset) {
itemSizes.applyIgnoringMutation(changeset, newElement: .unknown, whenInserted: { _, _ in })

for index in changeset.mutations {
itemSizes[index] = .unknown
}

for move in changeset.moves where move.isMutated {
itemSizes[move.destination] = .unknown
}
}
}

extension CGSize {
/// A sentinel size representing unknown size. This is not known to be generated by Auto Layout, and the logical
/// size is either zero or of positive decimals.
fileprivate static var unknown: CGSize {
return CGSize(width: -.infinity, height: -.infinity)
}
}

extension Array {
mutating func applyIgnoringMutation(_ changeset: Changeset, newElement: Element, whenInserted: (inout Element, _ index: Index) -> Void) {
let old = self

let allRemovals = changeset.removals
.union(IndexSet(changeset.moves.lazy.map { $0.source }))
let allInsertions = changeset.inserts
.union(IndexSet(changeset.moves.lazy.map { $0.destination }))

// Remove all entries for removed sections, and moved sections at their original position.
for range in allRemovals.rangeView.reversed() {
removeSubrange(range)
}

// Create all entries for newly inserted sections, and moved sections at their new position.
for range in allInsertions.rangeView {
insert(contentsOf: repeatElement(newElement, count: range.count), at: range.lowerBound)

for index in range {
whenInserted(&self[index], index)
}
}

// Copy over the old section info for moved sections.
for move in changeset.moves {
self[move.destination] = old[move.source]
}
}
}
Loading

0 comments on commit d846848

Please sign in to comment.