Ios architecture expert skill
Use when the user is designing, structuring, refactoring, or reviewing the architecture of an iOS/Swift app: separating features into modules/layers (domain, API, cache, presentation, UI), wiring dependencies in a composition root, breaking up massive view controllers, making code testable via protocol boundaries and test doubles, adapter/proxy/pagination patterns, or handling Sendable/@MainActor at module boundaries. Trigger even when 'architecture' isn't mentioned -- e.g. 'where should networking code live', 'my view controller is untestable', 'how do I split this into Swift packages', 'MVVM vs MVP'. Do NOT use for pure SwiftUI view/state/styling work (swiftui-expert), writing BDD specs or user stories (requirements-engineering), Swift Testing syntax (swift-testing-expert), or non-iOS architecture (Android, backend services).From its SKILL.md
npx -y skills add SwiftyJourney/ios-architecture-expert-skillAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.3 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
iOS Architecture Expert — Clean Modular Architecture
Agent Behavior Contract
When this skill is active, follow these rules strictly:
- Feature modules have zero UIKit/SwiftUI imports — only Foundation. Domain models, use cases, presenters, and API/cache logic never depend on a UI framework.
- Define protocol boundaries at every layer transition —
FeedStore,HTTPClient,ResourceView,FeedCache, etc. Concrete types live behind protocols. - Domain models are value types —
struct,Hashable,Sendable. No classes for models. - Presentation logic is framework-agnostic — presenters output view models (structs). No
UIImage,UIColor, or SwiftUI types in the presentation layer. - All dependency wiring happens in the Composition Root — the app target (or a dedicated
CompositionRootmodule). Feature modules never create their own dependencies. - Tests drive the design — one test class per use case, named by behavior (
CacheFeedUseCaseTests, notLocalFeedLoaderTests). UsemakeSUT()factory in every test class. 6b. Test only the public API — no@testable import. Test targets use a plainimport Feed, never@testable import Feed. Tests exercise a module exclusively through its public surface; they never reach forinternal/privatemembers. If a behavior can't be observed publicly, that's a design signal — expose it through the module's public API (a protocol boundary, a return value, an injected collaborator/spy), not by widening access for the test. Same-target tests (e.g. acceptance tests in the app target) still assert through the composed public seams, not private state. - Use SPM multi-target packages for module separation —
Feed(Foundation),FeediOS(UIKit),App(composition). - Prefer async/await over closures/Combine for async operations. Use
Task.immediatefor synchronous-first execution in adapters. - Write new tests in the project's test framework. The reference patterns here are XCTest, as in the source codebase. When the project uses Swift Testing, translate via the
swift-testing-expertskill — notetrackForMemoryLeaks/addTeardownBlockis XCTest-only. - Mark shared mutable state with
@MainActor— presenters, adapters, view controllers, and composition code run on the main actor.
Architecture Diagnostic Table
| Symptom | First check | Smallest safe fix | Deep dive |
|---|---|---|---|
| Feature module imports UIKit/SwiftUI | Dependency graph | Move type to correct layer | references/architecture-layers.md |
| Retain cycle between presenter and view | Proxy wiring | Add WeakRefVirtualProxy | references/adapters-and-proxies.md |
| CoreData crashes on launch | Fallback strategy | Add InMemoryFeedStore fallback | references/composition-root.md |
| Duplicate network requests on refresh | isLoading guard | Use LoadResourcePresentationAdapter | references/adapters-and-proxies.md |
| Test requires real infrastructure | Protocol boundary | Extract protocol at boundary | references/testing-strategy.md |
| Sendable warning at composition boundary | Closure annotations | Add @Sendable + @MainActor | references/concurrency-at-boundaries.md |
| Pagination won't load next page | loadMore closure | Verify recursive composition in FeedViewAdapter | references/composition-root.md |
| Cache validation doesn't run | Task.immediate usage | Use fire-and-forget with Task.immediate | references/concurrency-at-boundaries.md |
| Debug/test-only code in the production composition | #if DEBUG isolation | Move it to a #if DEBUG SceneDelegate subclass | references/composition-root.md |
Gotchas
Task.immediaterequires a Swift 6.2-era toolchain/recent deployment target (the case study targets iOS 26). On older targets, fall back to plainTaskand handle the synchronousdidStartLoadingrequirement explicitly.CoreDataFeedStore(storeURL: URL(fileURLWithPath: "/dev/null"), contextQueue: .main)is effectively in-memory AND synchronous — it's what makes acceptance tests deterministic without sleeps.- Capture
[store], never[self], in@Sendableclosures created inside the@MainActorservice — capturing self drags MainActor isolation into the closure (compile error under Swift 6). UIWindow(frame:)is deprecated on iOS 26; tests build a dummy scene via(UIWindowScene.self as NSObject.Type).init() as? UIWindowScene(no public initializer exists).didEndDisplaying(cell:forRowAt:)fires for OLD cells after a data-source update — never cancel by indexing the NEW model; cancel through a[IndexPath: CellController]registry populated incellForRowAt.- Diffable data sources don't reflow on Dynamic Type changes — reload when
preferredContentSizeCategorychanges; useapplySnapshotUsingReloadDataguarded by anitemIdentifiersequality check. Thread.isMainThreadis not "on the main queue" (the main thread can run other queues) — prefer@MainActorisolation over runtime checks.- Load
NSManagedObjectModelonce and cache it statically — loading it twice registers duplicateNSEntityDescriptions claiming the sameNSManagedObjectsubclasses (undefined behavior).
Architecture Layers
| Layer | Responsibility |
|---|---|
| Feature | Domain models (struct, Sendable) and abstract use-case protocols. Zero framework imports beyond Foundation. |
| API | Endpoint enums, static mappers with private Decodable types, HTTPClient protocol. |
| Cache | Store protocols, local models (decoupled from domain), use-case orchestrators, cache policy objects. |
| Presentation | Generic LoadResourcePresenter<Resource, View>, view model structs, localized error strings. |
| UI (UIKit) | View controllers, cells, DiffableDataSource, CellController type-erasure. Conforms to presenter view protocols. |
| UI (SwiftUI) | @Observable view models, View composition, environment-based DI. Same presenter patterns, different binding. |
| Composition | Composer static factories, PresentationAdapter, WeakRefVirtualProxy, FeedViewAdapter. App-target only. |
Full code examples: architecture-layers.md
Key Patterns Quick Reference
| Pattern | Purpose |
|---|---|
LoadResourcePresenter<Resource, View> | Reusable loading/error/success state machine with generic mapper |
WeakRefVirtualProxy<T> | Break retain cycles in presenter->view binding via conditional conformance |
LoadResourcePresentationAdapter | Generic async loader bridging use cases to presenters with cancellation |
FeedViewAdapter | Maps domain models to CellController array, composes recursive loadMore |
Paginated<Item> | Recursive pagination with optional loadMore closure (Sendable) |
Composition Root / FeedService | @MainActor orchestrator with lazy init, Scheduler, and fallback strategy |
Scheduler protocol | Abstract store execution context for CoreData/InMemory polymorphism |
InMemoryFeedStore | @MainActor, NSCache-backed PRODUCTION fallback when CoreData fails to init — not a test double (acceptance tests use the real CoreDataFeedStore at /dev/null) |
LoaderSpy<Param, Resource> | Generic async test spy using AsyncThrowingStream for UI integration tests |
| Specification Pattern | Protocol-driven shared test specs across store implementations |
| UI Composer (static factory) | Wire presenter->adapter->view chain per feature (FeedUIComposer.feedComposedWith) |
| Static Mapper | Pure function for data transformation — FeedItemsMapper.map(_:from:) |
| Cache Policy | Business rule encapsulation for cache validation — FeedCachePolicy.validate(_:against:) |
CellController | Type-erased cell composition — wraps UITableViewDataSource + Delegate + Prefetching |
Feature Decision Tree
Starting a new feature? Follow this path:
- Define the domain model ->
structin Feature layer,Hashable,Sendable - Add concurrency annotations ->
@MainActoron view protocols,Sendableon models - Need remote data? -> Endpoint enum + static mapper in API layer
- Need persistence? -> Store protocol + local model + cache policy in Cache layer
- Need to display it? ->
LoadResourcePresenter+ view model struct in Presentation layer - UIKit or SwiftUI? -> Build view layer, conform to
ResourceViewprotocols - Wire it up -> Composer + adapter + proxy in Composition Root
- Verify concurrency -> Build with
SWIFT_STRICT_CONCURRENCY = complete, run Thread Sanitizer
Step-by-step guide: feature-implementation-workflow.md
Guardrails
- Do not create concrete types inside feature modules — all instantiation belongs in the Composition Root
- Do not use singletons —
lazy varinFeedServiceachieves deferred init without global state - Do not add
@MainActorto domain types or store protocols — only presentation, adapters, and composition - Do not use
@unchecked Sendable— redesign the type as a value type or use@MainActor - Do not embed cache policy logic inside the loader — keep it as a separate type
- Do not put navigation logic in view controllers — use closure callbacks wired in the Composition Root
- Do not use
@testable import— test through the public API only; widen production access (public) intentionally rather than tunneling intointernal/privatefrom tests - Do not generalize with a single client. The case study's rhythm is duplicate → generalize → replace → delete: copy the working concrete component for the second feature, and only extract generics (
LoadResourcePresenter-style) once two green implementations exist side by side - Defer non-architectural concurrency questions to the
swift-concurrencyskill
Verification Checklist
When implementing or reviewing architecture:
- Build with
SWIFT_STRICT_CONCURRENCY = complete— zero warnings - No UIKit/SwiftUI imports in Feature/API/Cache modules
- All protocol boundaries have corresponding test doubles
3b. Test targets
importmodules plainly — grep for@testablereturns nothing; every assertion goes through the public API makeSUT()exists in every test class- CoreData store has
InMemoryFeedStorefallback in Composition Root - Run Thread Sanitizer (
-enableThreadSanitizer YES) — zero data races WeakRefVirtualProxywraps all view references in UIKit composition (or@Observablein SwiftUI)- Pagination
loadMoreisnilfor the last page
Reference Router
Open the smallest reference that matches the question:
- Architecture & Layers
- architecture-layers.md — layer boundaries, domain models, protocols
- spm-project-structure.md — module layout, Package.swift, CI
- Composition & Wiring
- composition-root.md — FeedService, Scheduler, fallback, dependency creation
- adapters-and-proxies.md — adapter, proxy, view adapter, pagination wiring
- Concurrency in Architecture
- concurrency-at-boundaries.md — Scheduler, @Sendable, Task.immediate, cancellation
- Testing
- testing-strategy.md — unit, spec, integration, snapshot, acceptance
- Workflow
- feature-implementation-workflow.md — step-by-step feature building