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. This is lesson 4 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, and your progress syncs across the web and the CoddyKit app. The Swift Academy course includes 4 lessons in total.
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.
Learn Swift with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 122
- Lessons
- 409
Frequently Asked Questions
Is the “Wiring Layers Together without a DI Framework” lesson free?
Yes — any 3 lessons of the Swift Academy learning path, including “Wiring Layers Together without a DI Framework”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Swift Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
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