Skip to content

Microapps Architecture: Dependency Injection (Swift)

A SwiftUI guide for coding agents. Also covers dependencies struct closures, inject services view model, mock dependencies previews, app dependencies container, testable feature module.

Part four of the microapps series keeps low-level code out of feature modules. A feature module declares the exact functions it needs as a Dependencies struct of closures, and the app container fulfils them. That keeps modules isolated, testable, and previewable.

Declare Dependencies as Closures

The view model owns a nested Dependencies struct listing only what it uses. Passing a whole SearchService instance would work, but it exposes every method the service has; closures expose exactly two.

@MainActor public final class SearchViewModel: ObservableObject {
  public struct Dependencies {
    var search: (String) async throws -> [String]
    var fetchRecent: () async throws -> [String]
  }

  let dependencies: Dependencies
  public init(dependencies: Dependencies) {
    self.dependencies = dependencies
  }

  @Published private(set) var items: [String] = []
  @Published private(set) var recent: [String] = []

  func search(matching query: String) async {
    do {
      items = try await dependencies.search(query)
    } catch {
      items = []
    }
  }
}

Each module stays free to pick its own internal architecture: MVVM here, unidirectional flow in another module.

Mocks for Tests and Previews

Because dependencies are plain closures, a mock is a two-line extension, and Xcode previews run without any real service.

extension SearchViewModel.Dependencies {
  static let mock: Self = .init(
    search: { _ in ["Search Item 1", "Search Item 2"] },
    fetchRecent: { ["query1", "query2"] }
  )
}

struct SearchView_Previews: PreviewProvider {
  static var previews: some View {
    SearchView(viewModel: .init(dependencies: .mock))
  }
}

One Container in the App Target

The app target holds the real services in one AppDependencies container, stored in the AppDelegate or the root view. Computed properties slice it into each feature's Dependencies, wiring service methods to the closures.

struct AppDependencies {
  static let production = Self(
    searchService: SearchService(),
    storage: Storage()
  )

  let searchService: SearchService
  let storage: Storage
}

extension AppDependencies {
  var search: SearchViewModel.Dependencies {
    .init(
      search: searchService.search,
      fetchRecent: searchService.fetchRecent
    )
  }
}

struct RootView: View {
  @StateObject var viewModel = SearchViewModel(
    dependencies: AppDependencies.production.search
  )

  var body: some View {
    SearchView(viewModel: viewModel)
  }
}

Add one computed slice per feature module; the modules never learn which concrete types sit behind their closures.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-microapps-architecture-in-swift-dependency-injection" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems