diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..99e0828 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,771 @@ +# SectionKit — System Architecture Document + +## Document Metadata + +| Field | Value | +|-------|-------| +| **Document ID** | `[PENDING CLARIFICATION — no IA.XX.YY identifier present in the repository; needs the Confluence IA record for "sectionkit"]` | +| **System Name** | SectionKit | +| **Asset Classification** | `[PENDING CLARIFICATION — not recorded in the repository; the only machine-readable record is catalog-info.yaml, which carries no classification field]` | +| **Repository** | https://github.com/traderepublic/SectionKit | +| **Owner** | `element-managed-products` (per catalog-info.yaml); code review ownership is `@traderepublic/atom-mobile-platform-ios` (per CODEOWNERS). See Critical Discrepancies. | +| **Document Owner** | `[PENDING CLARIFICATION — no document owner recorded in the repository]` | +| **Last Review Date** | 2026-09-14 | +| **Status** | Draft | +| **Confidentiality** | Public — the repository is a public open-source project distributed via SwiftPM and Carthage, and LICENSE.md is present. | + +### Related Documents + +| Relationship | Document | +|--------------|----------| +| Parent System | `[PENDING CLARIFICATION — no parent system recorded; catalog-info.yaml declares no system or subcomponentOf]` | +| Child Systems | None. The repository builds two library products (`SectionKit`, `DiffingSectionKit`) and one demo app; no child services exist. | +| Key ADRs | None. No ADR directory or decision-record files exist in the repository. | +| Runbooks | Not applicable — SectionKit is a client-side library with no runtime operation. See Missing/Unknown Infrastructure. | + +--- + +## 1. Overview + +### What It Does + +SectionKit is a Swift library for UIKit that lets every section of a `UICollectionView` be implemented as a separate, self-contained object. A host app hands the library a list of `Section` values — each pairing a stable identifier, a model and a `SectionController` — and the library takes over the collection view's datasource and delegate roles, forwarding every callback to the controller that owns the relevant `IndexPath`. + +`DiffingSectionKit` is a separate, optional product that adds DifferenceKit-backed animated diffing on top of the core library. + +_Sources: README, Package.swift, CollectionViewAdapter, Section, SectionController._ + +### Why It Exists + +**The Problem**: A `UICollectionView` has exactly one datasource and one delegate. On a screen with many heterogeneous sections, every `numberOfItemsInSection`, `cellForItemAt`, `didSelectItemAt` and `sizeForItemAt` callback funnels into one object, which grows into a large `switch` over section indices. The README states that Trade Republic screens contain up to 30 different section types. + +**The Solution**: Insert an adapter between the collection view and the app. The adapter owns the single UIKit datasource/delegate conformance and resolves, per `IndexPath`, which `SectionController` should answer. Each section becomes a small class that can be developed, reused and unit-tested in isolation. The README names IGListKit as the inspiration and type-safe generics as the differentiator. + +_Sources: README, ListCollectionViewAdapter+UICollectionViewDataSource, SectionController._ + +### How It Works (Simplified) + +1. The host view controller creates a `ListCollectionViewAdapter` (many sections) or a `SingleSectionCollectionViewAdapter` (one section) over its `UICollectionView`. The adapter assigns itself as the collection view's `dataSource` and `delegate`. +2. The app supplies `[Section]` — either eagerly through an initialiser or lazily through a datasource protocol that the adapter re-queries on `invalidateDataSource()`. Each `Section` carries a unique `id`, an untyped model and a closure producing its `SectionController`. +3. UIKit calls the adapter. The adapter validates the `IndexPath`, looks up the controller for that section, converts the `IndexPath` into a `SectionIndexPath` (which exposes both the collection-view index and the section-local index), and forwards the call. +4. The controller configures cells by dequeuing them from the shared `CollectionViewContext`, which registers each cell class on first use. +5. When a controller's items change, it computes a `CollectionViewSectionUpdate` describing deletes, inserts, moves and reloads, and asks the context to apply it. The context resolves the controller's current section index and translates the update into `performBatchUpdates`, `reloadSections` or `reloadData`. + +_Sources: ListCollectionViewAdapter, SingleSectionCollectionViewAdapter, ListCollectionViewAdapter+UICollectionViewDataSource, SectionIndexPath, MainCollectionViewContext, UICollectionView+Apply, ListSectionController._ + +### Key Concepts + +| Term | Definition | +|------|------------| +| `Section` | A value object binding a unique `AnyHashable` id, an untyped `Any` model, and a lazily created `SectionController`. | +| `SectionController` | The owner of one section: it supplies that section's datasource, delegate, flow delegate, drag/drop delegates and layout provider. | +| `CollectionViewAdapter` | The single object that UIKit sees as the collection view's datasource and delegate; it forwards each call to the responsible `SectionController`. | +| `CollectionViewContext` | The shared object a controller uses to reach the collection view, dequeue registered cells, report errors and apply its own updates. | +| `SectionIndexPath` | A pair holding both the collection-view-wide `IndexPath` and the section-local item index, so controllers can index their own arrays. | +| Section id | The identity used to recycle a live `SectionController` across an update. It must be unique within one list and stable across updates, otherwise the controller is recreated and animation state is lost. | +| `CollectionViewUpdate` / `CollectionViewSectionUpdate` | Descriptions of a change — batch operations plus a `setData` closure and a `shouldReload` predicate — that the library translates into UIKit batch-update calls. | + +_Sources: Section, SectionController, CollectionViewAdapter, CollectionViewContext, SectionIndexPath, ListCollectionViewAdapter, CollectionViewUpdate, CollectionViewSectionUpdate, README._ + +--- + +## 2. Context & Scope + +### System Context Diagram + +![System context](diagrams/structurizr-SystemContext.png) + +### Stakeholders + +| Stakeholder | Role | Concern | +|-------------|------|---------| +| iOS engineers integrating SectionKit | User | A section API that stays small and testable; correct `IndexPath` forwarding; predictable animation behaviour. | +| App users of a host application | Indirect user | Correct rendering, smooth scrolling and correct animated updates in the collection view. | +| `element-managed-products` | Declared owner in catalog-info.yaml | Component lifecycle, marked `production`. | +| `@traderepublic/atom-mobile-platform-ios` | Code owner (CODEOWNERS) | Reviews every change to the repository — CODEOWNERS applies `*`. | +| External open-source consumers | User | Public SwiftPM/Carthage distribution and release tags. | + +_Sources: catalog-info.yaml, CODEOWNERS, README, LICENSE.md._ + +### Business Constraints + +| Constraint | Description | Impact on Architecture | +|------------|-------------|----------------------| +| Public open-source distribution | The repository is public, carries a LICENSE.md, and README documents SwiftPM and Carthage installation. | The public API surface is a compatibility contract; most types are `open` so consumers can subclass and override. | +| No third-party dependency in the core product | The `SectionKit` target declares no dependencies in Package.swift; DifferenceKit is isolated in the separate `DiffingSectionKit` target. | Diffing is opt-in; consumers who cannot take DifferenceKit can still use the core library or the `FoundationDiffingListSectionController` built on Swift's own `CollectionDifference`. | +| Carthage support | README instructs consumers to build with `--use-xcframeworks` because the xcframework variant of DifferenceKit is linked. | Three parallel Xcode projects and a Cartfile are maintained alongside Package.swift. | +| UIKit-only | Every adapter and controller is built around `UICollectionView` and its delegate protocols. | The library is not usable from SwiftUI or AppKit; it is `@MainActor`-isolated throughout. | + +_Sources: Package.swift, Cartfile, README, LICENSE.md, SectionKit.xcodeproj, DiffingSectionKit.xcodeproj._ + +--- + +## 3. Architecture + +### Container Diagram + +![Containers](diagrams/structurizr-Containers.png) + +### Core Component Diagram + +![Core components](diagrams/structurizr-CoreComponents.png) + +### Component Description + +#### 3.1 `SectionKit` (core library) + +| Attribute | Value | +|-----------|-------| +| **Description** | The core product: collection view adapters, section controllers, the context abstraction, the update model and error handling. Declares no third-party dependencies. | +| **Technology** | Swift 5 (`swift-tools-version:5.5`, `swiftLanguageVersions: [.v5]`), UIKit; `.iOS(.v9)` declared in Package.swift | +| **Repository** | https://github.com/traderepublic/SectionKit — sources under `SectionKit/Sources` | +| **Deployment** | Linked into a host iOS app as a Swift package product or a Carthage-built xcframework. No server-side deployment. | + +**Data Handled**: + +| Data Type | Classification | Storage | +|-----------|---------------|---------| +| Section models (`Section.model`, typed as `Any`) | Caller-defined; the library does not interpret them | In memory only, retained by `Section` for the lifetime of the adapter | +| Section item arrays (`ListSectionController.collectionViewItems`) | Caller-defined | In memory only | +| Registered cell/supplementary reuse identifiers | Internal | In memory, `Set` per `MainCollectionViewContext` | + +The library persists nothing. It has no database, file, keychain or network access. + +**Interfaces Exposed**: + +| Interface | Type | Consumers | Documentation | +|-----------|------|-----------|---------------| +| `CollectionViewAdapter` | Swift protocol (`context`, `scrollViewDelegate`, `sections`, `invalidateDataSource()`) | Host app | Doc comments in CollectionViewAdapter | +| `ListCollectionViewAdapter`, `SingleSectionCollectionViewAdapter` | `open` classes; each conforms to `UICollectionViewDataSource`, `UICollectionViewDelegate`, `UICollectionViewDelegateFlowLayout`, `UICollectionViewDataSourcePrefetching`, `UICollectionViewDragDelegate`, `UICollectionViewDropDelegate` and `UIScrollViewDelegate` in a separate extension file per protocol | Host app, UIKit | Doc comments; per-protocol source files | +| `ListCollectionViewAdapterDataSource`, `SingleSectionCollectionViewAdapterDataSource` | Swift protocols returning `[Section]` / `Section?` | Host app implements | ListCollectionViewAdapterDataSource | +| `SectionController` plus `SectionDataSource`, `SectionDelegate`, `SectionFlowDelegate`, `SectionDragDelegate`, `SectionDropDelegate`, `SectionDataSourcePrefetchingDelegate` | Swift protocols with default implementations | Host app implements or subclasses | SectionController and the section protocol files | +| `BaseSectionController`, `ListSectionController`, `SingleItemSectionController`, `SingleModelSectionController`, `FoundationDiffingListSectionController` | `open` base classes | Host app subclasses | BaseSectionController, ListSectionController, SingleItemSectionController, SingleModelSectionController, FoundationDiffingListSectionController | +| `CollectionViewContext` | Swift protocol: cell/header/footer dequeuing, `apply(update:)`, `index(of:)`, `sectionControllerWithAdjustedIndexPath(for:)`, plus `containerSize` / `insetContainerSize` extensions | `SectionController` implementations | CollectionViewContext, CollectionViewContext+Size | +| `ErrorHandling` / `Error` / `Error.Severity` | Swift protocol and enums | Host app may substitute its own handler at adapter init | ErrorHandling, Error | +| `SectionKitCompositionalLayout` | `final` `UICollectionViewCompositionalLayout` subclass, iOS 13+ | Host app | SectionKitCompositionalLayout | +| `SectionLayoutProvider` | Enum with `.flowLayout(FlowLayoutProvider?)` and `.compositionalLayout(CompositionalLayoutProvider)` | `SectionController` implementations | SectionController | + +**Dependencies**: None internal to this repository. `SectionKit` sits at the bottom of the dependency graph; every other target depends on it. + +_Sources: Package.swift, CollectionViewAdapter, ListCollectionViewAdapter, SingleSectionCollectionViewAdapter, ListCollectionViewAdapterDataSource, SectionController, BaseSectionController, ListSectionController, SingleItemSectionController, SingleModelSectionController, FoundationDiffingListSectionController, CollectionViewContext, CollectionViewContext+Size, MainCollectionViewContext, ErrorHandling, Error, SectionKitCompositionalLayout, Section._ + +##### Key internal mechanisms + +**Delegate forwarding.** Each UIKit protocol conformance lives in its own extension file per adapter (`+UICollectionViewDataSource`, `+UICollectionViewDelegate`, `+UICollectionViewDelegateFlowLayout`, `+UICollectionViewDataSourcePrefetching`, `+UICollectionViewDragDelegate`, `+UICollectionViewDropDelegate`, `+UIScrollViewDelegate`). Every handler follows the same shape: validate the `IndexPath`, resolve the per-section delegate via the `+Convenience` accessors, and forward — or report a `.missingDataSource` / `.invalidIndexPath` error and return a neutral value (`0`, `false`, a bare `UICollectionViewCell()`). `UIScrollViewDelegate` callbacks are not per-section; they are forwarded to a single `scrollViewDelegate` on the adapter. + +**Section identity and controller recycling.** Setting `sections` on `ListCollectionViewAdapter` first drops duplicate ids (reporting `.duplicateSectionIds`, keeping the first occurrence), then builds a dictionary of the currently displayed sections by id. For every incoming section whose id is already displayed, the live controller is transplanted onto the new `Section` and `didUpdate(model:)` is called on it. This is why the README requires ids to be both unique and stable. Setting `collectionViewSections` clears `context` on outgoing controllers and assigns the shared context to incoming ones — a controller can only apply its own updates while it holds a context. + +**Layout provider.** `BaseSectionController.layoutProvider` defaults to `.flowLayout(self)`, so a controller is its own flow delegate unless it overrides. The adapters resolve `flowDelegate(at:)` from `layoutProvider.flowLayoutProvider` on iOS 13+ and from the legacy `flowDelegate` property below that. The `sizeForItem(at:using:in:)` family on `BaseSectionController` is marked deprecated from iOS 13 with the message "Please use the layoutProvider with the flowLayout type". + +**Compositional layout.** `SectionKitCompositionalLayout` holds a `sections` closure that the adapter injects at init. Its section provider reads `sections[index].controller.layoutProvider` and returns `.empty` after an `assertionFailure` if the index is out of bounds or the provider is not `.compositionalLayout`. All three inherited initialisers are marked `@available(*, unavailable)`. + +_Sources: ListCollectionViewAdapter, ListCollectionViewAdapter+Convenience, ListCollectionViewAdapter+UICollectionViewDataSource, ListCollectionViewAdapter+UICollectionViewDelegate, SingleSectionCollectionViewAdapter, BaseSectionController, SectionController, SectionKitCompositionalLayout, README._ + +#### 3.2 `DiffingSectionKit` + +| Attribute | Value | +|-----------|-------| +| **Description** | Optional product adding DifferenceKit-based diffing: `DiffingListSectionController`, `ManualDiffingListSectionController` and `DiffingListCollectionViewAdapter`, each overriding only `calculateUpdate(from:to:)`. | +| **Technology** | Swift 5, UIKit, DifferenceKit | +| **Repository** | Same repository — sources under `DiffingSectionKit/Sources` | +| **Deployment** | Optional additional product linked into the host app | + +**Interfaces Exposed**: + +| Interface | Type | Consumers | Documentation | +|-----------|------|-----------|---------------| +| `DiffingListSectionController` | `open` class extending `ListSectionController` | Host app subclasses | DiffingListSectionController | +| `ManualDiffingListSectionController` | `open` class extending `ListSectionController`; takes `itemId` and `itemContentIsEqual` closures instead of a `Differentiable` constraint, with convenience inits for `Equatable`, `Identifiable` and both | Host app subclasses | ManualDiffingListSectionController, DifferentiableBox | +| `DiffingListCollectionViewAdapter` | `open` class extending `ListCollectionViewAdapter` | Host app | DiffingListCollectionViewAdapter | +| `Section: Differentiable` conformance | Swift extension — `differenceIdentifier` is the section `id`, and `isContentEqual` compares only ids so section content changes are delegated to the controller rather than reloading the section | DifferenceKit | Section+Differentiable | +| `Changeset.collectionBatchOperation` / `.sectionBatchOperation` | Swift extensions bridging DifferenceKit changesets to SectionKit batch operations | Internal and host app | Changeset+BatchOperations | + +**Dependencies**: + +| Dependency | Purpose | Failure Impact | +|------------|---------|----------------| +| `SectionKit` | Provides the base classes, update types and `Section` that this target extends | Compile-time dependency; the target cannot build without it | + +_Sources: Package.swift, DiffingListSectionController, ManualDiffingListSectionController, DifferentiableBox, DiffingListCollectionViewAdapter, Section+Differentiable, Changeset+BatchOperations._ + +#### 3.3 Example app and its local packages + +| Attribute | Value | +|-----------|-------| +| **Description** | A demo iOS app plus four local SwiftPM packages that exercise the library. Not part of either shipped product. | +| **Technology** | Swift 5, UIKit, iOS 14+ (`platforms: [.iOS(.v14)]` in each example manifest) | +| **Repository** | Same repository — `Example/` | +| **Deployment** | Built from `Example/SectionKitExample.xcodeproj`; not distributed | + +The app's `SceneDelegate` builds a `UINavigationController`, constructs an `ExampleListCoordinator` and pushes it. The list screen itself uses `ListCollectionViewAdapter` over a `UICollectionViewCompositionalLayout.list` configuration. + +| Package | Product(s) | External dependencies | What it demonstrates | +|---------|-----------|----------------------|----------------------| +| `Examples` | `Examples` | none | The example list screen (`ExampleListViewController`, `ExampleListViewModel`, `ExampleSectionController`) and navigation into every demo | +| `VanillaSwiftExamples` | `VanillaSwiftExamples` | swift-snapshot-testing (test target only) | Colors (`SingleSectionCollectionViewAdapter` + `ListSectionController`), Numbers (`ListCollectionViewAdapter` + multiple `ListSectionController`), Emojis (`FoundationDiffingListSectionController` refreshed via `invalidateDataSource`), Activities (`ListSectionController` with a `.compositionalLayout` provider), Names (`ListCollectionViewAdapter` over `SectionKitCompositionalLayout`) | +| `ReactiveSwiftExamples` | `ReactiveSwiftExamples` | ReactiveCocoa, ReactiveSwift, swift-snapshot-testing (test target only) | A ReactiveSwift-driven Emojis screen, plus three helpers that replace model-derived updates with a `Disposable` binding disposed in `deinit`: `BindingListSectionController` overrides `shouldUpdateItems(afterModelChangedTo:)` to return `false`, `BindingSingleItemSectionController` overrides `shouldUpdateItem(afterModelChangedTo:)`, and `BindingSingleModelSectionController` overrides `collectionViewModel` | +| `Utilities` | `Utilities`, `TestUtilities` | swift-snapshot-testing (test target only) | Shared cells and supplementary views; the `Presentable` / `NavigationSegue` navigation helpers; and the snapshot harness `SectionSnapshotContainer` / `SnapshotCollectionView` / `SnapshotCollectionViewAdapter` | + +The demo screens are navigated through a small coordinator pattern: `Presentable` has one requirement, `present(segue:)`, with `push(onto:)` and `present(on:)` conveniences, and `UIViewController` is extended to conform. + +_Sources: Example/Examples Package.swift, Example/VanillaSwiftExamples Package.swift, Example/ReactiveSwiftExamples Package.swift, Example/Utilities Package.swift, SceneDelegate, ExampleListViewController, ExampleListViewModel, ExampleSectionController, ExampleListCoordinator+Presentable, ColorsViewController, ColorsSectionController, NumbersViewController, EmojisViewController (VanillaSwift), EmojisSectionController (VanillaSwift), NamesViewController, ActivitiesSectionController, EmojisViewController (ReactiveSwift), BindingListSectionController, BindingSingleItemSectionController, BindingSingleModelSectionController, SectionSnapshotContainer, SnapshotCollectionView, SnapshotCollectionViewAdapter, Presentable, NavigationSegue._ + +--- + +## 4. Runtime Behavior + +### Key Scenarios + +#### 4.1 Rendering a cell + +**Trigger**: UIKit calls the adapter's `UICollectionViewDataSource` methods — on first layout, on scroll, or after a reload. + +**Actors**: App user, `UICollectionView`, `ListCollectionViewAdapter`, `Section`, `SectionController` (as `SectionDataSource`), `MainCollectionViewContext`, `ErrorHandler`. + +![Cell rendering](diagrams/sequence-cell-rendering.png) + +**Notes**: + +Everything in this flow is synchronous and `@MainActor`-isolated; there is no async boundary anywhere in the library. `Section.controller` is a `lazy var`, so the controller for a section is constructed on first access rather than when the `Section` is built. + +Cell registration is lazy and one-shot: `MainCollectionViewContext` keeps `registeredCellTypes`, `registeredHeaderViewTypes` and `registeredFooterViewTypes` sets keyed by reuse identifier, registers the class on the collection view when the identifier is absent, and then dequeues. If the dequeued view fails to downcast, the context reports `.dequeuedViewHasNotTheCorrectType` and returns a freshly constructed instance rather than trapping. + +Error recovery in the datasource path is uniformly "report and degrade": an invalid `IndexPath` or a missing controller yields `0`, `false`, or an empty `UICollectionViewCell()` / `UICollectionReusableView()`. An unrecognised supplementary element kind reports `.unsupportedSupplementaryViewKind`. `indexPathForIndexTitle` reports `.notImplemented()` and returns `IndexPath(item: 0, section: 0)`. + +_Sources: ListCollectionViewAdapter+UICollectionViewDataSource, ListCollectionViewAdapter+Convenience, MainCollectionViewContext, Section, SectionIndexPath, Error, ErrorHandling._ + +#### 4.2 Animated item update inside one section + +**Trigger**: App code (or a ReactiveSwift binding in the examples) assigns a new array to a controller's `items`. + +**Actors**: Caller, `DiffingListSectionController`, DifferenceKit `StagedChangeset`, `CollectionViewSectionUpdate`, `MainCollectionViewContext`, `ListCollectionViewAdapter`, `UICollectionView`. + +![Item diffing update](diagrams/sequence-item-diffing-update.png) + +**Notes**: + +`items` is a computed property over the stored `collectionViewItems`. If the controller has no `context` — it is not attached to an adapter — the setter writes straight through to `collectionViewItems` and no collection view work happens. This makes the "configure before attach" path free of side effects. + +The update is applied through three guard layers. `MainCollectionViewContext.apply(update:)` holds a `weak var adapter`; if the adapter is gone it reports `.adapterIsNotSetOnContext` and falls back to `reloadData()`. If the adapter no longer contains the controller, it reports `.adapterDoesNotContainSectionController` and again falls back to `reloadData()`. Only with a resolved section index does it call the `UICollectionView+Apply` extension. + +`UICollectionView+Apply` then decides how to animate. If `window == nil` the update is applied with `reloadData()` and completions receive `false` — an off-screen collection view is never animated. If `shouldAnimate` is `false`, the section is reloaded inside `UIView.performWithoutAnimation`. Otherwise each batch operation runs inside its own `performBatchUpdates`, and within it `shouldReload(batchOperation)` can escalate to `reloadSections`. The shipped diffing controllers set that predicate to `{ $0.count > 100 }`, so an update of more than 100 changes reloads the section instead of animating it. Deletes are applied in descending index order and inserts in ascending order. + +`setData` is invoked inside the `performBatchUpdates` block, so the backing array and the collection view change together. + +`ListSectionController` (non-diffing) instead returns an update with no batch operations, which reloads the section. `FoundationDiffingListSectionController` uses Swift's own `newData.difference(from: oldData).inferringMoves()` rather than DifferenceKit, which is why it lives in the core module but is gated `@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)`. + +_Sources: ListSectionController, DiffingListSectionController, ManualDiffingListSectionController, FoundationDiffingListSectionController, CollectionViewSectionUpdate, MainCollectionViewContext, UICollectionView+Apply, Changeset+BatchOperations._ + +#### 4.3 Updating the list of sections + +**Trigger**: The host calls `invalidateDataSource()`, or assigns a new `[Section]` to the adapter's `sections`. + +**Actors**: Host view controller (as `ListCollectionViewAdapterDataSource`), `ListCollectionViewAdapter`, existing `SectionController` instances, `CollectionViewUpdate`, `MainCollectionViewContext`, `UICollectionView`. + +![Section list update](diagrams/sequence-section-list-update.png) + +**Notes**: + +`invalidateDataSource()` is a no-op when no datasource is set; otherwise it assigns `collectionViewSections = dataSource.sections(for: self)`, which bypasses the recycling path and performs a full reload. Assigning `sections` is the path that recycles controllers by id and diffs. + +The base `ListCollectionViewAdapter.calculateUpdate(from:to:)` returns `CollectionViewUpdate(data:setData:)` with no batch operations, whose `shouldReload` therefore always answers `true` — the base adapter reloads rather than animates section-level changes. `DiffingListCollectionViewAdapter` overrides it to produce DifferenceKit batch operations with `shouldReload: { $0.count > 100 }`. `SingleSectionCollectionViewAdapter` computes its own deletes/inserts/reloads for the four transitions between "no section" and "a section". + +Because `Section.isContentEqual(to:)` in DiffingSectionKit compares only ids, a section whose model changed is not reloaded by the section-level diff. The model change instead reaches the recycled controller through `didUpdate(model:)`, which triggers that controller's own item-level update. + +`didUpdate(model:)` performs a runtime downcast. On mismatch, the generic controllers report `.sectionControllerModelTypeMismatch(expected:actual:)` and return without updating — a typed controller paired with the wrong model silently stops updating in release builds. + +_Sources: ListCollectionViewAdapter, SingleSectionCollectionViewAdapter, DiffingListCollectionViewAdapter, CollectionViewUpdate, UICollectionView+Apply, Section+Differentiable, ListSectionController, SingleItemSectionController, SingleModelSectionController, Error._ + +### Event Flows + +Not applicable. The library uses no message broker, event bus or pub/sub topic. All communication is synchronous in-process Swift method dispatch on the main actor. The only "event-like" mechanisms present are UIKit delegate callbacks (Section 4.1) and, in the example app only, ReactiveSwift `Signal`/`Property` bindings. + +_Sources: Package.swift, CollectionViewAdapter, BindingListSectionController._ + +--- + +## 5. Deployment & Infrastructure + +SectionKit is a client-side library. It has no servers, containers, clusters, databases or cloud resources. "Deployment" means being resolved as a dependency and compiled into a host iOS application binary. + +### Deployment Diagram + +No deployment diagram is provided: there is no deployment topology to draw. The container diagram in Section 3 already shows the full set of build artefacts and how they link together. + +### Environment Configuration + +There are no production/staging/development environments. The build-time configuration that does exist: + +| Aspect | Value | Source | +|--------|-------|--------| +| Swift tools version | `5.5` | Package.swift | +| Swift language version | `.v5` | Package.swift | +| Declared SwiftPM platform | `.iOS(.v9)` | Package.swift | +| Xcode framework deployment target | `IPHONEOS_DEPLOYMENT_TARGET = 9.0` | SectionKit.xcodeproj | +| Example app deployment target | `IPHONEOS_DEPLOYMENT_TARGET = 14.5` | SectionKit.xcodeproj | +| Example packages platform | `.iOS(.v14)` | Example package manifests | +| CI runner | `macos-15` | unittests.yml | +| CI toolchain | `DEVELOPER_DIR=/Applications/Xcode_16.4.app/Contents/Developer` | unittests.yml | +| CI test device | `iPhone 16` simulator | Fastfile | +| Debug-only behaviour | `assertionFailure` for non-critical errors is inside `#if DEBUG` | ErrorHandling | + +No feature-flag system, remote configuration or environment-variable-driven runtime behaviour exists in the library. + +### Build & Release Pipeline + +| Stage | Definition | Detail | +|-------|-----------|--------| +| Trigger | unittests.yml | `pull_request` on `ready_for_review`, `opened`, `synchronize`, `reopened`; skipped while the PR is a draft | +| Dependency resolution | unittests.yml | `carthage bootstrap --platform iOS --no-use-binaries --use-xcframeworks`, cached on a hash of Cartfile.resolved | +| Ruby tooling | Gemfile | `bundle install` — danger, danger-todoist, danger-swiftlint, danger-xcov, fastlane | +| Tests | Fastfile | `bundle exec fastlane unittest` runs `scan` twice — once for the `SectionKit` scheme, once for `DiffingSectionKit` — both with code signing disabled | +| Lint configuration | .swiftlint.yml | `only_rules` allowlist; the danger-swiftlint gem is declared in the Gemfile | +| Release | git tags | Latest tag in the repository is `2.1.4` | + +The Danger gems are declared in the Gemfile but no `Dangerfile` and no workflow step invoking Danger exists in the repository, so Danger does not currently run. + +_Sources: Package.swift, SectionKit.xcodeproj, Example package manifests, unittests.yml, Fastfile, Gemfile, .swiftlint.yml, ErrorHandling, Cartfile.resolved._ + +### Infrastructure as Code + +| Resource | Repository | Path | +|----------|-----------|------| +| Terraform | None | No `.tf` files exist | +| Kubernetes / Helm | None | No manifests or charts exist | +| ArgoCD | None | No application definitions exist | +| Backstage catalog entry | This repository | `.traderepublic/catalog-info.yaml` | + +--- + +## 6. External Dependencies + +Every external dependency here is a source-code library resolved at build time. There are no runtime partner integrations, no network calls and no credentials of any kind in this repository. + +### 6.1 DifferenceKit + +| Attribute | Value | +|-----------|-------| +| **Partner** | [ra1028/DifferenceKit](https://github.com/ra1028/DifferenceKit) — open-source project | +| **Purpose** | Computes `StagedChangeset` diffs that `DiffingSectionKit` converts into collection view batch operations | +| **Criticality** | Medium — required to build the `DiffingSectionKit` product; the `SectionKit` product builds without it | +| **Contract Type** | Public open-source library | + +**Integration Details**: + +| Aspect | Description | +|--------|-------------| +| **Protocol** | Compile-time Swift module import (`import DifferenceKit`) | +| **Version constraints** | Package.swift: `from: "1.1.5"`; Package.resolved pins `1.2.0` (revision `62745d7780deef4a023a792a1f8f763ec7bf9705`); Cartfile: `~> 1.3.0`; Cartfile.resolved: `1.3.0`. See Critical Discrepancies. | +| **Data Exchanged** | In-process only: arrays of `Differentiable` values in, `StagedChangeset` out | +| **APIs used** | `StagedChangeset`, `Changeset`, `Differentiable`, `mapData` | +| **Documentation** | The upstream repository README | + +**Security & Compliance**: Not applicable — no network transport, no data at rest, no authentication. Carthage builds it as an xcframework, which README notes requires `--use-xcframeworks`. + +**Resilience**: Not applicable — a build-time dependency has no runtime SLA, timeout, retry or circuit breaker. Build-time failure mode: `DiffingSectionKit` does not compile. There is no vendored copy or fallback. + +**Monitoring**: None. No dependency-scanning or update-bot configuration (no Dependabot or Renovate config) exists in the repository. + +_Sources: Package.swift, Package.resolved, Cartfile, Cartfile.resolved, DiffingListSectionController, ManualDiffingListSectionController, DiffingListCollectionViewAdapter, Changeset+BatchOperations, README._ + +### 6.2 ReactiveSwift and ReactiveCocoa (example app only) + +| Attribute | Value | +|-----------|-------| +| **Partner** | [ReactiveCocoa/ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift), [ReactiveCocoa/ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) | +| **Purpose** | Drive the ReactiveSwift example screen and the `Binding*` section controller helpers | +| **Criticality** | Low — neither shipped product depends on these; only `Example/ReactiveSwiftExamples` does | +| **Contract Type** | Public open-source libraries | + +**Integration Details**: Compile-time Swift module imports. `ReactiveSwift from: "6.6.0"`, `ReactiveCocoa from: "11.2.1"`. APIs used include `Signal`, `Property`, `Disposable`. README notes that Trade Republic combines SectionKit with ReactiveSwift in production, but that combination is not part of this repository's products. + +**Security & Compliance / Resilience / Monitoring**: Not applicable — build-time dependencies of a demo target. + +_Sources: Example/ReactiveSwiftExamples Package.swift, BindingListSectionController, EmojisViewController (ReactiveSwift), README._ + +### 6.3 swift-snapshot-testing (test targets only) + +| Attribute | Value | +|-----------|-------| +| **Partner** | [pointfreeco/swift-snapshot-testing](https://github.com/pointfreeco/swift-snapshot-testing) | +| **Purpose** | Snapshot assertions in the example packages' test targets | +| **Criticality** | Low — test-only | +| **Contract Type** | Public open-source library | + +**Integration Details**: `from: "1.10.0"`, declared by the `Utilities`, `VanillaSwiftExamples` and `ReactiveSwiftExamples` manifests. Reference images live in `__Snapshots__` directories, which each manifest `exclude`s from compilation. The harness renders a controller inside a `SectionSnapshotContainer`, which wraps it in a `SnapshotCollectionViewAdapter` — a `SingleSectionCollectionViewAdapter` subclass whose `calculateUpdate` always produces a plain reload so snapshots are deterministic. + +**Security & Compliance / Resilience / Monitoring**: Not applicable — test-only dependency. + +_Sources: Example/Utilities Package.swift, Example/VanillaSwiftExamples Package.swift, Example/ReactiveSwiftExamples Package.swift, SectionSnapshotContainer, SnapshotCollectionViewAdapter._ + +### 6.4 Build and CI tooling + +| Tool | Declared in | Purpose | +|------|-------------|---------| +| Fastlane | Gemfile, Fastfile | Runs `scan` against both library schemes | +| Danger + danger-todoist, danger-swiftlint, danger-xcov | Gemfile | Declared as gems; no Dangerfile or workflow invocation exists | +| SwiftLint | .swiftlint.yml | `only_rules` allowlist configuration | +| Carthage | Cartfile, unittests.yml | Resolves DifferenceKit as an xcframework in CI | +| GitHub Actions | unittests.yml | Runs the Unit Tests workflow on non-draft pull requests | + +_Sources: Gemfile, Fastfile, .swiftlint.yml, Cartfile, unittests.yml._ + +--- + +## 7. Cross-Cutting Concerns + +### Security + +SectionKit is a UI composition library. It performs no I/O, so most of the usual security surface does not exist here. + +| Aspect | Implementation | +|--------|---------------| +| **Authentication** | None. The library has no notion of a user or a session. | +| **Authorization** | None. | +| **Data Encryption at Rest** | Not applicable — the library persists nothing; all state is in-memory and lives only as long as the adapter. | +| **Data Encryption in Transit** | Not applicable — no network code. No `URLSession`, socket or HTTP usage exists in either product. | +| **Secrets Management** | No secrets exist in the repository. CI needs no credentials: the Unit Tests workflow uses only `actions/checkout`, `actions/cache`, Carthage, Bundler and Fastlane, and disables code signing via `CODE_SIGN_IDENTITY='' CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO`. | +| **Input validation** | `IndexPath` values received from UIKit are validated with `indexPath.isValid` before use, and item moves are rejected across section boundaries with `.moveIsNotInTheSameSection`. | +| **Memory safety** | `MainCollectionViewContext.adapter` is `weak`; `SectionController.context` is cleared on sections leaving the adapter; `ExampleListCoordinator.navigationController` is `unowned`. | +| **Vulnerability Scanning** | None configured. No dependency-scanning, SAST or secret-scanning configuration exists in the repository. | + +_Sources: ListCollectionViewAdapter+UICollectionViewDataSource, MainCollectionViewContext, ListCollectionViewAdapter, unittests.yml, Fastfile, Error, ExampleListCoordinator._ + +### Error Handling + +Error handling is the most opinionated part of the library and is worth reading as an architectural choice rather than a detail. All errors funnel through the injectable `ErrorHandling` protocol, whose single requirement is `on(error:severity:file:line:)`. The adapters accept an `errorHandler` at init, so a host app can substitute its own reporting. + +`Error` is an enum of ten library-specific cases: `notImplemented`, `duplicateSectionIds`, `missingDataSource`, `unsupportedSupplementaryViewKind`, `invalidIndexPath`, `moveIsNotInTheSameSection`, `adapterIsNotSetOnContext`, `adapterDoesNotContainSectionController`, `dequeuedViewHasNotTheCorrectType` and `sectionControllerModelTypeMismatch`. Severity is chosen by the call site, not stored on the case. + +The default `ErrorHandler` treats the three severities differently: `critical` calls `fatalError` unconditionally; `nonCritical` calls `assertionFailure` but only inside `#if DEBUG`, otherwise printing when `verbose` is set; `informational` only prints when `verbose` is set. The library itself only ever raises `nonCritical` and `informational` errors — no call site in either product invokes `critical`, so the `fatalError` path is reachable only by a consumer's own handler or call. + +| Scenario | Handling Strategy | +|----------|------------------| +| Invalid `IndexPath` from UIKit | Report `.invalidIndexPath` as non-critical, return a neutral value (`0`, `false`, empty cell/view) | +| No controller for a section index | Report `.missingDataSource(section:)` as non-critical, return a neutral value | +| Dequeued view has the wrong type | Report `.dequeuedViewHasNotTheCorrectType` as non-critical, return a newly constructed instance of the expected type | +| Duplicate section ids | Report `.duplicateSectionIds`, keep the first occurrence, drop the rest | +| Model type mismatch in `didUpdate(model:)` | Report `.sectionControllerModelTypeMismatch`, skip the update entirely | +| Context has lost its adapter, or adapter no longer holds the controller | Report `.adapterIsNotSetOnContext` / `.adapterDoesNotContainSectionController`, fall back to `reloadData()` | +| Unimplemented override called | Report `.notImplemented(function:)`, return `nil` or a placeholder | +| Off-screen collection view | Skip animation: apply data and `reloadData()`, invoke completions with `false` | +| Very large batch (>100 changes in shipped diffing controllers) | `shouldReload` escalates the batch to `reloadSections` instead of animating | +| Deprecated flow-layout override in use | Report `informational` severity, guiding the caller toward `layoutProvider` | + +The consistent pattern is graceful degradation over trapping: in release builds a misconfigured section renders empty rather than crashing the host app. + +**Logging and observability**: there is none beyond the above. The only output statement in either product is the `print` inside the default `ErrorHandler`, gated on `verbose`. No `os_log`, `Logger`, metrics, tracing or crash-reporting integration exists. + +_Sources: ErrorHandling, Error, MainCollectionViewContext, ListCollectionViewAdapter, ListCollectionViewAdapter+UICollectionViewDataSource, ListCollectionViewAdapter+UICollectionViewDelegate, UICollectionView+Apply, ListSectionController, SingleItemSectionController, SingleModelSectionController, BaseSectionController, SectionDataSource._ + +### Concurrency + +Concurrency is handled by exclusion rather than coordination. `CollectionViewAdapter`, `CollectionViewContext`, `Section`, `SectionController`, all six section delegate protocols, every adapter and every shipped controller are annotated `@MainActor`. Closure parameters carry the annotation too — `Section`'s controller closure is `@escaping @MainActor () -> SectionController`, and `ManualDiffingListSectionController`'s `itemId` and `itemContentIsEqual` are `@MainActor` closures. Update completion handlers are typed `(@MainActor (Bool) -> Void)?`. There is no `Task`, `async`, `await`, `DispatchQueue` or lock anywhere in either product. + +_Sources: CollectionViewAdapter, CollectionViewContext, Section, SectionController, ListCollectionViewAdapter, ListSectionController, ManualDiffingListSectionController, CollectionViewBatchOperation._ + +### Testing + +The core library is tested primarily through its UIKit protocol conformances. `SectionKit/Tests` contains directories per UIKit protocol (`UICollectionViewDataSource`, `UICollectionViewDelegate`, `UICollectionViewDelegateFlowLayout`, `UICollectionViewDataSourcePrefetching`, `UICollectionViewDragDelegate`, `UICollectionViewDropDelegate`, `UIScrollViewDelegate`), each with a `Base…`, `List…` and `SingleSection…` variant, so both adapters are exercised against the same shared expectations. A parallel set of `ProtocolDefaultValues…` tests pins down the default implementations of the section protocols. A `TestUtilities` directory supplies mocks for the collection view, adapter, context, error handler, controller and each section delegate. + +`DiffingSectionKit/Tests` contains a single file, `DiffingListCollectionViewAdapterTests`, so the diffing controllers themselves (`DiffingListSectionController`, `ManualDiffingListSectionController`) have no direct unit tests in that target. + +The example packages add three snapshot test targets (`UtilitiesSnapshotTests`, `VanillaSwiftExamplesSnapshotTests`, `ReactiveSwiftExamplesSnapshotTests`) plus `UtilitiesTests`. Only the `SectionKit` and `DiffingSectionKit` schemes run in CI; the example package tests are not invoked by the Fastfile. + +_Sources: SectionKit/Tests, DiffingSectionKit/Tests, Example/Utilities Tests, Example/VanillaSwiftExamples Tests, Example/ReactiveSwiftExamples Tests, Fastfile, unittests.yml._ + +--- + +## 8. Quality Attributes & Risks + +### Quality Goals + +The repository defines no SLOs, latency budgets, throughput targets or recovery objectives — these concepts do not apply to a linked library, and no such configuration exists. The measurable quality gates that do exist: + +| Attribute | Target | Current | How Measured | +|-----------|--------|---------|--------------| +| Unit tests pass | Both library schemes green | Enforced on every non-draft PR | unittests.yml → Fastfile `unittest` lane (`scan`) | +| Lint compliance | The `only_rules` allowlist | `[PENDING CLARIFICATION — .swiftlint.yml exists and danger-swiftlint is in the Gemfile, but no Dangerfile or CI step runs SwiftLint, so this gate is not currently enforced]` | +| Code coverage | `[PENDING CLARIFICATION — danger-xcov is declared in the Gemfile but no coverage threshold is configured and no Dangerfile exists]` | Not enforced | — | +| Animation cost ceiling | Batches over 100 changes reload rather than animate | Implemented as the `shouldReload` default in the shipped diffing controllers | DiffingListSectionController, ManualDiffingListSectionController, FoundationDiffingListSectionController | +| API stability | Public API is a contract for external consumers | Latest release tag `2.1.4` | git tags | + +### Known Risks & Technical Debt + +| Risk/Debt | Severity | Impact | Mitigation | Ticket | +|-----------|----------|--------|------------|--------| +| Section ids must be unique *and* stable, but the requirement is only enforced for uniqueness | Medium | A non-stable id silently recreates the `SectionController`, losing animation and cell state. Only the duplicate case reports an error. | README documents the requirement; duplicates report `.duplicateSectionIds` | None found | +| `Section.model` is `Any`, checked by runtime downcast in `didUpdate(model:)` | Medium | Pairing a controller with the wrong model type is not a compile error. In release builds the update is silently skipped. | `.sectionControllerModelTypeMismatch` reported as non-critical; `assertionFailure` fires in DEBUG only | None found | +| Non-critical errors are silent in release builds | Medium | Misconfiguration that would trap in DEBUG degrades to an empty section in production with no telemetry, since the library has no logging integration. | `ErrorHandling` is injectable, so a host app can route errors to its own reporting | None found | +| `DiffingListSectionController` and `ManualDiffingListSectionController` have no dedicated unit tests | Medium | Regressions in the DifferenceKit diffing paths would not be caught; only `DiffingListCollectionViewAdapterTests` exists in that target. | Core `ListSectionController` behaviour is covered in the SectionKit target | None found | +| Declared platform floor (iOS 9) is far below what CI exercises | Medium | CI runs one Xcode 16.4 / iPhone 16 simulator, so nothing verifies the iOS 9–12 path that the `@available` annotations carefully preserve. | Extensive `@available` gating throughout the sources | None found | +| Deprecated flow-layout API surface retained | Low | `BaseSectionController`'s `sizeForItem`-family overrides are deprecated from iOS 13 in favour of `layoutProvider`, but both paths remain live and the adapters branch on `#available(iOS 13.0, *)`. | Deprecation warnings and `informational` errors guide migration | None found | +| Declared-but-unused CI tooling | Low | Danger, danger-todoist, danger-swiftlint and danger-xcov are installed by `bundle install` on every CI run without being invoked, and the quality gates they imply are not enforced. | — | None found | +| Example package tests not run in CI | Low | The three snapshot test targets and `UtilitiesTests` can rot undetected; the Fastfile only runs the two library schemes. | — | None found | +| Two parallel dependency-management systems | Low | Package.swift/Package.resolved and Cartfile/Cartfile.resolved specify DifferenceKit independently and have drifted apart. | — | None found | + +### Capacity Planning + +Not applicable in the infrastructure sense. The one capacity-shaped decision in the code is the batch-size threshold: the shipped diffing controllers and `DiffingListCollectionViewAdapter` set `shouldReload` to `{ $0.count > 100 }`, above which a batch reloads instead of animating. `SectionKitCompositionalLayout` bounds-checks the requested section index and returns `.empty` after an `assertionFailure` when it is out of range. + +_Sources: unittests.yml, Fastfile, Gemfile, .swiftlint.yml, README, Section, ListSectionController, SingleItemSectionController, SingleModelSectionController, DiffingListSectionController, ManualDiffingListSectionController, FoundationDiffingListSectionController, DiffingListCollectionViewAdapter, ErrorHandling, Error, BaseSectionController, ListCollectionViewAdapter+Convenience, SectionKitCompositionalLayout, Package.swift, Cartfile, DiffingSectionKit/Tests, git tags._ + +--- + +## 9. Decisions & Evolution + +### Architecture Decision Records + +No ADR files exist in the repository. The decisions below are visible in code and doc comments rather than in decision records, and are recorded here as observed rather than as ratified ADRs. + +| Decision | Where visible | Observed rationale | +|----------|--------------|--------------------| +| Adapter owns the single UIKit datasource/delegate conformance and forwards per section | ListCollectionViewAdapter and its protocol extension files | Removes the per-screen `switch` over section indices | +| Diffing lives in a separate product | Package.swift, DiffingSectionKit sources | Keeps the core product free of third-party dependencies | +| A second, Foundation-only diffing controller ships in the core module | FoundationDiffingListSectionController | Gives iOS 13+ consumers diffing without adding DifferenceKit | +| Errors degrade rather than trap in release | ErrorHandling, adapter datasource extensions | A misconfigured section should not crash the host app | +| Section identity drives controller recycling | ListCollectionViewAdapter, Section+Differentiable | Reuse of the live controller is what makes animated updates possible | +| Section-level diff compares ids only | Section+Differentiable | Model changes are delegated to the controller instead of reloading the whole section | +| `@MainActor` isolation throughout instead of internal synchronisation | CollectionViewAdapter, CollectionViewContext, Section, SectionController | UIKit is main-thread-bound; the compiler enforces it | +| `layoutProvider` supersedes the flow-delegate methods on iOS 13+ | SectionController, BaseSectionController, ListCollectionViewAdapter+Convenience | Supports compositional layout alongside flow layout | +| Cell types register themselves on first dequeue | MainCollectionViewContext | Removes registration boilerplate from the host; noted in README | + +### Deprecation Notices + +| Component | Deprecation Date | Removal Date | Migration Path | +|-----------|-----------------|--------------|----------------| +| `BaseSectionController.sizeForItem(at:using:in:)` and the surrounding flow-layout override family | Marked `@available(iOS, introduced: 6.0, deprecated: 13.0)` — no calendar date recorded | Not announced | Message in source: "Please use the layoutProvider with the flowLayout type" | +| `SectionController.flowDelegate` (legacy path) | Superseded on iOS 13+ | Not announced | Adapters read `layoutProvider.flowLayoutProvider` on iOS 13+; return a `.flowLayout` provider instead | + +_Sources: Package.swift, ListCollectionViewAdapter, DiffingSectionKit sources, FoundationDiffingListSectionController, ErrorHandling, Section+Differentiable, CollectionViewAdapter, CollectionViewContext, Section, SectionController, BaseSectionController, ListCollectionViewAdapter+Convenience, MainCollectionViewContext, README._ + +--- + +## 10. Operations + +SectionKit has no runtime operation: there is nothing deployed, nothing to page on and no incident surface owned by this repository. The sections below are populated with what a maintainer — rather than an on-call engineer — needs. + +### Quick Reference + +| Resource | Link | +|----------|------| +| **Primary Dashboard** | None — no deployed runtime, no metrics emitted | +| **Logs** | None — the only output statement in the library is the `verbose`-gated `print` in the default `ErrorHandler` | +| **Alerts** | None configured | +| **Runbooks** | None exist in the repository | +| **On-Call Rotation** | Not applicable. Change review is owned by `@traderepublic/atom-mobile-platform-ios` per CODEOWNERS. | +| **CI status** | GitHub Actions "Unit Tests" workflow, badged in README | +| **Releases** | Git tags — latest is `2.1.4` | + +### Common Operations + +| Operation | Frequency | Procedure | Authorization | +|-----------|-----------|-----------|---------------| +| Run the library test suites | Per pull request | `bundle exec fastlane unittest` — `scan` against the `SectionKit` and `DiffingSectionKit` schemes on an iPhone 16 simulator | Automated on non-draft PRs | +| Resolve Carthage dependencies | On Cartfile.resolved change | `carthage bootstrap --platform iOS --no-use-binaries --use-xcframeworks` | Automated, cached in CI | +| Install Ruby tooling | Per CI run | `bundle install` | Automated | +| Regenerate architecture diagrams | On architecture change | `./docs/generate-diagrams.sh` (requires `structurizr-cli` and `plantuml`) | Maintainers | +| Cut a release | Ad-hoc | `[PENDING CLARIFICATION — no release workflow, script or documented tagging procedure exists in the repository; only the resulting tags are visible]` | `[PENDING CLARIFICATION]` | +| Update snapshot reference images | On intentional UI change | `[PENDING CLARIFICATION — reference images live in __Snapshots__ directories but no documented record/re-record procedure exists in the repository]` | Maintainers | + +### Troubleshooting Guide + +The symptoms below are the failure modes the library's own error cases describe, mapped back to their causes. + +#### Symptom: a section renders empty, or cells are blank + +**Possible Causes**: +1. No controller resolved for that section index → the error handler receives `.missingDataSource(section:)`; in DEBUG this trips `assertionFailure` → check that the datasource returns a `Section` for that index and that ids are not colliding. +2. Cell downcast failed → `.dequeuedViewHasNotTheCorrectType(expected:actual:)`, and a bare instance of the expected type is returned → verify the type argument passed to `dequeueReusableCell`. +3. A required override was not implemented → `.notImplemented(function:)` names the function → override `items(for:)` / `item(for:)` / `cellForItem(at:in:)` on the controller. + +**Escalation**: Enable `ErrorHandler(verbose: true)` or inject a custom `ErrorHandling` to surface non-critical errors in release builds, where they are otherwise silent. + +#### Symptom: updates are not animated, or the whole section flashes + +**Possible Causes**: +1. The collection view is off-screen (`window == nil`) → `UICollectionView+Apply` applies data and calls `reloadData()`, completing with `false` → expected behaviour; no action. +2. More than 100 changes in one batch → the shipped diffing controllers' `shouldReload` escalates to `reloadSections` → expected; override `calculateUpdate` to change the threshold. +3. A non-diffing `ListSectionController` or the base `ListCollectionViewAdapter` is in use → both produce updates with no batch operations, which reload → switch to a diffing controller or `DiffingListCollectionViewAdapter`. +4. `shouldAnimateItems(from:to:)` returned `false` → the reload runs inside `UIView.performWithoutAnimation` → check the override. + +#### Symptom: a section stops reflecting model changes after a data reload + +**Possible Causes**: +1. Section ids are not stable across updates → the controller is recreated rather than recycled → make ids persistent. +2. The model type does not match the controller's generic parameter → `.sectionControllerModelTypeMismatch(expected:actual:)`, and the update is skipped → align the `Section` model with the controller's `Model`. +3. `shouldUpdateItems(afterModelChangedTo:)` was overridden to return `false` → items are not re-derived from the model; this is what the example `Binding*` controllers do deliberately → confirm the override is intended. +4. `invalidateDataSource()` was called rather than assigning `sections` → this bypasses controller recycling and reloads → assign `sections` when recycling is wanted. + +#### Symptom: `assertionFailure` "Please set the layout provider with `CompositionalLayoutProvider`" + +**Possible Cause**: A `SectionKitCompositionalLayout` is in use but a section's controller returns `.flowLayout` from `layoutProvider` → override `layoutProvider` to return `.compositionalLayout(...)` on every controller in that collection view. + +### Incident History + +None recorded. No post-mortem or incident documentation exists in the repository. + +_Sources: CODEOWNERS, README, Fastfile, unittests.yml, generate-diagrams.sh, ErrorHandling, Error, MainCollectionViewContext, UICollectionView+Apply, ListSectionController, ListCollectionViewAdapter, SectionKitCompositionalLayout, BindingListSectionController, git tags._ + +--- + +## Missing / Unknown Infrastructure + +Components commonly present in a production system that are **absent** from this repository. Listed explicitly so their absence is a documented finding rather than an omission. + +| Component | Status | Notes | +|-----------|--------|-------| +| Authentication / authorization | Absent | No user, session or permission concept exists in a UI composition library | +| Network / HTTP client | Absent | No `URLSession` or any network API in either product | +| Persistence (database, file, keychain) | Absent | All state is in-memory for the lifetime of the adapter | +| Structured logging | Absent | Only a `verbose`-gated `print` in the default `ErrorHandler`; no `os_log` or `Logger` | +| Metrics / tracing / crash reporting | Absent | No telemetry integration of any kind | +| Health checks | Absent | Not applicable to a linked library | +| Rate limiting | Absent | Not applicable | +| Message broker / event bus | Absent | All communication is synchronous main-actor method dispatch | +| Container image / Dockerfile | Absent | No Dockerfile or compose file | +| Infrastructure as code (Terraform, Helm, ArgoCD) | Absent | No infrastructure to define | +| Secrets management | Absent | No secrets required; CI disables code signing | +| Dependency vulnerability scanning | Absent | No Dependabot, Renovate or scanning workflow | +| Dangerfile | Absent | Danger gems are installed by CI but never invoked | +| SwiftLint CI enforcement | Absent | `.swiftlint.yml` exists; no workflow step or Dangerfile runs it | +| Code coverage enforcement | Absent | danger-xcov declared; no threshold configured | +| Release / publish automation | Absent | Tags exist (`2.1.4`); no release workflow or script | +| CHANGELOG | Absent | No changelog file | +| CONTRIBUTING guide | Absent | No contribution guide | +| ADR directory | Absent | No decision records | +| Runbooks | Absent | No operational documentation | +| Example package tests in CI | Absent | Fastfile runs only the two library schemes | + +_Sources: Package.swift, unittests.yml, Fastfile, Gemfile, .swiftlint.yml, ErrorHandling, repository file listing._ + +--- + +## Critical Discrepancies + +Conflicts found between the repository's own documentation, configuration and code. **The code is the final truth**; each row states what the documentation claims and what the code actually does. + +| # | Discrepancy | Evidence | Severity | +|---|-------------|----------|----------| +| 1 | The "Names" example description claims it uses `ListCompositionalLayoutCollectionViewAdapter` and multiple `ListCompositionalLayoutSectionController`. **Neither type exists anywhere in the repository.** The screen actually uses `ListCollectionViewAdapter` over a `SectionKitCompositionalLayout`, with `NamesSectionController`. This string is user-visible in the demo app. | ExampleListCoordinator+Presentable vs. NamesViewController, NamesSectionController, SectionKitCompositionalLayout | High | +| 2 | The IA / Backstage record declares `spec.type: service` and `spec.owner: element-managed-products`. The repository is a client-side Swift library with no service runtime, and CODEOWNERS assigns review of all paths to a different group, `@traderepublic/atom-mobile-platform-ios`. The authoritative record therefore misstates both the component type and, apparently, the owning team. | catalog-info.yaml vs. CODEOWNERS, Package.swift | High | +| 3 | README installation instructions are a major version behind the released artefact: SwiftPM `from: "1.0.0"` and Carthage `~> 1.0`, while the latest tag in the repository is `2.1.4`. A consumer following the README pins to 1.x and never receives 2.x. | README vs. git tags | High | +| 4 | DifferenceKit is constrained three different ways across the two dependency systems: Package.swift says `from: "1.1.5"`, Package.resolved pins `1.2.0`, and Cartfile/Cartfile.resolved specify `~> 1.3.0` / `1.3.0`. CI builds via Carthage, so CI validates against 1.3.0 while a SwiftPM consumer of this repository resolves against a different range, and the checked-in Package.resolved records a third version. | Package.swift, Package.resolved, Cartfile, Cartfile.resolved, unittests.yml | Medium | +| 5 | Package.swift declares `.iOS(.v9)` and the framework target sets `IPHONEOS_DEPLOYMENT_TARGET = 9.0`, but the test lane runs on a single iPhone 16 simulator under Xcode 16.4 and the example packages require iOS 14. The iOS 9–12 support that the extensive `@available` gating exists to preserve is never exercised. | Package.swift, SectionKit.xcodeproj, unittests.yml, Fastfile, Example package manifests | Medium | +| 6 | The doc comment on `ErrorHandler` states it "calls `assertionFailure` for `nonCritical` errors". The implementation only does so inside `#if DEBUG`; in release builds a non-critical error prints when `verbose` is set and is otherwise discarded. Callers reading the doc comment would expect release-build assertions. | ErrorHandling | Low | +| 7 | The Gemfile installs Danger and three Danger plugins (todoist, swiftlint, xcov) on every CI run, and `.swiftlint.yml` defines a full `only_rules` allowlist, but no Dangerfile exists and no workflow step invokes Danger or SwiftLint. The lint and coverage gates these imply are not enforced. | Gemfile, .swiftlint.yml, unittests.yml, repository file listing | Low | +| 8 | Package.resolved records DifferenceKit at schema `"version": 1` with the legacy `object.pins` / `repositoryURL` format, while Package.swift declares `swift-tools-version:5.5`. The resolved file predates the manifest's toolchain, which is consistent with it having gone stale rather than being regenerated. | Package.resolved, Package.swift | Low | + +_Sources: ExampleListCoordinator+Presentable, NamesViewController, NamesSectionController, SectionKitCompositionalLayout, catalog-info.yaml, CODEOWNERS, README, Package.swift, Package.resolved, Cartfile, Cartfile.resolved, unittests.yml, Fastfile, SectionKit.xcodeproj, Example package manifests, ErrorHandling, Gemfile, .swiftlint.yml, git tags._ + +--- + +## Appendix + +### Glossary + +The domain vocabulary of this library is defined in Section 1, Key Concepts. No separate central glossary exists in the repository. + +### Source Map + +Every short name used in a `_Sources:_` line above, resolved to its repository-relative path. + +| Short name | Path | +|------------|------| +| .swiftlint.yml | `.swiftlint.yml` | +| ActivitiesSectionController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Activities/ActivitiesSectionController.swift` | +| BaseSectionController | `SectionKit/Sources/SectionController/Generic/BaseSectionController.swift` | +| BindingListSectionController | `Example/ReactiveSwiftExamples/Sources/ReactiveSwiftExamples/Utility/BindingListSectionController.swift` | +| BindingSingleItemSectionController | `Example/ReactiveSwiftExamples/Sources/ReactiveSwiftExamples/Utility/BindingSingleItemSectionController.swift` | +| BindingSingleModelSectionController | `Example/ReactiveSwiftExamples/Sources/ReactiveSwiftExamples/Utility/BindingSingleModelSectionController.swift` | +| Cartfile | `Cartfile` | +| Cartfile.resolved | `Cartfile.resolved` | +| catalog-info.yaml | `.traderepublic/catalog-info.yaml` | +| Changeset+BatchOperations | `DiffingSectionKit/Sources/Utility/Changeset+BatchOperations.swift` | +| CODEOWNERS | `.github/CODEOWNERS` | +| ColorsSectionController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Colors/ColorsSectionController.swift` | +| ColorsViewController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Colors/ColorsViewController.swift` | +| CollectionViewAdapter | `SectionKit/Sources/CollectionViewAdapter/CollectionViewAdapter.swift` | +| CollectionViewBatchOperation | `SectionKit/Sources/CollectionViewAdapter/Update/CollectionViewBatchOperation.swift` | +| CollectionViewContext | `SectionKit/Sources/CollectionViewAdapter/CollectionViewContext/CollectionViewContext.swift` | +| CollectionViewContext+Size | `SectionKit/Sources/CollectionViewAdapter/CollectionViewContext/CollectionViewContext+Size.swift` | +| CollectionViewSectionUpdate | `SectionKit/Sources/SectionController/Update/CollectionViewSectionUpdate.swift` | +| CollectionViewUpdate | `SectionKit/Sources/CollectionViewAdapter/Update/CollectionViewUpdate.swift` | +| DiffingListCollectionViewAdapter | `DiffingSectionKit/Sources/DiffingListCollectionViewAdapter.swift` | +| DiffingListSectionController | `DiffingSectionKit/Sources/DiffingListSectionController.swift` | +| DiffingSectionKit sources | `DiffingSectionKit/Sources/` | +| DiffingSectionKit.xcodeproj | `DiffingSectionKit.xcodeproj/` | +| DiffingSectionKit/Tests | `DiffingSectionKit/Tests/` | +| DifferentiableBox | `DiffingSectionKit/Sources/Utility/DifferentiableBox.swift` | +| EmojisSectionController (VanillaSwift) | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Emojis/EmojisSectionController.swift` | +| EmojisViewController (ReactiveSwift) | `Example/ReactiveSwiftExamples/Sources/ReactiveSwiftExamples/Emojis/EmojisViewController.swift` | +| EmojisViewController (VanillaSwift) | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Emojis/EmojisViewController.swift` | +| Error | `SectionKit/Sources/Error/Error.swift` | +| ErrorHandling | `SectionKit/Sources/Error/ErrorHandling.swift` | +| Example package manifests | `Example/Examples/Package.swift`, `Example/VanillaSwiftExamples/Package.swift`, `Example/ReactiveSwiftExamples/Package.swift`, `Example/Utilities/Package.swift` | +| Example/Examples Package.swift | `Example/Examples/Package.swift` | +| Example/ReactiveSwiftExamples Package.swift | `Example/ReactiveSwiftExamples/Package.swift` | +| Example/ReactiveSwiftExamples Tests | `Example/ReactiveSwiftExamples/Tests/` | +| Example/Utilities Package.swift | `Example/Utilities/Package.swift` | +| Example/Utilities Tests | `Example/Utilities/Tests/` | +| Example/VanillaSwiftExamples Package.swift | `Example/VanillaSwiftExamples/Package.swift` | +| Example/VanillaSwiftExamples Tests | `Example/VanillaSwiftExamples/Tests/` | +| ExampleListCoordinator | `Example/Examples/Sources/Examples/ExampleList/ExampleListCoordinator.swift` | +| ExampleListCoordinator+Presentable | `Example/Examples/Sources/Examples/ExampleList/ExampleListCoordinator+Presentable.swift` | +| ExampleListViewController | `Example/Examples/Sources/Examples/ExampleList/ExampleListViewController.swift` | +| ExampleListViewModel | `Example/Examples/Sources/Examples/ExampleList/ExampleListViewModel.swift` | +| ExampleSectionController | `Example/Examples/Sources/Examples/ExampleSection/ExampleSectionController.swift` | +| Fastfile | `fastlane/Fastfile` | +| FoundationDiffingListSectionController | `SectionKit/Sources/SectionController/Generic/FoundationDiffingListSectionController.swift` | +| Gemfile | `Gemfile` | +| generate-diagrams.sh | `docs/generate-diagrams.sh` | +| git tags | Not a file — `git tag` output in this clone; latest tag `2.1.4` | +| LICENSE.md | `LICENSE.md` | +| ListCollectionViewAdapter | `SectionKit/Sources/CollectionViewAdapter/ListCollectionViewAdapter/ListCollectionViewAdapter.swift` | +| ListCollectionViewAdapter+Convenience | `SectionKit/Sources/CollectionViewAdapter/ListCollectionViewAdapter/ListCollectionViewAdapter+Convenience.swift` | +| ListCollectionViewAdapter+UICollectionViewDataSource | `SectionKit/Sources/CollectionViewAdapter/ListCollectionViewAdapter/ListCollectionViewAdapter+UICollectionViewDataSource.swift` | +| ListCollectionViewAdapter+UICollectionViewDelegate | `SectionKit/Sources/CollectionViewAdapter/ListCollectionViewAdapter/ListCollectionViewAdapter+UICollectionViewDelegate.swift` | +| ListCollectionViewAdapterDataSource | `SectionKit/Sources/CollectionViewAdapter/ListCollectionViewAdapter/ListCollectionViewAdapterDataSource.swift` | +| ListSectionController | `SectionKit/Sources/SectionController/Generic/ListSectionController.swift` | +| MainCollectionViewContext | `SectionKit/Sources/CollectionViewAdapter/CollectionViewContext/MainCollectionViewContext.swift` | +| ManualDiffingListSectionController | `DiffingSectionKit/Sources/ManualDiffingListSectionController.swift` | +| NamesSectionController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Names/NamesSectionController.swift` | +| NamesViewController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Names/NamesViewController.swift` | +| NavigationSegue | `Example/Utilities/Sources/Utilities/Presentable/NavigationSegue.swift` | +| NumbersViewController | `Example/VanillaSwiftExamples/Sources/VanillaSwiftExamples/Numbers/NumbersViewController.swift` | +| Package.resolved | `Package.resolved` | +| Package.swift | `Package.swift` | +| Presentable | `Example/Utilities/Sources/Utilities/Presentable/Presentable.swift` | +| README | `README.md` | +| repository file listing | Not a file — the absence findings were established by directory traversal of the repository root | +| SceneDelegate | `Example/SectionKitExample/SceneDelegate.swift` | +| Section | `SectionKit/Sources/CollectionViewAdapter/Section.swift` | +| Section+Differentiable | `DiffingSectionKit/Sources/Utility/Section+Differentiable.swift` | +| SectionController | `SectionKit/Sources/SectionController/SectionController.swift` | +| SectionDataSource | `SectionKit/Sources/SectionController/SectionDataSource.swift` | +| SectionIndexPath | `SectionKit/Sources/SectionController/SectionIndexPath.swift` | +| SectionKit.xcodeproj | `SectionKit.xcodeproj/` | +| SectionKit/Tests | `SectionKit/Tests/` | +| SectionKitCompositionalLayout | `SectionKit/Sources/Utility/CompositionalLayout/SectionKitCompositionalLayout.swift` | +| SectionSnapshotContainer | `Example/Utilities/Sources/TestUtilities/SnapshotTests/SectionSnapshotContainer.swift` | +| SingleItemSectionController | `SectionKit/Sources/SectionController/Generic/SingleItemSectionController.swift` | +| SingleModelSectionController | `SectionKit/Sources/SectionController/Generic/SingleModelSectionController.swift` | +| SingleSectionCollectionViewAdapter | `SectionKit/Sources/CollectionViewAdapter/SingleSectionCollectionViewAdapter/SingleSectionCollectionViewAdapter.swift` | +| SnapshotCollectionView | `Example/Utilities/Sources/TestUtilities/SnapshotTests/SnapshotCollectionView.swift` | +| SnapshotCollectionViewAdapter | `Example/Utilities/Sources/TestUtilities/SnapshotTests/SnapshotCollectionViewAdapter.swift` | +| UICollectionView+Apply | `SectionKit/Sources/Utility/UICollectionView+Apply.swift` | +| unittests.yml | `.github/workflows/unittests.yml` | + +Diagram sources: `docs/structurizr/system.dsl`, `docs/diagrams/sequence-cell-rendering.puml`, `docs/diagrams/sequence-item-diffing-update.puml`, `docs/diagrams/sequence-section-list-update.puml`. Regenerate with `docs/generate-diagrams.sh`. + +### Change Log + +| Date | Change | +|------|--------| +| 2026-09-14 | Initial document generated from the repository at commit `bd4a6cd`. C4 context, container and core-component diagrams plus three runtime sequence diagrams authored and committed. | + +### Review History + +| Date | Reviewer | Outcome | +|------|----------|---------| +| 2026-09-14 | `[PENDING CLARIFICATION — awaiting human review]` | Draft, not yet reviewed | diff --git a/docs/diagrams/sequence-cell-rendering.png b/docs/diagrams/sequence-cell-rendering.png new file mode 100644 index 0000000..844bd55 Binary files /dev/null and b/docs/diagrams/sequence-cell-rendering.png differ diff --git a/docs/diagrams/sequence-cell-rendering.puml b/docs/diagrams/sequence-cell-rendering.puml new file mode 100644 index 0000000..be8cbbc --- /dev/null +++ b/docs/diagrams/sequence-cell-rendering.puml @@ -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 diff --git a/docs/diagrams/sequence-item-diffing-update.png b/docs/diagrams/sequence-item-diffing-update.png new file mode 100644 index 0000000..98b66c3 Binary files /dev/null and b/docs/diagrams/sequence-item-diffing-update.png differ diff --git a/docs/diagrams/sequence-item-diffing-update.puml b/docs/diagrams/sequence-item-diffing-update.puml new file mode 100644 index 0000000..cf2d82e --- /dev/null +++ b/docs/diagrams/sequence-item-diffing-update.puml @@ -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 diff --git a/docs/diagrams/sequence-section-list-update.png b/docs/diagrams/sequence-section-list-update.png new file mode 100644 index 0000000..d16ff94 Binary files /dev/null and b/docs/diagrams/sequence-section-list-update.png differ diff --git a/docs/diagrams/sequence-section-list-update.puml b/docs/diagrams/sequence-section-list-update.puml new file mode 100644 index 0000000..bae767e --- /dev/null +++ b/docs/diagrams/sequence-section-list-update.puml @@ -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 diff --git a/docs/diagrams/structurizr-Containers-key.png b/docs/diagrams/structurizr-Containers-key.png new file mode 100644 index 0000000..1c24a73 Binary files /dev/null and b/docs/diagrams/structurizr-Containers-key.png differ diff --git a/docs/diagrams/structurizr-Containers-key.puml b/docs/diagrams/structurizr-Containers-key.puml new file mode 100644 index 0000000..47e3379 --- /dev/null +++ b/docs/diagrams/structurizr-Containers-key.puml @@ -0,0 +1,83 @@ +@startuml + +set separator none +hide stereotype + + + +rectangle "==Container" <> + +person "==Person" <> + +rectangle "==Software System, External" <> + +rectangle "." <<.Element-Transparent>> as 1 +1 --> 1 <> : "Relationship" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/structurizr-Containers.png b/docs/diagrams/structurizr-Containers.png new file mode 100644 index 0000000..022454b Binary files /dev/null and b/docs/diagrams/structurizr-Containers.png differ diff --git a/docs/diagrams/structurizr-Containers.puml b/docs/diagrams/structurizr-Containers.puml new file mode 100644 index 0000000..63351f7 --- /dev/null +++ b/docs/diagrams/structurizr-Containers.puml @@ -0,0 +1,113 @@ +@startuml +title Container View: SectionKit + +set separator none +top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 +hide stereotype + + + +person "==iOS Engineer\n[Person]\n\nIntegrates SectionKit into an app and implements SectionControllers and cells." <> as iOSEngineer +rectangle "==Host iOS Application\n[Software System]\n\nAn app (e.g. the Trade Republic iOS app) that links SectionKit and owns the UIViewController and UICollectionView." <> as HostiOSApplication +rectangle "==UIKit\n[Software System]\n\nApple framework providing UICollectionView, its datasource/delegate protocols and layout classes." <> as UIKit +rectangle "==DifferenceKit\n[Software System]\n\nThird-party diffing library (ra1028/DifferenceKit) used to compute StagedChangesets." <> as DifferenceKit +rectangle "==ReactiveSwift / ReactiveCocoa\n[Software System]\n\nThird-party reactive libraries used only by the example app." <> as ReactiveSwiftReactiveCocoa +rectangle "==swift-snapshot-testing\n[Software System]\n\nThird-party snapshot assertion library used only by example test targets." <> as swiftsnapshottesting + +rectangle "SectionKit\n[Software System]" <> { + rectangle "==SectionKit\n[Container: Swift 5, UIKit framework]\n\nCore library: collection view adapters, section controllers, contexts, update model and error handling. Has no third-party dependencies." <> as SectionKit.SectionKit + rectangle "==DiffingSectionKit\n[Container: Swift 5, UIKit framework]\n\nOptional library adding DifferenceKit-based animated diffing for sections and items." <> as SectionKit.DiffingSectionKit + rectangle "==Examples\n[Container: Swift package library, iOS 14+]\n\nLocal package assembling the example list screen and navigating to every demo." <> as SectionKit.Examples + rectangle "==VanillaSwiftExamples\n[Container: Swift package library, iOS 14+]\n\nLocal package with MVC/MVVM-C demos: Colors, Numbers, Emojis, Activities, Names." <> as SectionKit.VanillaSwiftExamples + rectangle "==ReactiveSwiftExamples\n[Container: Swift package library, iOS 14+]\n\nLocal package with a ReactiveSwift-bound Emojis demo and Binding* section controller helpers." <> as SectionKit.ReactiveSwiftExamples + rectangle "==Utilities\n[Container: Swift package library, iOS 14+]\n\nLocal package with shared cells, supplementary views and the Presentable/NavigationSegue navigation helpers." <> as SectionKit.Utilities + rectangle "==TestUtilities\n[Container: Swift package library, iOS 14+]\n\nLocal package with mocks and the snapshot harness (SectionSnapshotContainer, SnapshotCollectionView, SnapshotCollectionViewAdapter)." <> as SectionKit.TestUtilities + rectangle "==SectionKitExample\n[Container: iOS app, Swift, UIKit]\n\nDemo iOS app that hosts every example screen." <> as SectionKit.SectionKitExample +} + +iOSEngineer --> SectionKit.SectionKitExample <> : "Runs to explore the API" +HostiOSApplication --> SectionKit.SectionKit <> : "Creates an adapter and supplies Sections\n[Swift API]" +HostiOSApplication --> SectionKit.DiffingSectionKit <> : "Subclasses diffing controllers for animated updates\n[Swift API]" +SectionKit.SectionKit --> UIKit <> : "Acts as UICollectionViewDataSource, delegate, flow-layout delegate, prefetching, drag and drop delegate\n[Swift API]" +SectionKit.DiffingSectionKit --> SectionKit.SectionKit <> : "Extends ListSectionController, ListCollectionViewAdapter and Section\n[Swift API]" +SectionKit.DiffingSectionKit --> DifferenceKit <> : "StagedChangeset, Differentiable, Changeset\n[Swift API]" +SectionKit.SectionKitExample --> SectionKit.Examples <> : "Presents ExampleListCoordinator on launch\n[Swift API]" +SectionKit.Examples --> SectionKit.SectionKit <> : "Builds the example list with ListCollectionViewAdapter\n[Swift API]" +SectionKit.Examples --> SectionKit.VanillaSwiftExamples <> : "Navigates to Colors, Numbers, Emojis, Activities and Names\n[Swift API]" +SectionKit.Examples --> SectionKit.ReactiveSwiftExamples <> : "Navigates to the ReactiveSwift Emojis demo\n[Swift API]" +SectionKit.Examples --> SectionKit.Utilities <> : "Presentable, NavigationSegue\n[Swift API]" +SectionKit.VanillaSwiftExamples --> SectionKit.SectionKit <> : "Subclasses ListSectionController and FoundationDiffingListSectionController\n[Swift API]" +SectionKit.VanillaSwiftExamples --> SectionKit.Utilities <> : "Shared cells and navigation helpers\n[Swift API]" +SectionKit.ReactiveSwiftExamples --> SectionKit.SectionKit <> : "Subclasses FoundationDiffingListSectionController and SingleItem/SingleModel controllers\n[Swift API]" +SectionKit.ReactiveSwiftExamples --> SectionKit.Utilities <> : "Shared cells and navigation helpers\n[Swift API]" +SectionKit.ReactiveSwiftExamples --> ReactiveSwiftReactiveCocoa <> : "Signal, Property, Disposable bindings\n[Swift API]" +SectionKit.TestUtilities --> SectionKit.SectionKit <> : "Wraps a section in SingleSectionCollectionViewAdapter for rendering\n[Swift API]" +SectionKit.VanillaSwiftExamples --> SectionKit.TestUtilities <> : "Renders sections in snapshot tests\n[Test-only]" +SectionKit.ReactiveSwiftExamples --> SectionKit.TestUtilities <> : "Renders sections in snapshot tests\n[Test-only]" +SectionKit.Utilities --> SectionKit.TestUtilities <> : "Renders cells in snapshot tests\n[Test-only]" +SectionKit.TestUtilities --> swiftsnapshottesting <> : "assertSnapshot\n[Test-only]" +UIKit --> SectionKit.SectionKit <> : "Datasource and delegate callbacks: numberOfItemsInSection, cellForItemAt, didSelectItemAt, sizeForItemAt, prefetch, drag and drop\n[UIKit delegation]" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/structurizr-CoreComponents-key.png b/docs/diagrams/structurizr-CoreComponents-key.png new file mode 100644 index 0000000..bf467d0 Binary files /dev/null and b/docs/diagrams/structurizr-CoreComponents-key.png differ diff --git a/docs/diagrams/structurizr-CoreComponents-key.puml b/docs/diagrams/structurizr-CoreComponents-key.puml new file mode 100644 index 0000000..18abe04 --- /dev/null +++ b/docs/diagrams/structurizr-CoreComponents-key.puml @@ -0,0 +1,69 @@ +@startuml + +set separator none +hide stereotype + + + +rectangle "==Component" <> + +rectangle "==Software System, External" <> + +rectangle "." <<.Element-Transparent>> as 1 +1 --> 1 <> : "Relationship" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/structurizr-CoreComponents.png b/docs/diagrams/structurizr-CoreComponents.png new file mode 100644 index 0000000..2717172 Binary files /dev/null and b/docs/diagrams/structurizr-CoreComponents.png differ diff --git a/docs/diagrams/structurizr-CoreComponents.puml b/docs/diagrams/structurizr-CoreComponents.puml new file mode 100644 index 0000000..540d352 --- /dev/null +++ b/docs/diagrams/structurizr-CoreComponents.puml @@ -0,0 +1,108 @@ +@startuml +title Component View: SectionKit - SectionKit + +set separator none +top to bottom direction +skinparam ranksep 80 +skinparam nodesep 20 +hide stereotype + + + +rectangle "==UIKit\n[Software System]\n\nApple framework providing UICollectionView, its datasource/delegate protocols and layout classes." <> as UIKit + +rectangle "SectionKit\n[Software System]" <> { + rectangle "SectionKit\n[Container: Swift 5, UIKit framework]" <> { + rectangle "==CollectionViewAdapter\n[Component: Swift protocol]\n\nProtocol for the root object of a UICollectionView that forwards datasource and delegate calls to SectionControllers." <> as SectionKit.SectionKit.CollectionViewAdapter + rectangle "==ListCollectionViewAdapter\n[Component: Swift open class (NSObject)]\n\nAdapter holding an ordered list of sections; implements UICollectionViewDataSource, delegate, flow-layout, prefetching, drag and drop by forwarding per section." <> as SectionKit.SectionKit.ListCollectionViewAdapter + rectangle "==SingleSectionCollectionViewAdapter\n[Component: Swift open class (NSObject)]\n\nAdapter holding exactly one optional section, with the same forwarding surface as the list adapter." <> as SectionKit.SectionKit.SingleSectionCollectionViewAdapter + rectangle "==Adapter DataSources\n[Component: Swift protocols]\n\nListCollectionViewAdapterDataSource and SingleSectionCollectionViewAdapterDataSource: supply Section values to an adapter." <> as SectionKit.SectionKit.AdapterDataSources + rectangle "==Section\n[Component: Swift final class]\n\nValue object binding a unique AnyHashable id, an untyped model and a lazily created SectionController." <> as SectionKit.SectionKit.Section + rectangle "==CollectionViewContext\n[Component: Swift protocol]\n\nProtocol giving a SectionController access to the collection view, view controller, error handler, cell dequeuing and update application." <> as SectionKit.SectionKit.CollectionViewContext + rectangle "==MainCollectionViewContext\n[Component: Swift open class]\n\nDefault context; registers cell/view types on first dequeue and applies updates straight to the UICollectionView." <> as SectionKit.SectionKit.MainCollectionViewContext + rectangle "==SectionController\n[Component: Swift protocol]\n\nProtocol exposing per-section dataSource, delegate, flowDelegate, drag/drop delegates and layoutProvider." <> as SectionKit.SectionKit.SectionController + rectangle "==Section Delegate Protocols\n[Component: Swift protocols]\n\nSectionDataSource, SectionDelegate, SectionFlowDelegate, SectionDragDelegate, SectionDropDelegate, SectionDataSourcePrefetchingDelegate — the per-section mirrors of the UIKit protocols." <> as SectionKit.SectionKit.SectionDelegateProtocols + rectangle "==BaseSectionController\n[Component: Swift open class]\n\nOpen no-op implementation of every section protocol; base class for all shipped controllers." <> as SectionKit.SectionKit.BaseSectionController + rectangle "==Generic SectionControllers\n[Component: Swift generic open classes]\n\nListSectionController, SingleItemSectionController, SingleModelSectionController and FoundationDiffingListSectionController — typed model/item controllers." <> as SectionKit.SectionKit.GenericSectionControllers + rectangle "==Update Model\n[Component: Swift structs]\n\nCollectionViewUpdate, CollectionViewSectionUpdate and their batch operations describing deletes, inserts, moves and reloads plus a shouldReload predicate." <> as SectionKit.SectionKit.UpdateModel + rectangle "==UICollectionView+Apply\n[Component: Swift extension]\n\nTranslates an update into performBatchUpdates, reloadSections or reloadData depending on window state and shouldAnimate/shouldReload." <> as SectionKit.SectionKit.UICollectionViewApply + rectangle "==Error Handling\n[Component: Swift enum, protocol, struct]\n\nSectionKit Error enum, Severity, the ErrorHandling protocol and the default ErrorHandler (fatalError / assertionFailure / ignore)." <> as SectionKit.SectionKit.ErrorHandling + rectangle "==SectionKitCompositionalLayout\n[Component: Swift final class, iOS 13+]\n\nUICollectionViewCompositionalLayout subclass that resolves each section's NSCollectionLayoutSection from its CompositionalLayoutProvider." <> as SectionKit.SectionKit.SectionKitCompositionalLayout + } + +} + +UIKit --> SectionKit.SectionKit.ListCollectionViewAdapter <> : "Datasource and delegate callbacks: numberOfItemsInSection, cellForItemAt, didSelectItemAt, sizeForItemAt, prefetch, drag and drop\n[UIKit delegation]" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.CollectionViewAdapter <> : "Conforms to" +SectionKit.SectionKit.SingleSectionCollectionViewAdapter --> SectionKit.SectionKit.CollectionViewAdapter <> : "Conforms to" +SectionKit.SectionKit.SingleSectionCollectionViewAdapter --> SectionKit.SectionKit.ListCollectionViewAdapter <> : "Same forwarding surface, but holds at most one Section" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.AdapterDataSources <> : "Queries sections(for:) on invalidateDataSource" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.Section <> : "Holds an ordered list of, de-duplicated by id" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.MainCollectionViewContext <> : "Creates and owns; sets itself as adapter" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.SectionDelegateProtocols <> : "Resolves the per-section delegate for an IndexPath and forwards the call" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.UpdateModel <> : "Builds a CollectionViewUpdate in calculateUpdate(from:to:)" +SectionKit.SectionKit.ListCollectionViewAdapter --> SectionKit.SectionKit.SectionKitCompositionalLayout <> : "Injects its sections closure when the layout is a SectionKitCompositionalLayout" +SectionKit.SectionKit.Section --> SectionKit.SectionKit.SectionController <> : "Lazily creates and retains one" +SectionKit.SectionKit.MainCollectionViewContext --> SectionKit.SectionKit.CollectionViewContext <> : "Conforms to" +SectionKit.SectionKit.MainCollectionViewContext --> SectionKit.SectionKit.UICollectionViewApply <> : "Delegates apply(update:) to" +SectionKit.SectionKit.MainCollectionViewContext --> SectionKit.SectionKit.ErrorHandling <> : "Reports non-critical errors to" +SectionKit.SectionKit.MainCollectionViewContext --> UIKit <> : "register, dequeueReusableCell, dequeueReusableSupplementaryView, reloadData\n[UIKit API]" +SectionKit.SectionKit.UICollectionViewApply --> UIKit <> : "performBatchUpdates, insert/delete/move/reload, reloadData\n[UIKit API]" +SectionKit.SectionKit.UICollectionViewApply --> SectionKit.SectionKit.UpdateModel <> : "Reads batch operations, shouldAnimate and shouldReload from" +SectionKit.SectionKit.SectionController --> SectionKit.SectionKit.SectionDelegateProtocols <> : "Exposes" +SectionKit.SectionKit.SectionController --> SectionKit.SectionKit.CollectionViewContext <> : "Reads container size and applies updates through" +SectionKit.SectionKit.BaseSectionController --> SectionKit.SectionKit.SectionController <> : "Implements every member of" +SectionKit.SectionKit.GenericSectionControllers --> SectionKit.SectionKit.BaseSectionController <> : "Inherit from" +SectionKit.SectionKit.GenericSectionControllers --> SectionKit.SectionKit.UpdateModel <> : "Return a CollectionViewSectionUpdate from calculateUpdate(from:to:)" +SectionKit.SectionKit.GenericSectionControllers --> SectionKit.SectionKit.ErrorHandling <> : "Report model type mismatch and notImplemented to" +SectionKit.SectionKit.SectionKitCompositionalLayout --> SectionKit.SectionKit.Section <> : "Asks each section's controller for its NSCollectionLayoutSection" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/structurizr-SystemContext-key.png b/docs/diagrams/structurizr-SystemContext-key.png new file mode 100644 index 0000000..fdb8316 Binary files /dev/null and b/docs/diagrams/structurizr-SystemContext-key.png differ diff --git a/docs/diagrams/structurizr-SystemContext-key.puml b/docs/diagrams/structurizr-SystemContext-key.puml new file mode 100644 index 0000000..fffaadc --- /dev/null +++ b/docs/diagrams/structurizr-SystemContext-key.puml @@ -0,0 +1,72 @@ +@startuml + +set separator none +hide stereotype + + + +person "==Person" <> + +rectangle "==Software System" <> + +rectangle "==Software System, External" <> + +rectangle "." <<.Element-Transparent>> as 1 +1 --> 1 <> : "Relationship" + +@enduml \ No newline at end of file diff --git a/docs/diagrams/structurizr-SystemContext.png b/docs/diagrams/structurizr-SystemContext.png new file mode 100644 index 0000000..8543373 Binary files /dev/null and b/docs/diagrams/structurizr-SystemContext.png differ diff --git a/docs/diagrams/structurizr-SystemContext.puml b/docs/diagrams/structurizr-SystemContext.puml new file mode 100644 index 0000000..6262420 --- /dev/null +++ b/docs/diagrams/structurizr-SystemContext.puml @@ -0,0 +1,83 @@ +@startuml +title System Context View: SectionKit + +set separator none +top to bottom direction +skinparam ranksep 60 +skinparam nodesep 30 +hide stereotype + + + +person "==iOS Engineer\n[Person]\n\nIntegrates SectionKit into an app and implements SectionControllers and cells." <> as iOSEngineer +rectangle "==SectionKit\n[Software System]\n\nA Swift library that lets each section of a UICollectionView be implemented, tested and reused separately." <> as SectionKit +person "==App User\n[Person]\n\nScrolls, selects and drags items in the collection view of the host app." <> as AppUser +rectangle "==Host iOS Application\n[Software System]\n\nAn app (e.g. the Trade Republic iOS app) that links SectionKit and owns the UIViewController and UICollectionView." <> as HostiOSApplication +rectangle "==UIKit\n[Software System]\n\nApple framework providing UICollectionView, its datasource/delegate protocols and layout classes." <> as UIKit +rectangle "==DifferenceKit\n[Software System]\n\nThird-party diffing library (ra1028/DifferenceKit) used to compute StagedChangesets." <> as DifferenceKit +rectangle "==ReactiveSwift / ReactiveCocoa\n[Software System]\n\nThird-party reactive libraries used only by the example app." <> as ReactiveSwiftReactiveCocoa +rectangle "==swift-snapshot-testing\n[Software System]\n\nThird-party snapshot assertion library used only by example test targets." <> as swiftsnapshottesting +rectangle "==Swift Package Manager / Carthage\n[Software System]\n\nDistribution channels that resolve and build SectionKit for consumers." <> as SwiftPackageManagerCarthage +rectangle "==GitHub Actions\n[Software System]\n\nRuns the Unit Tests workflow on pull requests via Carthage, Bundler and Fastlane." <> as GitHubActions + +iOSEngineer --> SectionKit <> : "Implements SectionControllers and cells with" +AppUser --> HostiOSApplication <> : "Scrolls, selects and drags items in" +HostiOSApplication --> SectionKit <> : "Renders its collection view screens with" +SectionKit --> UIKit <> : "Drives UICollectionView via its datasource and delegate protocols" +SectionKit --> DifferenceKit <> : "Computes StagedChangesets with" +SectionKit --> ReactiveSwiftReactiveCocoa <> : "Binds example view models with" +SectionKit --> swiftsnapshottesting <> : "Asserts rendered sections in tests with" +SwiftPackageManagerCarthage --> SectionKit <> : "Resolves and builds" +GitHubActions --> SectionKit <> : "Builds and runs unit tests for" +UIKit --> SectionKit <> : "Datasource and delegate callbacks: numberOfItemsInSection, cellForItemAt, didSelectItemAt, sizeForItemAt, prefetch, drag and drop\n[UIKit delegation]" + +@enduml \ No newline at end of file diff --git a/docs/generate-diagrams.sh b/docs/generate-diagrams.sh new file mode 100755 index 0000000..e231f1d --- /dev/null +++ b/docs/generate-diagrams.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# generate-diagrams.sh — Regenerate all architecture diagrams from source. +# +# Prerequisites (macOS): +# brew install structurizr-cli plantuml +# +# Usage: +# ./docs/generate-diagrams.sh + +set -euo pipefail + +# PlantUML silently clips PNG output at PLANTUML_LIMIT_SIZE (default 4096px) +# in whichever dimension overflows, instead of scaling or erroring. Raise it. +export PLANTUML_LIMIT_SIZE=16384 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DSL="$SCRIPT_DIR/structurizr/system.dsl" +DIAGRAMS="$SCRIPT_DIR/diagrams" + +structurizr-cli export \ + -workspace "$DSL" \ + -format plantuml \ + -output "$DIAGRAMS" 1>&2 + +plantuml -tpng "$DIAGRAMS"/*.puml 1>&2 + +# Structured output: JSON to stdout for agent composability. +# Example: +# {"status":"ok","diagrams_dir":"docs/diagrams","files":[{"path":"docs/diagrams/system-context.png","bytes":48213}]} +printf '{"status":"ok","diagrams_dir":"%s","files":[' "$DIAGRAMS" +first=1 +for f in "$DIAGRAMS"/*.png; do + [ -e "$f" ] || continue + bytes=$(wc -c < "$f" | tr -d ' ') + if [ $first -eq 1 ]; then first=0; else printf ','; fi + printf '{"path":"%s","bytes":%s}' "$f" "$bytes" +done +printf ']}\n' diff --git a/docs/structurizr/system.dsl b/docs/structurizr/system.dsl new file mode 100644 index 0000000..b7251b8 --- /dev/null +++ b/docs/structurizr/system.dsl @@ -0,0 +1,147 @@ +workspace "SectionKit" "Swift library that splits a UICollectionView into independently implemented sections" { + !identifiers hierarchical + + model { + engineer = person "iOS Engineer" "Integrates SectionKit into an app and implements SectionControllers and cells." "Person" + appUser = person "App User" "Scrolls, selects and drags items in the collection view of the host app." "Person" + + hostApp = softwareSystem "Host iOS Application" "An app (e.g. the Trade Republic iOS app) that links SectionKit and owns the UIViewController and UICollectionView." "External" + uikit = softwareSystem "UIKit" "Apple framework providing UICollectionView, its datasource/delegate protocols and layout classes." "External" + differenceKit = softwareSystem "DifferenceKit" "Third-party diffing library (ra1028/DifferenceKit) used to compute StagedChangesets." "External" + reactive = softwareSystem "ReactiveSwift / ReactiveCocoa" "Third-party reactive libraries used only by the example app." "External" + snapshotTesting = softwareSystem "swift-snapshot-testing" "Third-party snapshot assertion library used only by example test targets." "External" + spm = softwareSystem "Swift Package Manager / Carthage" "Distribution channels that resolve and build SectionKit for consumers." "External" + ci = softwareSystem "GitHub Actions" "Runs the Unit Tests workflow on pull requests via Carthage, Bundler and Fastlane." "External" + + sectionKit = softwareSystem "SectionKit" "A Swift library that lets each section of a UICollectionView be implemented, tested and reused separately." { + core = container "SectionKit" "Core library: collection view adapters, section controllers, contexts, update model and error handling. Has no third-party dependencies." "Swift 5, UIKit framework" { + adapterProtocol = component "CollectionViewAdapter" "Protocol for the root object of a UICollectionView that forwards datasource and delegate calls to SectionControllers." "Swift protocol" + listAdapter = component "ListCollectionViewAdapter" "Adapter holding an ordered list of sections; implements UICollectionViewDataSource, delegate, flow-layout, prefetching, drag and drop by forwarding per section." "Swift open class (NSObject)" + singleAdapter = component "SingleSectionCollectionViewAdapter" "Adapter holding exactly one optional section, with the same forwarding surface as the list adapter." "Swift open class (NSObject)" + adapterDataSource = component "Adapter DataSources" "ListCollectionViewAdapterDataSource and SingleSectionCollectionViewAdapterDataSource: supply Section values to an adapter." "Swift protocols" + section = component "Section" "Value object binding a unique AnyHashable id, an untyped model and a lazily created SectionController." "Swift final class" + contextProtocol = component "CollectionViewContext" "Protocol giving a SectionController access to the collection view, view controller, error handler, cell dequeuing and update application." "Swift protocol" + mainContext = component "MainCollectionViewContext" "Default context; registers cell/view types on first dequeue and applies updates straight to the UICollectionView." "Swift open class" + controllerProtocol = component "SectionController" "Protocol exposing per-section dataSource, delegate, flowDelegate, drag/drop delegates and layoutProvider." "Swift protocol" + sectionProtocols = component "Section Delegate Protocols" "SectionDataSource, SectionDelegate, SectionFlowDelegate, SectionDragDelegate, SectionDropDelegate, SectionDataSourcePrefetchingDelegate — the per-section mirrors of the UIKit protocols." "Swift protocols" + baseController = component "BaseSectionController" "Open no-op implementation of every section protocol; base class for all shipped controllers." "Swift open class" + genericControllers = component "Generic SectionControllers" "ListSectionController, SingleItemSectionController, SingleModelSectionController and FoundationDiffingListSectionController — typed model/item controllers." "Swift generic open classes" + updateModel = component "Update Model" "CollectionViewUpdate, CollectionViewSectionUpdate and their batch operations describing deletes, inserts, moves and reloads plus a shouldReload predicate." "Swift structs" + applyExtension = component "UICollectionView+Apply" "Translates an update into performBatchUpdates, reloadSections or reloadData depending on window state and shouldAnimate/shouldReload." "Swift extension" + errorHandling = component "Error Handling" "SectionKit Error enum, Severity, the ErrorHandling protocol and the default ErrorHandler (fatalError / assertionFailure / ignore)." "Swift enum, protocol, struct" + compositional = component "SectionKitCompositionalLayout" "UICollectionViewCompositionalLayout subclass that resolves each section's NSCollectionLayoutSection from its CompositionalLayoutProvider." "Swift final class, iOS 13+" + } + + diffing = container "DiffingSectionKit" "Optional library adding DifferenceKit-based animated diffing for sections and items." "Swift 5, UIKit framework" + + examplesPkg = container "Examples" "Local package assembling the example list screen and navigating to every demo." "Swift package library, iOS 14+" + vanillaPkg = container "VanillaSwiftExamples" "Local package with MVC/MVVM-C demos: Colors, Numbers, Emojis, Activities, Names." "Swift package library, iOS 14+" + reactivePkg = container "ReactiveSwiftExamples" "Local package with a ReactiveSwift-bound Emojis demo and Binding* section controller helpers." "Swift package library, iOS 14+" + utilitiesPkg = container "Utilities" "Local package with shared cells, supplementary views and the Presentable/NavigationSegue navigation helpers." "Swift package library, iOS 14+" + testUtilitiesPkg = container "TestUtilities" "Local package with mocks and the snapshot harness (SectionSnapshotContainer, SnapshotCollectionView, SnapshotCollectionViewAdapter)." "Swift package library, iOS 14+" + exampleApp = container "SectionKitExample" "Demo iOS app that hosts every example screen." "iOS app, Swift, UIKit" + } + + # Context relationships + engineer -> sectionKit "Implements SectionControllers and cells with" + engineer -> sectionKit.exampleApp "Runs to explore the API" + appUser -> hostApp "Scrolls, selects and drags items in" + hostApp -> sectionKit "Renders its collection view screens with" + sectionKit -> uikit "Drives UICollectionView via its datasource and delegate protocols" + sectionKit -> differenceKit "Computes StagedChangesets with" + sectionKit -> reactive "Binds example view models with" + sectionKit -> snapshotTesting "Asserts rendered sections in tests with" + spm -> sectionKit "Resolves and builds" + ci -> sectionKit "Builds and runs unit tests for" + + # Container relationships + hostApp -> sectionKit.core "Creates an adapter and supplies Sections" "Swift API" + hostApp -> sectionKit.diffing "Subclasses diffing controllers for animated updates" "Swift API" + sectionKit.core -> uikit "Acts as UICollectionViewDataSource, delegate, flow-layout delegate, prefetching, drag and drop delegate" "Swift API" + sectionKit.diffing -> sectionKit.core "Extends ListSectionController, ListCollectionViewAdapter and Section" "Swift API" + sectionKit.diffing -> differenceKit "StagedChangeset, Differentiable, Changeset" "Swift API" + sectionKit.exampleApp -> sectionKit.examplesPkg "Presents ExampleListCoordinator on launch" "Swift API" + sectionKit.examplesPkg -> sectionKit.core "Builds the example list with ListCollectionViewAdapter" "Swift API" + sectionKit.examplesPkg -> sectionKit.vanillaPkg "Navigates to Colors, Numbers, Emojis, Activities and Names" "Swift API" + sectionKit.examplesPkg -> sectionKit.reactivePkg "Navigates to the ReactiveSwift Emojis demo" "Swift API" + sectionKit.examplesPkg -> sectionKit.utilitiesPkg "Presentable, NavigationSegue" "Swift API" + sectionKit.vanillaPkg -> sectionKit.core "Subclasses ListSectionController and FoundationDiffingListSectionController" "Swift API" + sectionKit.vanillaPkg -> sectionKit.utilitiesPkg "Shared cells and navigation helpers" "Swift API" + sectionKit.reactivePkg -> sectionKit.core "Subclasses FoundationDiffingListSectionController and SingleItem/SingleModel controllers" "Swift API" + sectionKit.reactivePkg -> sectionKit.utilitiesPkg "Shared cells and navigation helpers" "Swift API" + sectionKit.reactivePkg -> reactive "Signal, Property, Disposable bindings" "Swift API" + sectionKit.testUtilitiesPkg -> sectionKit.core "Wraps a section in SingleSectionCollectionViewAdapter for rendering" "Swift API" + sectionKit.vanillaPkg -> sectionKit.testUtilitiesPkg "Renders sections in snapshot tests" "Test-only" + sectionKit.reactivePkg -> sectionKit.testUtilitiesPkg "Renders sections in snapshot tests" "Test-only" + sectionKit.utilitiesPkg -> sectionKit.testUtilitiesPkg "Renders cells in snapshot tests" "Test-only" + sectionKit.testUtilitiesPkg -> snapshotTesting "assertSnapshot" "Test-only" + + # Component relationships + uikit -> sectionKit.core.listAdapter "Datasource and delegate callbacks: numberOfItemsInSection, cellForItemAt, didSelectItemAt, sizeForItemAt, prefetch, drag and drop" "UIKit delegation" + sectionKit.core.listAdapter -> sectionKit.core.adapterProtocol "Conforms to" + sectionKit.core.singleAdapter -> sectionKit.core.adapterProtocol "Conforms to" + sectionKit.core.singleAdapter -> sectionKit.core.listAdapter "Same forwarding surface, but holds at most one Section" + sectionKit.core.listAdapter -> sectionKit.core.adapterDataSource "Queries sections(for:) on invalidateDataSource" + sectionKit.core.listAdapter -> sectionKit.core.section "Holds an ordered list of, de-duplicated by id" + sectionKit.core.listAdapter -> sectionKit.core.mainContext "Creates and owns; sets itself as adapter" + sectionKit.core.listAdapter -> sectionKit.core.sectionProtocols "Resolves the per-section delegate for an IndexPath and forwards the call" + sectionKit.core.listAdapter -> sectionKit.core.updateModel "Builds a CollectionViewUpdate in calculateUpdate(from:to:)" + sectionKit.core.listAdapter -> sectionKit.core.compositional "Injects its sections closure when the layout is a SectionKitCompositionalLayout" + sectionKit.core.section -> sectionKit.core.controllerProtocol "Lazily creates and retains one" + sectionKit.core.mainContext -> sectionKit.core.contextProtocol "Conforms to" + sectionKit.core.mainContext -> sectionKit.core.applyExtension "Delegates apply(update:) to" + sectionKit.core.mainContext -> sectionKit.core.errorHandling "Reports non-critical errors to" + sectionKit.core.mainContext -> uikit "register, dequeueReusableCell, dequeueReusableSupplementaryView, reloadData" "UIKit API" + sectionKit.core.applyExtension -> uikit "performBatchUpdates, insert/delete/move/reload, reloadData" "UIKit API" + sectionKit.core.applyExtension -> sectionKit.core.updateModel "Reads batch operations, shouldAnimate and shouldReload from" + sectionKit.core.controllerProtocol -> sectionKit.core.sectionProtocols "Exposes" + sectionKit.core.controllerProtocol -> sectionKit.core.contextProtocol "Reads container size and applies updates through" + sectionKit.core.baseController -> sectionKit.core.controllerProtocol "Implements every member of" + sectionKit.core.genericControllers -> sectionKit.core.baseController "Inherit from" + sectionKit.core.genericControllers -> sectionKit.core.updateModel "Return a CollectionViewSectionUpdate from calculateUpdate(from:to:)" + sectionKit.core.genericControllers -> sectionKit.core.errorHandling "Report model type mismatch and notImplemented to" + sectionKit.core.compositional -> sectionKit.core.section "Asks each section's controller for its NSCollectionLayoutSection" + } + + views { + systemContext sectionKit "SystemContext" { + include * + include appUser + autoLayout tb + } + + container sectionKit "Containers" { + include * + autoLayout tb + } + + component sectionKit.core "CoreComponents" { + include * + autoLayout tb 400 200 + } + + styles { + element "Person" { + shape Person + background #08427B + color #ffffff + } + element "Software System" { + background #1168BD + color #ffffff + } + element "Container" { + background #438DD5 + color #ffffff + } + element "Component" { + background #85BBF0 + color #000000 + } + element "External" { + background #999999 + color #ffffff + } + } + } +}