Skip to content
Open
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
771 changes: 771 additions & 0 deletions docs/ARCHITECTURE.md

Large diffs are not rendered by default.

Binary file added docs/diagrams/sequence-cell-rendering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions docs/diagrams/sequence-cell-rendering.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@startuml sequence-cell-rendering
title Rendering a cell — UICollectionView datasource call forwarded to a SectionController

skinparam maxMessageSize 260
skinparam wrapWidth 260

actor "App User" as user
participant "UICollectionView\n(UIKit)" as cv
participant "ListCollectionViewAdapter" as adapter
participant "Section" as section
participant "SectionController\n(dataSource)" as controller
participant "MainCollectionViewContext" as context
participant "ErrorHandler" as errors

user -> cv : Scrolls
activate cv

cv -> adapter : numberOfSections(in:)
activate adapter
adapter --> cv : sections.count
deactivate adapter

cv -> adapter : collectionView(_:numberOfItemsInSection:)
activate adapter
adapter -> adapter : dataSource(at: section)\ncontroller(at:)?.dataSource
adapter -> section : controller
activate section
section --> adapter : SectionController\n(lazily created on first access)
deactivate section
alt dataSource resolved
adapter -> controller : numberOfItems(in: context)
activate controller
controller --> adapter : Int
deactivate controller
else no controller at that index
adapter -> errors : nonCritical(.missingDataSource(section:))
adapter --> cv : 0
end
adapter --> cv : number of items
deactivate adapter

cv -> adapter : collectionView(_:cellForItemAt:)
activate adapter
alt indexPath.isValid == false
adapter -> errors : nonCritical(.invalidIndexPath)
adapter --> cv : UICollectionViewCell()
else valid
adapter -> adapter : SectionIndexPath(indexPath)\n(collection-view index + section-local index)
adapter -> controller : cellForItem(at: sectionIndexPath, in: context)
activate controller
controller -> context : dequeueReusableCell(Cell.self, for: indexPath)
activate context
alt cell type not registered yet
context -> cv : register(cellType, forCellWithReuseIdentifier:)
context -> context : registeredCellTypes.insert(identifier)
end
context -> cv : dequeueReusableCell(withReuseIdentifier:for:)
cv --> context : UICollectionReusableView
alt downcast fails
context -> errors : nonCritical(.dequeuedViewHasNotTheCorrectType)
context --> controller : Cell()
else
context --> controller : Cell
end
deactivate context
controller -> controller : Configure cell from items[indexPath]
controller --> adapter : UICollectionViewCell
deactivate controller
adapter --> cv : UICollectionViewCell
end
deactivate adapter

cv --> user : Renders cell
deactivate cv

note over adapter, context
All types are @MainActor. The adapter never owns cell configuration:
it resolves the SectionController for the IndexPath and forwards.
Supplementary views follow the same path via
viewForSupplementaryElementOfKind -> headerView / footerView.
end note

@enduml
Binary file added docs/diagrams/sequence-item-diffing-update.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions docs/diagrams/sequence-item-diffing-update.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@startuml sequence-item-diffing-update
title Animated item update inside one section — DiffingListSectionController

skinparam maxMessageSize 260
skinparam wrapWidth 260

participant "Caller\n(app code / binding)" as caller
participant "DiffingListSectionController\n(: ListSectionController)" as controller
participant "StagedChangeset\n(DifferenceKit)" as diff
participant "CollectionViewSectionUpdate" as update
participant "MainCollectionViewContext" as context
participant "ListCollectionViewAdapter" as adapter
participant "UICollectionView\n(UIKit)" as cv

caller -> controller : items = newItems
activate controller

alt context == nil (not attached to an adapter)
controller -> controller : collectionViewItems = newItems
controller --> caller : (no collection view work)
else attached
controller -> controller : calculateUpdate(from: collectionViewItems, to: newItems)
controller -> diff : StagedChangeset(source: oldData, target: newData)
activate diff
diff --> controller : [Changeset]
deactivate diff
controller -> controller : shouldAnimateItems(from:to:)
controller -> update : init(controller: self,\nbatchOperations: changeSet.map(\\.sectionBatchOperation),\nshouldAnimate:, setData:,\nshouldReload: { $0.count > 100 })
activate update
update --> controller : CollectionViewSectionUpdate<[Item]>
deactivate update

controller -> context : apply(update:)
activate context
alt adapter == nil
context -> context : errorHandler.nonCritical(.adapterIsNotSetOnContext)
context -> cv : reloadData()
else
context -> adapter : index(of: update.controller)
activate adapter
adapter --> context : Int?
deactivate adapter
alt controller not found in adapter.sections
context -> context : errorHandler.nonCritical(.adapterDoesNotContainSectionController)
context -> cv : reloadData()
else index found
context -> cv : apply(update:, at: index)
activate cv
alt collectionView.window == nil
cv -> update : setData(batchOperation.data)
cv -> cv : reloadData()
cv -> caller : completion?(false)
else shouldAnimate == false
cv -> update : setData(batchOperation.data)
cv -> cv : UIView.performWithoutAnimation { reloadSections(index) }
cv -> caller : completion?(false)
else animated
loop for each batchOperation
cv -> cv : performBatchUpdates {
cv -> update : setData(batchOperation.data)
alt shouldReload(batchOperation) == true (>100 changes)
cv -> cv : reloadSections(IndexSet(integer: index))
else
cv -> cv : deleteItems(at:) — descending order
cv -> cv : insertItems(at:) — ascending order
cv -> cv : moveItem(at:to:) per move
cv -> cv : reloadItems(at:)
end
cv -> cv : }
cv -> caller : completion?(finished)
end
end
deactivate cv
end
end
deactivate context
end
deactivate controller

