swiftyjourney/ios-architecture-expert-skill · Archived

ios-architecture-expert

>- 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), choosing between MVC/MVVM/MVP, wiring dependencies in a composition root, breaking up a view controller that coordinates too much, making code testable via protocol boundaries and test doubles, adapter/proxy/composite/decorator patterns, or handling Sendable/@MainActor at module boundaries. Also use it to …

First seen Mar 3, 2026

Installation

$ npx skills add swiftyjourney/ios-architecture-expert-skill --skill ios-architecture-expert

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from swiftyjourney/ios-architecture-expert-skill.

npx skills add swiftyjourney/ios-architecture-expert-skill

Browse all from swiftyjourney/ios-architecture-expert-skill

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 21,962 B
  • docs SUMMARY.md 1,099 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 23 installs

SKILL.md

iOS Architecture Expert — Clean Modular Architecture

Agent Behavior Contract

When this skill is active, follow these rules strictly:

  1. Feature modules have zero UIKit/SwiftUI imports — only Foundation. Domain models, use cases, presenters, and API/cache logic never depend on a UI framework.
  2. **Define a boundary at every layer transition — protocol or closure.** A layer never references a concrete type from another layer. Use a protocol when there are genuinely multiple strategies (FeedStore, HTTPClient, ResourceView, FeedCache); use a plain composed closure (() async throws -> Resource) when there is one — see rule 12.
  3. Domain models are value types — struct, Hashable, Sendable. No classes for models.
  4. Presentation logic is framework-agnostic — presenters output view models (structs). No UIImage, UIColor, or SwiftUI types in the presentation layer.
  5. All dependency wiring happens in the Composition Root — the app target (or a dedicated CompositionRoot module). Feature modules never create their own dependencies.
  6. Tests drive the design — one test class per use case, named by behavior (CacheFeedUseCaseTests, not LocalFeedLoaderTests). Use makeSUT() factory in every test class.
  7. Test only the public API — no @testable import outside the Composition Root. Test targets use a plain import Feed; a behavior that can't be observed publicly is a missing public seam (a boundary, a return value, an injected collaborator), never a reason to widen the test's access. This is deliberately stricter than the source codebase, which uses @testable in a handful of places — the full argument and the Composition Root exception live in testability-and-seams.md.
  8. Use SPM multi-target packages for module separation — Feed (Foundation), FeediOS (UIKit), App (composition).
  9. Prefer async/await over closures/Combine for async operations. Use Task.immediate for synchronous-first execution in adapters where the deployment target allows it (iOS 26+; plain Task + observable-state tests is the documented fallback — see concurrency-at-boundaries.md).
  10. Match the project's existing test framework; default to Swift Testing for greenfield suites. For test doubles, test design, deterministic async testing, Swift Testing syntax and XCTest migration, defer to the swift-testing-expert skill. This skill keeps only the places where a testing decision is a design decision — see testability-and-seams.md. Two things that bite regardless: trackForMemoryLeaks/addTeardownBlock is XCTest-only (the Swift Testing equivalent is a test-scoping trait, ST-0007), and Swift Testing runs tests in parallel by default, so suites sharing a store URL or on-disk artifacts need .serialized.
  11. Mark shared mutable state with @MainActor — presenters, adapters, view controllers, and composition code run on the main actor.
  12. Do not add a protocol boundary that has only one implementation and no second strategy in sight — compose a closure in the Composition Root instead. Boundaries exist to select between strategies.

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
"MVVM or MVP or clean architecture?" What problem you actually have Start MVC; extract when duplication or cross-platform reuse appears references/presentation-patterns.md
View controller coordinates too much Count its views/models/state, not its lines Split into tiny MVCs; move creation to a composer references/presentation-patterns.md
Presentation logic duplicated across controllers Value transformation in the controller Extract a platform-agnostic view model or presenter references/presentation-patterns.md
Protocol with exactly one implementation Is there a second strategy? Delete it; compose a closure in the root references/design-principles.md
"Should this be a singleton?" Which of the three kinds it actually is Inject it; let the root decide the lifetime references/dependency-management.md
Every feature depends on one shared API client Which of the four levels you're on Invert: each module declares the interface it needs references/dependency-management.md
Checking connectivity before a request There is no reliable check Attempt the request; configure waitsForConnectivity references/infrastructure-and-networking.md
@MainActor type races despite Swift 6 Is it reached via a sync protocol requirement? Hop executors in the root via Scheduler references/concurrency-at-boundaries.md
Storyboard "forces" property injection iOS version instantiateViewController(identifier:creator:) references/dependency-management.md
Use case both returns data and mutates state Command–Query Separation Split into a query and a command use case references/design-principles.md
Conformance with empty method bodies Interface Segregation Split the protocol, or compose optional capabilities in a struct references/design-principles.md
Domain model has Codable/@Model/CodingKeys Framework detail in an agnostic type Add a private mirror type in the infrastructure references/architecture-layers.md
Cross-cutting concern duplicated across UI types Where the concrete types are known Decorate at the composition site references/adapters-and-proxies.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/testability-and-seams.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
SwiftUI app has no obvious place to wire dependencies Views constructing their own dependencies Compose in @main App/Scene, inject via init or environment references/swiftui-composition.md

Gotchas

  • Task.immediate (SE-0472, Swift 6.2) has runtime availability aligned to iOS 26/macOS 26 with no back-deployment. On earlier targets, fall back to plain Task — the work is then enqueued, not started synchronously, so handle the synchronous didStartLoading requirement explicitly and make tests await observable state.
  • 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 @Sendable closures created inside the @MainActor service — capturing self drags MainActor isolation into the closure (compile error under Swift 6).
  • UIWindow(frame:) and UIWindow() are deprecated on iOS 26 ("Use init(windowScene:)") — deprecated, not obsoleted: they still compile with a warning, which is acceptable in test targets. The (UIWindowScene.self as NSObject.Type).init() as? UIWindowScene dummy-scene trick works (no public initializer exists) but is an unsanctioned runtime bypass that may break in any release — see testability-and-seams.md.
  • 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 in cellForRowAt.
  • Diffable data sources don't reflow on Dynamic Type changes — reload when the content size category changes via registerForTraitChanges([UITraitPreferredContentSizeCategory.self]) { ... } (traitCollectionDidChange(_:) is deprecated since iOS 17); use applySnapshotUsingReloadData guarded by an itemIdentifiers equality check.
  • Thread.isMainThread is not "on the main queue" (the main thread can run other queues) — prefer @MainActor isolation over runtime checks.
  • Load NSManagedObjectModel once and cache it statically — loading it twice registers duplicate NSEntityDescriptions claiming the same NSManagedObject subclasses (undefined behavior).
  • A diffable data source has no sections at all until the first snapshot is applied — guard numberOfSections in test DSLs, or they crash before the first render.
  • adjustsFontForContentSizeCategory defaults to false — UIFont.preferredFont(forTextStyle:) alone does not give you Dynamic Type.
  • An NSKeyValueObservation must be retained or the observation is removed immediately; release it deliberately (e.g. in didEndDisplaying).
  • Diffable data sources request cells eagerly, ahead of display, based on estimatedRowHeight — set a realistic estimate, and move expensive work from cellForRowAt to willDisplay.
  • **A @MainActor type reached through a non-isolated synchronous protocol requirement is not isolated** — protocol witness tables don't carry isolation and there is no suspension point, so it runs on the caller's thread with no warning and no error. Hop executors in the Composition Root.
  • URLSession holds a strong reference to its delegate until you invalidate the session — invalidateAndCancel() or you leak until the app exits.
  • Setting anything on a URLSessionConfiguration after creating the session has no effect.
  • Task is not AnyCancellable: it does not cancel on deinit, and Task.isCancelled must be checked manually. Combine's Future runs eagerly, Task does not — use Task.immediate to preserve that timing.

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.

Which layer a line of code belongs to is decided by what kind of logic it is: application-specific (use cases), application-agnostic (business models and policies), or framework (infrastructure). That distinction — not the folder name — is the rule.

Full code examples: [architecture-layers.md](references/architecture-layers.md)
· The reasoning: [design-principles.md](references/design-principles.md)
· Choosing MVC/MVVM/MVP for the presentation + UI layers: [presentation-patterns.md](references/presentation-patterns.md)


Key Patterns Quick Reference

Pattern Purpose
Adapter Connect two APIs that don't match (LoadResourcePresentationAdapter, FeedViewAdapter)
Decorator Add behaviour keeping the same interface — cross-cutting concerns at the composition site
Composite Combine implementations behind one interface — fallback, retry, layered caching
Interception Inject a side effect at the composition site so neither collaborator knows (cache-on-success)
Null Object Neutral behaviour instead of optionality — the production sibling of a dummy double
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:

  1. Define the domain model -> struct in Feature layer, Hashable, Sendable
  2. Add concurrency annotations -> @MainActor on view protocols, Sendable on models
  3. Need remote data? -> Endpoint enum + static mapper in API layer
  4. Need persistence? -> Store protocol + local model + cache policy in Cache layer
  5. Need to display it? -> Start with MVC and split into tiny MVCs; reach for a view model or presenter only when duplication or cross-platform reuse demands it. Once two features share the loading/error/success shape, generalize to LoadResourcePresenter + view model struct in the Presentation layer
  6. UIKit or SwiftUI? -> Build view layer, conform to ResourceView protocols
  7. Wire it up -> Composer + adapter + proxy in Composition Root
  8. Verify concurrency -> Build with SWIFTSTRICTCONCURRENCY = complete, run Thread Sanitizer

