Swift Academy · Lesson

Wiring Layers Together without a DI Framework

Composing objects at the app entry point using a Composition Root pattern.

Lesson 4 of 413 steps

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 fetchUser

Scoped 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) ← NotificationService

When 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 factories

Recap 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.

Free to start

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

  1. Layers: Domain, Data and Presentation
  2. Use Case and Repository Patterns
  3. Dependency Inversion with Protocol Abstractions
  4. Wiring Layers Together without a DI Framework
← Back to Swift Academy