Skip to content

Quick Start Guide

Get up and running with WeaveDI in 5 minutes. This comprehensive guide covers Swift 5 and Swift 6 compatibility, detailed code explanations, and real-world integration examples.

Installation

Swift Version Requirements

WeaveDI supports a wide range of Swift versions with optimized features for each:

Swift VersioniOS VersionmacOS VersionFeatures
Swift 6.0+iOS 17.0+macOS 14.0+🔥 Full concurrency features, strict Sendable compliance, actor isolation
Swift 5.9+iOS 16.0+macOS 13.0+✅ Complete async/await support, property wrappers, performance optimizations
Swift 5.8+iOS 15.0+macOS 12.0+✅ Core dependency injection, basic concurrency support
Swift 5.7+iOS 14.0+macOS 11.0+⚠️ Limited concurrency, fallback implementations

Swift Package Manager

Add WeaveDI to your project's Package.swift file. This configuration tells Swift Package Manager to download WeaveDI version 3.2.0 or later from the GitHub repository:

swift
// Package.swift
dependencies: [
    .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.2.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: ["WeaveDI"],
        // Swift 6 specific compiler flags for strict concurrency
        swiftSettings: [
            .enableExperimentalFeature("StrictConcurrency") // Swift 6 only
        ]
    )
]

What this does:

  • Downloads the WeaveDI framework from the official repository
  • Ensures you get version 3.2.0 or newer (with latest features and bug fixes)
  • Integrates seamlessly with your Swift project's build system
  • Enables Swift 6 strict concurrency checking for maximum safety

Xcode Integration

For Swift 6 Projects

  1. File → Add Package Dependencies
  2. Enter: https://github.com/Roy-wonji/WeaveDI.git
  3. Choose "Up to Next Major Version" from "3.2.0"
  4. Add to Target
  5. Configure Swift 6 Settings:
    • Target → Build Settings → Swift Language Version → Swift 6
    • Target → Build Settings → Other Swift Flags → Add -strict-concurrency=complete

For Swift 5 Projects

  1. File → Add Package Dependencies
  2. Enter: https://github.com/Roy-wonji/WeaveDI.git
  3. Choose version range that matches your Swift version:
    • Swift 5.9+: Use latest (3.2.0+)
    • Swift 5.8: Use 3.0.x branch
    • Swift 5.7: Use 2.x.x for compatibility

Verification:

swift
// Add this to verify WeaveDI is properly integrated
import WeaveDI

print("WeaveDI Version: \(WeaveDI.version)") // Should print current version
print("Swift Version: \(#if swift(>=6.0) "6.0+" #elseif swift(>=5.9) "5.9+" #else "5.8 or below" #endif)")

Basic Usage

1. Import

First, import WeaveDI into your Swift files where you need dependency injection. This gives you access to all WeaveDI features including property wrappers, registration APIs, and container management:

swift
import WeaveDI

What this enables:

  • Access to @Injected (v3.2.0+), @Factory, @Inject (deprecated), and @SafeInject (deprecated) property wrappers
  • UnifiedDI registration and resolution APIs
  • WeaveDI.Container bootstrap functionality
  • All WeaveDI utility classes and protocols

2. Define Services

Create protocols (interfaces) and implementations for your services. This follows the dependency inversion principle - depend on abstractions, not concrete implementations:

swift
// Define the service contract (what functionality is available)
protocol UserService {
    func fetchUser(id: String) async -> User?
}

// Implement the actual service logic
class UserServiceImpl: UserService {
    func fetchUser(id: String) async -> User? {
        // In real apps, this would call an API or database
        // For demo purposes, we return a simple User object
        return User(id: id, name: "John")
    }
}

Why use protocols?

  • Testability: Easy to create mock implementations for testing
  • Flexibility: Can swap implementations without changing dependent code
  • Maintainability: Clear separation between interface and implementation
  • Best Practice: Follows SOLID principles for clean architecture

3. Register Dependencies

swift
// 1. Define InjectedKey
struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()
}

// 2. Extend InjectedValues
extension InjectedValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }
}

How @Injected registration works:

  • InjectedKey Protocol: Defines live and test values for the dependency
  • InjectedValues Extension: Provides a KeyPath-based accessor
  • Type Safety: Compile-time safety with KeyPath resolution
  • Test Support: Built-in test value support with withInjectedValues

Legacy: UnifiedDI Registration (Deprecated v3.2.0)

swift
// Register at app startup - this creates the binding between protocol and implementation
let userService = UnifiedDI.register(UserService.self) {
    UserServiceImpl()  // Factory closure that creates the actual implementation
}

How registration works:

  • Type Registration: Maps the UserService protocol to UserServiceImpl class
  • Factory Closure: The { UserServiceImpl() } closure defines how to create instances
  • Lazy Creation: Instances are only created when first requested (lazy loading)
  • Singleton by Default: The same instance is reused across the app unless configured otherwise
  • Return Value: Returns the created instance for immediate use if needed