Step-by-step guide: [feature-implementation-workflow.md](references/feature-implementation-workflow.md)


Guardrails

  • Do not create concrete types inside feature modules — all instantiation belongs in the Composition Root
  • Do not reach for .shared from a component — inject it and let the Composition Root own the lifetime (lazy var in FeedService gives a shared instance without global state). Note "singleton" means three different things — see dependency-management.md
  • Do not reference a DI container, coordinator or router from a feature module — that is the Service Locator anti-pattern
  • Do not check connectivity before making a request
  • Do not add @MainActor to 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 into internal/private from 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
  • Do not encode memory-management policy in a component — lifetime is the composer's decision, which is why the presenter holds its view strongly and the composer wraps it in a proxy
  • Do not let a feature module depend on the Composition Root — invert it and pass a closure in
  • Do not reach for coordinators, routers or a DI container before the complexity justifies them; when you do, they live in the Composition Root only
  • Do not make a domain or store boundary async because one implementation is — keep it synchronous and inject the execution context at composition time
  • Defer non-architectural concurrency questions (task groups, async sequences, actor reentrancy) to the swift-language-expert skill

Verification Checklist

When implementing or reviewing architecture:

  1. Build with SWIFTSTRICTCONCURRENCY = complete — zero warnings
  2. No UIKit/SwiftUI imports in Feature/API/Cache modules
  3. All protocol boundaries have corresponding test doubles
  4. Test targets import modules plainly — grep for @testable returns nothing; every assertion goes through the public API
  5. makeSUT() exists in every test class
  6. Store initialization failure is handled deliberately (in-memory fallback or Null Object — a logged product decision)
  7. Run Thread Sanitizer (-enableThreadSanitizer YES) — zero data races
  8. WeakRefVirtualProxy wraps all view references in UIKit composition (or @Observable in SwiftUI)
  9. Pagination loadMore is nil for the last page
  10. Every protocol boundary has, or is credibly about to have, more than one implementation
  11. No conformance in the codebase has an empty method body
  12. No feature module imports the Composition Root
  13. Deleting import UIKit/import SwiftUI from every presentation file still compiles

Reference Router

Open the smallest reference that matches the question:

  • Why, before how

- [design-principles.md](references/design-principles.md) — symptom→principle table, CQS, ISP, dependency inversion & rejection, DTOs, invalid states, Functional Core/Imperative Shell

  • Dependencies & Ownership

- [dependency-management.md](references/dependency-management.md) — singleton taxonomy, the four levels, injection techniques, composer rules, Pure DI vs containers, lifetimes

  • Infrastructure

- [infrastructure-and-networking.md](references/infrastructure-and-networking.md) — connectivity (never pre-flight), URLSession configuration & delegate lifetime, representable states, CoreData details, logging & fallbacks

  • Architecture & Layers

- [architecture-layers.md](references/architecture-layers.md) — layer boundaries, domain models, protocols, do-you-need-persistence - [spm-project-structure.md](references/spm-project-structure.md) — onion, horizontal/vertical slicing, module layout, Package.swift, CI

  • Presentation

- [presentation-patterns.md](references/presentation-patterns.md) — MVC/MVVM/MVP, diagnosing a bloated controller, where memory policy lives

  • SwiftUI

- [swiftui-composition.md](references/swiftui-composition.md) — @main App composition root, environment DI, NavigationStack routing at the root, @Observable presenters, acceptance seams

  • Composition & Wiring

- [composition-root.md](references/composition-root.md) — what the root is, lifetimes, FeedService, Scheduler, fallback, logging - [adapters-and-proxies.md](references/adapters-and-proxies.md) — adapter, proxy, composite, decorator, interception, pagination wiring

  • Concurrency in Architecture

- [concurrency-at-boundaries.md](references/concurrency-at-boundaries.md) — why boundaries stay synchronous, Scheduler, @Sendable, Task.immediate, cancellation

  • Testing

- [testability-and-seams.md](references/testability-and-seams.md) — testing decisions that are design decisions: public-API testing, extracting boundaries, contract specs, acceptance tests through the root - Everything else about testing → the swift-testing-expert skill

  • Workflow

- [feature-implementation-workflow.md](references/feature-implementation-workflow.md) — step-by-step feature building, spy-first/protocol-last, legacy-code extraction