Dependency Injection in StarterAppKit

This guide covers how StarterAppKit uses the Factory dependency injection container to manage and resolve dependencies throughout the application.

Overview

The main point of integration is in StarterApp+Container.swift, located at:

iosStarterApp/Helpers/StarterApp+Container.swift

Within this file, the container is implemented as a singleton through the SharedContainer protocol provided by Factory.

Container Structure

The container class provides a shared, globally accessible point to define and resolve dependencies. For example:

final class StarterAppContainer: SharedContainer { let manager = ContainerManager() static var shared = StarterAppContainer() }

Adding Dependencies

All dependencies are defined as computed properties within an extension of StarterAppContainer. Each property returns a Factory type that describes how to create and optionally cache the dependency instance.

Basic Dependency Example

extension StarterAppContainer { var myNewService: Factory<MyNewService> { self { MyNewService() }.cached } }

How it works:

  • self { MyNewService() } defines how to create a MyNewService instance.
  • .cached ensures the created instance is retained for reuse until memory pressure dictates otherwise

Dependencies Requiring Parameters

Some services may need configuration or parameters:

extension StarterAppContainer { var authenticatedService: Factory<AuthService> { self { AuthService(apiKey: AppConstants.apiKey) }.cached } }

This example injects a fixed API key from AppConstants, ensuring each resolved AuthService is consistently configured.

Storage Types

Factory supports different storage strategies:

  • .cached: A single instance is created and reused; it may be released under memory pressure.
  • .singleton: A single permanent instance is created and never released during the app’s lifecycle.
  • .unique: A fresh instance is created each time the property is accessed.
Use these options judiciously to balance performance and memory usage.

Using Dependencies

To consume defined dependencies, rely on @Injected property wrappers within classes like view models:

import Factory class MyViewModel: ObservableObject { @Injected(\StarterAppContainer.networkManager) private var networkManager @Injected(\StarterAppContainer.dataStore) private var dataStore func fetchData() { // Use networkManager and dataStore here } }

This approach keeps your code cleaner and reduces the need for manual initialization in every class that needs a dependency.

Common Patterns

Simple Network Manager

var networkManager: Factory<NetworkManager> { self { NetworkManager() }.cached }

This sets up a reusable NetworkManager instance that’s retained as long as memory conditions allow.

Service with Configuration

var deeplinkManager: Factory<DeepLinkManager> { self { DeepLinkManager(configuration: AppConstants.deepLinkConfig) }.singleton }

This ensures DeepLinkManager persists throughout the app’s lifecycle, ideal for maintaining state or event registration.

Best Practices

  1. Default to .cached: It offers a good balance of reusability and memory management.
  2. Use .singleton for Stateful Managers: Keep services that need to maintain long-term state alive for the whole app session.
  3. Use .unique Sparingly: Only use when you must ensure a fresh instance (e.g., transient operations).
  4. Group and Organize: Keep related dependencies together in the extension for clarity and maintainability.
  5. Prefer Protocols: Define dependencies against protocol abstractions rather than concrete implementations to improve testability and flexibility.

Complete Example

Here’s a step-by-step example to integrate a new analytics service:

  1. Create the service:
class AnalyticsService { func logEvent(_ name: String) { // Implementation } }
  1. Add it to the container:
extension StarterAppContainer { var analyticsService: Factory<AnalyticsService> { self { AnalyticsService() }.singleton } }
  1. Use in a view model:
class HomeViewModel: ObservableObject { @Injected(\StarterAppContainer.analyticsService) private var analytics func onButtonTap() { analytics.logEvent("button_tapped") } }

Additional Resources

Factory GitHub Repository

By following these guidelines and best practices, you can maintain a clean, organized, and testable dependency injection setup in StarterAppKit.