4. Use Property Wrappers

swift
class UserViewController {
    // @Injected resolves via KeyPath - type-safe and TCA-style
    @Injected(\.userService) var userService

    func loadUser() async {
        // Use the injected service directly (non-optional)
        let user = await userService.fetchUser(id: "123")

        // Update UI with retrieved data
        DispatchQueue.main.async {
            // Update your UI here
            print("✅ User loaded: \(user?.name ?? "Unknown")")
        }
    }
}

How @Injected works:

  • KeyPath Resolution: Uses compile-time safe KeyPaths with InjectedValues
  • Non-Optional: Returns the value directly (liveValue or testValue as fallback)
  • Type Safe: Compile-time type checking
  • TCA Compatible: Familiar pattern for TCA developers

Legacy: @Inject (Deprecated v3.2.0)

swift
class UserViewController {
    // @Inject automatically resolves UserService from the DI container
    // The '?' makes it optional - the app won't crash if service isn't registered
    @Inject var userService: UserService?  // ⚠️ Deprecated

    func loadUser() async {
        // Always safely unwrap injected dependencies
        guard let service = userService else {
            print("❌ UserService not available")
            return
        }

        // Use the injected service to perform operations
        let user = await service.fetchUser(id: "123")

        // Update UI with retrieved data
        DispatchQueue.main.async {
            // Update your UI here
            print("✅ User loaded: \(user?.name ?? "Unknown")")
        }
    }
}

How @Inject works:

  • Automatic Resolution: WeaveDI automatically finds and injects the registered implementation
  • Optional Safety: Returns nil if the service isn't registered (prevents crashes)
  • Lazy Loading: The service is only resolved when first accessed
  • Thread Safe: Safe to use across different threads and actors

Property Wrappers

@Injected - Modern Dependency Injection (v3.2.0+)

Use @Injected for modern, type-safe dependency injection with TCA-style KeyPath access:

swift
// Define InjectedKey
struct APIClientKey: InjectedKey {
    static var liveValue: APIClient = URLSessionAPIClient()
    static var testValue: APIClient = MockAPIClient()
}

// Extend InjectedValues
extension InjectedValues {
    var apiClient: APIClient {
        get { self[APIClientKey.self] }
        set { self[APIClientKey.self] = newValue }
    }
}

// Use @Injected
class ViewModel {
    @Injected(\.apiClient) var apiClient
    @Injected(\.userService) var userService

    func loadData() async {
        let data = await apiClient.fetchData()
        let user = await userService.fetchUser(id: "123")
    }
}

When to use @Injected:

  • All new code: Recommended for all new development (v3.2.0+)
  • Type safety: When you want compile-time type checking
  • TCA projects: Familiar pattern for TCA developers
  • Testing: Easy to override with withInjectedValues

@Inject - Optional Dependencies (Deprecated v3.2.0)

Use @Inject for most dependency injection scenarios. It provides safe, optional injection that won't crash your app if a dependency isn't registered:

swift
class ViewController {
    // Standard dependency injection - safe and optional
    @Inject var userService: UserService?

    func viewDidLoad() {
        super.viewDidLoad()

        // Safe optional chaining - won't crash if service is nil
        userService?.fetchUser(id: "current") { [weak self] user in
            DispatchQueue.main.async {
                self?.displayUser(user)
            }
        }

        // Alternative: Explicit nil checking for better error handling
        guard let service = userService else {
            showErrorMessage("User service unavailable")
            return
        }

        // Now we know the service is available
        service.fetchUser(id: "current") { user in
            // Handle user data
        }
    }
}

When to use @Inject:

  • Most scenarios: Your primary choice for dependency injection
  • Optional dependencies: Services that are nice-to-have but not critical
  • Safe injection: When you want to prevent crashes from missing dependencies
  • Testing: Easy to mock by not registering the real service

@Factory - New Instance Each Time

Use @Factory when you need fresh instances rather than shared singletons. Perfect for stateless operations or when you need isolated instances:

swift
class DocumentProcessor {
    // @Factory creates a new PDFGenerator instance every time it's accessed
    // Each document gets its own generator to avoid state conflicts
    @Factory var pdfGenerator: PDFGenerator

    func createDocument(content: String) {
        // Each access to pdfGenerator returns a brand new instance
        let generator = pdfGenerator // New instance created here

        // Configure this specific generator
        generator.setContent(content)
        generator.setFormat(.A4)

        // Generate the PDF
        let pdfData = generator.generate()
        savePDF(pdfData)
    }

    func createMultipleDocuments(contents: [String]) {
        for content in contents {
            // Each iteration gets a completely new PDFGenerator
            let generator = pdfGenerator // Fresh instance for each document

            generator.setContent(content)
            let pdf = generator.generate()
            savePDF(pdf)

            // No need to reset or clean up - each generator is independent
        }
    }
}

