1. Modular Architecture & Inverted Dependency
Four modules wired as App → NurUIKit → NurKit ← NurCommsKit.
The last arrow points backwards on purpose — the data layer depends on the
inner layer, never the reverse. Built with Swift 6.1, SwiftUI,
XcodeGen and FactoryKit, targeting iOS 16.0+. Zero third-party HTTP libraries.
📐 Module Graph & Dependency Direction
Interactive Mermaid SVG+ AppDelegate"] NAV["Navigation
AppRouteType · AppRouter · RouteView"] START["Startup/
6 LaunchStep bodies"] DI["AppDI.autoRegister()
the ONLY wiring point"] end subgraph NUIKIT["🎨 NurUIKit — Presentation"] PAGES["Pages/ — 9 screens
View + ViewModel pairs"] COMPS["Components/
NetworkErrorView, AsyncActionButtonView"] end subgraph NURKIT["🧠 NurKit — Domain (inner layer)"] UC["UseCases/ — 6 protocols
+ Native implementations"] REPO["Repositories/ — 7 PROTOCOLS
+ Unwired defaults that throw"] NET["Networking/ — APIService PROTOCOL
APIRouteModel · APIEnvironmentType"] MOD["Models/
NetworkErrorType · LoginSyncModel"] end subgraph COMMSKIT["📡 NurCommsKit — Data / I/O"] RIMPL["7 RepositoryImpl"] DSIMPL["6 DataSourceImpl"] CLIENT["URLSessionAPIService
actor · single HTTP client"] WIRE["Request / Response
internal — stops here"] STORE["KeychainTokenStorage
UserDefaultsPreferenceStore"] end APP --> NUIKIT APP --> NURKIT APP --> COMMSKIT NUIKIT --> NURKIT COMMSKIT -->|"implements protocols
DEPENDENCY INVERSION"| NURKIT PAGES --> UC UC --> REPO RIMPL -.implements.-> REPO CLIENT -.implements.-> NET RIMPL --> CLIENT RIMPL --> DSIMPL DSIMPL --> STORE CLIENT --> WIRE style NURKIT fill:#1a3a5c,color:#fff style COMMSKIT fill:#9a6700,color:#fff
- NurKit owns
LoginRepository,APIService,TokenStorageas protocols. - NurCommsKit implements them — so it imports NurKit, not the other way round.
- Consequence: NurKit compiles and its full test suite runs with NurCommsKit absent from the graph.
- Each module declares dependencies in its own
config.yml; XcodeGen turns them into discrete targets. - Adding
NurCommsKitto NurKit produces “Cycle in dependencies between targets” — verified by actually trying it. - Not oral convention: an illegal
importfails the build.
🧩 Layer Explorer
Click a layerEvery feature is bound to the same chain. Pick a layer to see what it is allowed to touch — and what breaks when the rule is ignored.
📜 4-Module Responsibility Matrix
| Module | Owns (DO) | Boundary (DON’T) |
|---|---|---|
| App NurCore | Composition root, startup sequence, navigation, DI wiring in AppDI.autoRegister() |
No business rules. It assembles; it does not decide. |
| NurUIKit | 9 screens (View + ViewModel), shared components | Zero navigation knowledge — verified: no file references AppRouteType. Does not import NurCommsKit at all. |
| NurKit | Business rules, all protocols, domain models, pure testable logic | Zero HTTP implementation. Must never name a NurCommsKit type. |
| NurCommsKit | URLSession client, Keychain, UserDefaults, wire contracts | Must never import NurUIKit, and never be depended on by NurKit. |
📷 Evidence — the modules are real build targets
Xcode screenshot
“Modular” is a claim that is cheap to make and easy to fake with folders. This is the actual
Xcode project. Look at the TARGETS list: the three amber icons are
three separate framework targets — each one compiles to its own binary, with its own
Info.plist, its own dependency manifest and its own test bundle.
They are modules, not sub-folders of one big codebase.
NurCore.xcodeproj — generated by XcodeGen, never committed. Scheme:
NurCore Staging Debug.
Three framework targets — three modules.
NurKit, NurUIKit, NurCommsKit. Each links separately, so an
import that is not declared in that module’s config.yml fails to compile.
A folder cannot enforce that; a target can.
NurKit_Tests, NurUIKit_Tests, NurCommsKit_Tests — one per module.
NurKit_Tests runs without NurCommsKit in the graph at all, which is what proves the
dependency inversion is real rather than aspirational.
NurCore is the only application target — the composition root. It is the single place
that is allowed to see all three modules at once.
Display Name resolves to four different values —
NurCore_App_PD, _PR, _SD, _SR. That is the flavor matrix in
§5, visible as a build-time fact rather than a runtime branch.
The Modules/ group mirrors the targets, and each module carries its own
Info, Resources, Sources and Tests — the same shape four times over,
not one shared pile.
Factory 3.2.1 is the DI container; the Firebase group covers Crashlytics and Analytics.
No HTTP library appears anywhere — the networking stack is URLSession only.
Minimum deployment target reads iOS 16.0, and the destination list includes iPad,
Mac (Designed for iPad) and Apple Vision — which is why AppDeviceType has to resolve
.vision behind an availability check rather than assuming iPhone.
Package.swift at the repo root is misleading.
It lists the dependency direction reversed. That file is used by neither the local build nor CI —
XcodeGen reads project.yml + each module’s config.yml. Read those instead.