From cc-mobile-ios
Dependency injection patterns used in this project — composition root + constructor injection, protocol-oriented design, testable factories, and how DI fits with SwiftUI / @Observable. Load when adding a new type that needs dependencies, wiring a repository, setting up previews, writing tests, or debugging a DI-related crash.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cc-mobile-ios:ios-diThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Composition root + constructor injection via protocols.** No DI container library by default. If we outgrow this, `swift-dependencies` or `Factory` are the two options we'd consider — but start simple.
Composition root + constructor injection via protocols. No DI container library by default. If we outgrow this, swift-dependencies or Factory are the two options we'd consider — but start simple.
Why:
@Observable view models are constructed by the root view that hosts them.// A DIContainer assembled at app startup. Single source of truth for "how live objects are built."
struct DIContainer {
let apiClient: APIClient
let orderRepository: OrderRepository
static let live: DIContainer = {
let api = LiveAPIClient(baseURL: Env.apiBaseURL)
return DIContainer(
apiClient: api,
orderRepository: LiveOrderRepository(client: api)
)
}()
}
// Factory methods on the container construct things that need more context.
extension DIContainer {
func makeOrderListViewModel() -> OrderListViewModel {
OrderListViewModel(
getOrders: GetOrdersUseCase(orders: orderRepository)
)
}
func makeOrderDetailViewModel(id: OrderID) -> OrderDetailViewModel {
OrderDetailViewModel(
id: id,
getOrder: GetOrderUseCase(orders: orderRepository),
submit: SubmitOrderUseCase(orders: orderRepository)
)
}
}
The root view receives the container — either as an environment value or a plain property:
@main
struct MyApp: App {
let container: DIContainer = .live
var body: some Scene {
WindowGroup {
AppRootView(container: container)
.environment(\.diContainer, container) // optional, for deeper views
}
}
}
private struct DIContainerKey: EnvironmentKey {
static let defaultValue: DIContainer = .live
}
extension EnvironmentValues {
var diContainer: DIContainer {
get { self[DIContainerKey.self] }
set { self[DIContainerKey.self] = newValue }
}
}
Every collaborator used by a use case or view model is a protocol. Implementations live in Data/ (Live…) or in tests (Mock… / Stub…).
protocol OrderRepository: Sendable {
func get(id: OrderID) async throws -> Order
}
// Data/
final class LiveOrderRepository: OrderRepository { /* ... */ }
// Tests/
final class StubOrderRepository: OrderRepository {
let result: Result<Order, Error>
func get(id: OrderID) async throws -> Order { try result.get() }
}
DIContainer.live is the only top-level shared instance. Don't add static let shared elsewhere.lateinit-style setters.Domain layer (a plain struct).A stateful …RootView creates its view model from the container:
struct OrderListRootView: View {
@State private var model: OrderListViewModel
init(container: DIContainer) {
_model = State(initialValue: container.makeOrderListViewModel())
}
var body: some View { /* ... */ }
}
Don't construct view models inside a stateless child view.
Previews should use an in-memory container or direct stubs:
extension DIContainer {
static func preview(
orders: OrderRepository = StubOrderRepository(result: .success(.sample))
) -> DIContainer {
DIContainer(
apiClient: StubAPIClient(),
orderRepository: orders
)
}
}
#Preview {
OrderListRootView(container: .preview())
}
For unit tests, build the view model directly with stubs — you usually don't need the full container.
Consider swift-dependencies or Factory when:
swift-dependencies natively).Don't introduce one just because the app is growing — the plain pattern scales further than people expect.
DIContainer to a view model. The view model should only see the use cases it uses.EnvironmentValues as hidden DI. Environment is for UI concerns (theme, locale). Don't stash business services there.@MainActor on a view model. Leads to data races under Swift 6 strict concurrency.npx claudepluginhub dimitriremoiville/cc-mobile --plugin cc-mobile-iosSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.