note over controller, cv
ListSectionController.items is a computed property over collectionViewItems.
setData is invoked *inside* performBatchUpdates so the backing array and the
UICollectionView mutate atomically.
end note

@enduml
Binary file added docs/diagrams/sequence-section-list-update.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions docs/diagrams/sequence-section-list-update.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@startuml sequence-section-list-update
title Updating the list of sections — invalidateDataSource and controller reuse

skinparam maxMessageSize 260
skinparam wrapWidth 260

participant "ViewController\n(: ListCollectionViewAdapterDataSource)" as vc
participant "ListCollectionViewAdapter" as adapter
participant "New Section values" as newSections
participant "Existing SectionController" as oldController
participant "CollectionViewUpdate" as update
participant "MainCollectionViewContext" as context
participant "UICollectionView\n(UIKit)" as cv

vc -> adapter : invalidateDataSource()
activate adapter
adapter -> vc : sections(for: self)
activate vc
vc --> adapter : [Section]
deactivate vc

adapter -> adapter : sections = newValue

group filterDuplicateSectionIds
adapter -> adapter : sections.unique(by: \\.id)
alt duplicates found
adapter -> context : errorHandler.nonCritical(.duplicateSectionIds(ids))
note right of adapter : Duplicates are dropped, the first occurrence wins
end
end

group Controller reuse by section id
loop for each new Section
adapter -> adapter : existingSections[newSection.id]
alt id already displayed
adapter -> newSections : controller = existingController
note right of adapter
The new Section reuses the live controller so
cell state and scroll position survive the update
end note
adapter -> oldController : didUpdate(model: newSection.model)
activate oldController
oldController -> oldController : Downcast model to Model,\nelse errorHandler.nonCritical(.sectionControllerModelTypeMismatch)
oldController -> oldController : model = model → items = items(for: model)\n(triggers the per-section update flow)
deactivate oldController
else new id
note right of adapter : Section keeps its own lazily created controller
end
end
end

adapter -> adapter : calculateUpdate(from: collectionViewSections,\nto: uniqueNewSections)
alt ListCollectionViewAdapter (base)
adapter -> update : CollectionViewUpdate(data:setData:)\n→ shouldReload always true
else DiffingListCollectionViewAdapter (override)
adapter -> update : StagedChangeset(source:target:)\n→ batchOperations, shouldReload { $0.count > 100 }
end
activate update
update --> adapter : CollectionViewUpdate<[Section]>?
deactivate update

alt calculateUpdate returned nil
adapter -> adapter : collectionViewSections = uniqueNewSections
else
adapter -> context : apply(update:)
activate context
context -> cv : apply(update:)
activate cv
alt window == nil or shouldReload(batchOperation)
cv -> update : setData(batchOperation.data)
cv -> cv : reloadData()
cv -> vc : completion?(false)
else
loop for each batchOperation
cv -> cv : performBatchUpdates {
cv -> update : setData(batchOperation.data)
cv -> cv : deleteSections / insertSections /\nmoveSection / reloadSections
cv -> cv : }
cv -> vc : completion?(finished)
end
end
deactivate cv
deactivate context
end

note over adapter, context
Setting collectionViewSections clears context on the outgoing controllers
and assigns the shared context to the incoming ones, which is how a
SectionController gains the ability to apply its own updates.
end note

deactivate adapter

@enduml
Binary file added docs/diagrams/structurizr-Containers-key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions docs/diagrams/structurizr-Containers-key.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@startuml

set separator none
hide stereotype

<style>
root {
BackgroundColor: #ffffff;
FontColor: #444444;
}
// Element,Container
.Element-RWxlbWVudCxDb250YWluZXI= {
BackgroundColor: #438dd5;
LineColor: #2e6295;
LineStyle: 0;
LineThickness: 2;
FontColor: #ffffff;
FontSize: 24;
HorizontalAlignment: center;
Shadowing: 0;
MaximumWidth: 200;
}
// Element,Person
.Element-RWxlbWVudCxQZXJzb24= {
BackgroundColor: #08427b;
LineColor: #052e56;
LineStyle: 0;
LineThickness: 2;
FontColor: #ffffff;
FontSize: 24;
HorizontalAlignment: center;
Shadowing: 0;
MaximumWidth: 200;
}
// Element,Software System,External
.Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXh0ZXJuYWw= {
BackgroundColor: #999999;
LineColor: #6b6b6b;
LineStyle: 0;
LineThickness: 2;
FontColor: #ffffff;
FontSize: 24;
HorizontalAlignment: center;
Shadowing: 0;
MaximumWidth: 200;
}
// Relationship
.Relationship-UmVsYXRpb25zaGlw {
LineThickness: 2;
LineStyle: 10-10;
LineColor: #444444;
FontColor: #444444;
FontSize: 24;
}
// SectionKit
.Boundary-U2VjdGlvbktpdA== {
BackgroundColor: #ffffff;
LineColor: #0b4884;
LineStyle: 0;
LineThickness: 2;
FontColor: #ffffff;
FontSize: 24;
HorizontalAlignment: center;
Shadowing: 0;
}
// transparent element for relationships in legend
.Element-Transparent {
BackgroundColor: transparent;
LineColor: transparent;
FontColor: transparent;
}
</style>

rectangle "==Container" <<Element-RWxlbWVudCxDb250YWluZXI=>>

person "==Person" <<Element-RWxlbWVudCxQZXJzb24=>>

rectangle "==Software System, External" <<Element-RWxlbWVudCxTb2Z0d2FyZSBTeXN0ZW0sRXh0ZXJuYWw=>>

rectangle "." <<.Element-Transparent>> as 1
1 --> 1 <<Relationship-UmVsYXRpb25zaGlw>> : "Relationship"

@enduml
Binary file added docs/diagrams/structurizr-Containers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading