0Pricing
Swift Academy · Lesson

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

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

  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