Wiring Layers Together without a DI Framework
Composing objects at the app entry point using a Composition Root pattern.
Wiring Layers Together without a DI Framework is a free Swift Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Composition Root
The Composition Root is the single location where all objects are instantiated and wired together. In a SwiftUI app, it's typically @main.
@main
struct MyApp: App {
// All wiring happens here
private let container = AppContainer()
var body: some Scene {
WindowGroup { container.makeRootView() }
}
}AppContainer Pattern
An AppContainer struct/class builds the object graph, hiding wiring complexity from the App struct.
final class AppContainer {
let db = SQLiteDatabase()
lazy var userRepo: UserRepository = RemoteUserRepository(db: db)
lazy var fetchUser = FetchUserUseCase(repo: userRepo)
lazy var profileVM = ProfileViewModel(useCase: fetchUser)
func makeRootView() -> some View { ProfileView(viewModel: profileVM) }
}Lazy Properties for Order
Use lazy properties to ensure dependencies are built in the correct order without manual ordering.
final class Container {
lazy var network: NetworkClient = URLSessionClient()
lazy var api: APIService = APIService(client: network) // created after network
lazy var repo: ItemRepository = RemoteItemRepository(api: api)
}Factory Methods
Add factory methods to the container to create view-level objects with per-screen dependencies.
extension AppContainer {
func makeOrderFlow() -> OrderFlowView {
let repo = RemoteOrderRepository(api: api)
let useCase = PlaceOrderUseCase(repo: repo)
let vm = OrderViewModel(useCase: useCase)
return OrderFlowView(viewModel: vm)
}
}Environment Values for Injection
In SwiftUI, use custom EnvironmentKeys to inject dependencies deep into the view tree without prop-drilling.
private struct FetchUserKey: EnvironmentKey {
static let defaultValue: FetchingUser = FetchUserUseCase(repo: NullUserRepo())
}
extension EnvironmentValues {
var fetchUser: FetchingUser {
get { self[FetchUserKey.self] } set { self[FetchUserKey.self] = newValue }
}
}Using the Environment Key
Inject via .environment(\.fetchUser, container.fetchUser) and read with @Environment(\.fetchUser) in views.
ContentView()
.environment(\.fetchUser, container.fetchUser)
// In a deep view:
@Environment(\.fetchUser) private var fetchUserScoped Containers
Create child containers for feature scopes, inheriting shared dependencies but owning feature-specific ones.
final class CheckoutContainer {
let cart: CartRepository
let payment: PaymentService
lazy var placeOrder = PlaceOrderUseCase(cart: cart, payment: payment)
init(parent: AppContainer) {
cart = parent.cartRepo
payment = parent.paymentService
}
}Testing the Composition Root
Test the container's factory methods by verifying the returned objects have the correct type and configuration.
func testContainerBuildsProfileView() {
let container = AppContainer(db: InMemoryDatabase())
let view = container.makeRootView()
XCTAssertTrue(type(of: view) == ProfileView.self)
}Avoiding Circular Dependencies
If A depends on B and B depends on A, introduce a mediator or restructure responsibilities to break the cycle.
// Circular:
// UserService → NotificationService → UserService
// Fix: UserService → NotificationInput (protocol) ← NotificationServiceWhen to Reach for a DI Framework
Manual wiring scales well to medium-sized apps. Consider frameworks (Needle, Swinject) only when the container becomes too large to maintain.
// Manual: fine for <20 top-level dependencies
// Framework: helps with large teams, 50+ services, code-generation for factoriesRecap Example
Full wiring example in a small app: container builds all dependencies and a factory produces the root view.
final class AppContainer {
private let db = CoreDataStack.shared
lazy var userRepo: UserRepository = LocalUserRepository(db: db)
lazy var fetchUser = FetchUserUseCase(repo: userRepo)
func makeContentView() -> ContentView {
ContentView(viewModel: ProfileViewModel(useCase: fetchUser))
}
}Quick Check
What is the Composition Root in a Swift app?
Lesson Recap
Wire your app at a Composition Root (usually @main). Use an AppContainer with lazy properties for correct build order. Add factory methods for per-screen graphs. Inject deep dependencies via custom EnvironmentKeys. Break circular dependencies with mediator protocols.
Frequently asked questions
Is the “Wiring Layers Together without a DI Framework” lesson free?
Yes — the full text of “Wiring Layers Together without a DI Framework” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.
What will I learn in “Wiring Layers Together without a DI Framework”?
Composing objects at the app entry point using a Composition Root pattern. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Swift Academy?
No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Wiring Layers Together without a DI Framework” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Swift Academy lesson?
Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Layers: Domain, Data and Presentation
- Use Case and Repository Patterns
- Dependency Inversion with Protocol Abstractions
- Wiring Layers Together without a DI Framework