B Blueprint

Architecture

Dependency Injection

How I wired Discover manually with DIContainer, bundles, and factories.

How I connected clients, repositories, use cases, and screens without Swinject.

Discover has a lot to wire: HTTP client, three repositories, several use cases, location, SwiftData, feature flags, three screens.

I inject dependencies through init instead of globals inside types.

DIContainer

Created once in AppRouterView:

@State private var container = DIContainer()

DIContainer builds bundles, then factories:

@MainActor
final class DIContainer {
  let homeFactory: HomeFactory
  let detailFactory: DetailFactory
  let favoritesFactory: FavoritesFactory

  init() {
    let network = NetworkDependencies()
    let poi = POIDependencies(network: network)
    let location = LocationDependencies()
    let persistence = PersistenceDependencies()
    let featureFlags = FeatureFlagDependencies()

    homeFactory = HomeFactory(
      poi: poi, location: location,
      persistence: persistence, featureFlags: featureFlags
    )
    detailFactory = DetailFactory(persistence: persistence, featureFlags: featureFlags, poi: poi)
    favoritesFactory = FavoritesFactory(persistence: persistence)
  }
}

The whole graph fits in one file I can scroll through.

What I did

Manual DI: DIContainer + dependency bundles + screen factories. No third-party container.

Why (then)

On a study project I learn more when I see every wire. Native Birds (my reference) used the same pattern. I can grep “who creates HomeViewModel” and land in HomeFactory.

What I’d reconsider

If the graph doubled in size I might extract a composition root per feature or try a lightweight container. For three screens, manual wiring still beats magic.

Bundles

A bundle groups construction for one concern:

Bundle Creates
NetworkDependencies NetworkClient
POIDependencies Repositories + POI-related UseCases
LocationDependencies LocationService
PersistenceDependencies ModelContainer, FavoritesUseCase
FeatureFlagDependencies FeatureFlagService

Example from POIDependencies:

init(network: NetworkDependencies) {
  let repository = POIRepository(client: network.client, apiKey: Secrets.geoapifyAPIKey)
  fetchNearbyPOIs = FetchNearbyPOIsUseCase(repository: repository)
  // ...
}

Bundles keep DIContainer from becoming hundreds of lines.

Factories

A factory builds one screen: View + ViewModel with the right dependencies.

final class HomeFactory {
  func makeView(router: any RouterProtocol, namespace: Namespace.ID) -> some View {
    let viewModel = HomeViewModel(
      fetchNearbyPOIs: poi.fetchNearbyPOIs,
      searchLocation: poi.searchLocation,
      locationService: location.locationService
    )
    return HomeView(viewModel: viewModel, router: router, namespace: namespace)
  }
}

Views do not construct UseCases. Adding a dependency to Home means editing HomeFactory, not every preview.

App entry

@main
struct blueprintApp: App {
  var body: some Scene {
    WindowGroup {
      AppRouterView()
    }
  }
}

AppRouterView owns the container and calls container.homeFactory.makeView(router:) inside each tab’s NavigationStack.

Could I use @Environment instead?

Yes for app-wide services (theme, analytics). I still wanted explicit constructor injection for UseCases and repositories so tests and factories stay obvious. I might mix both on a larger app: Environment for truly global stuff, factories for screen graphs.

Could I use Swinject or Factory?

Yes. Less boilerplate when the graph grows. I skipped them here so the repo stays readable without learning a DI framework’s rules. Trade-off: more manual code, zero code generation.

Read next