When to use @Factory:

  • Stateless operations: PDF generation, image processing, data transformation
  • Concurrent processing: Each thread/task needs its own instance
  • Avoiding shared state: Prevent one operation from affecting another
  • Builder patterns: Fresh builder for each construction
  • Short-lived objects: Objects that don't need to persist

@SafeInject - Error Handling

Use @SafeInject when you need explicit error handling for missing dependencies. This wrapper provides more control over dependency resolution failures:

swift
class DataManager {
    // @SafeInject provides explicit error information when resolution fails
    @SafeInject var database: Database?

    func save(_ data: Data) throws {
        // Check if dependency injection succeeded
        guard let db = database else {
            // Log the specific error for debugging
            print("❌ Database dependency not found - check your DI registration")

            // Throw a descriptive error for the caller
            throw DIError.dependencyNotFound(type: "Database")
        }

        // Proceed with the database operation
        try db.save(data)
        print("✅ Data saved successfully")
    }

    func safeSave(_ data: Data) -> Result<Void, Error> {
        do {
            guard let db = database else {
                return .failure(DIError.dependencyNotFound(type: "Database"))
            }

            try db.save(data)
            return .success(())

        } catch {
            return .failure(error)
        }
    }
}

// Custom error type for better error handling
enum DIError: LocalizedError {
    case dependencyNotFound(type: String)

    var errorDescription: String? {
        switch self {
        case .dependencyNotFound(let type):
            return "Required dependency '\(type)' was not found. Please register it in your DI container."
        }
    }
}

When to use @SafeInject:

  • Critical dependencies: Services that are absolutely required for operation
  • Error reporting: When you need detailed error information about missing dependencies
  • Explicit failure handling: When nil isn't descriptive enough
  • Production debugging: To get better diagnostic information in logs

Advanced Features

Runtime Optimization

WeaveDI includes built-in performance optimizations that can significantly improve dependency resolution speed in production apps:

swift
// Enable automatic runtime optimization
// This should be called early in your app lifecycle, typically in AppDelegate or App.swift
UnifiedRegistry.shared.enableOptimization()

// The optimization system will:
// 1. Cache frequently resolved dependencies for faster access
// 2. Optimize dependency graphs for minimal resolution overhead
// 3. Use lazy loading strategies for better memory management
// 4. Monitor performance and auto-tune based on usage patterns

print("🚀 WeaveDI optimization enabled - expect better performance!")

What optimization does:

  • Hot Path Caching: Frequently accessed dependencies are cached for instant resolution
  • Graph Optimization: Dependency resolution paths are optimized for minimal overhead
  • Memory Management: Automatic cleanup of unused dependencies under memory pressure
  • Performance Monitoring: Real-time analysis of resolution patterns for continuous improvement

When to enable:

  • Production builds: Always enable in release builds for best performance
  • Large applications: Essential for apps with many dependencies
  • Performance-critical apps: Games, real-time apps, or apps with strict performance requirements

Bootstrap Pattern

The bootstrap pattern is the recommended way to set up all your dependencies in one place. This ensures proper initialization order and makes dependency management more organized:

swift
// Bootstrap all dependencies at app startup
// This is typically called in your App.swift or AppDelegate
await WeaveDI.Container.bootstrap { container in
    // Register services in logical order

    // 1. Core infrastructure services first
    container.register(LoggerProtocol.self) {
        ConsoleLogger() // Basic logging for debugging
    }

    // 2. Data layer services
    container.register(DatabaseService.self) {
        CoreDataService() // Database layer
    }

    // 3. Network services
    container.register(NetworkService.self) {
        URLSessionNetworkService() // HTTP client
    }

    // 4. Business logic services (depend on infrastructure)
    container.register(UserService.self) {
        UserServiceImpl() // Uses database and network services automatically
    }

    // 5. Presentation layer services
    container.register(AnalyticsService.self) {
        FirebaseAnalytics() // User tracking and analytics
    }

    print("✅ All dependencies registered successfully")
}

// Alternative: Environment-specific bootstrap
#if DEBUG
await WeaveDI.Container.bootstrap { container in
    // Use mock services for development
    container.register(UserService.self) { MockUserService() }
    container.register(NetworkService.self) { MockNetworkService() }
}
#else
await WeaveDI.Container.bootstrap { container in
    // Use real services for production
    container.register(UserService.self) { UserServiceImpl() }
    container.register(NetworkService.self) { URLSessionNetworkService() }
}
#endif

Benefits of Bootstrap Pattern:

  • Centralized Setup: All dependency registration in one place
  • Proper Ordering: Dependencies are registered in logical order
  • Environment Awareness: Different setups for debug/release builds
  • Error Detection: Easy to spot missing or incorrectly configured dependencies
  • Documentation: Serves as a clear map of your app's dependencies

Next Steps

Released under the MIT License.