Skip to content

Latest commit

 

History

History
201 lines (154 loc) · 6.6 KB

File metadata and controls

201 lines (154 loc) · 6.6 KB

ForgeInject

Dependency injection for iOS with constructor and property wrapper support.

Swift 6.3+ iOS 18+ macOS 15+ License Release


ForgeInject is a macro-based dependency injection library for iOS. It gives you three ways to resolve dependencies — one is probably the right fit for every scenario you'll encounter.

Macro Use when Resolution
@Injectable View models and services with stable dependencies. Constructor injection, swappable in tests. At init time
@Inject Heavy or optional properties that may never be touched. Lazy + cached + thread-safe. On first access
#inject() Local variables, default parameters, let in structs. Where used

Features

  • Three macros for three injection patterns — pick whichever suits each dependency
  • Testable by design@Injectable generates an init that accepts overrides, so tests don't need to touch the container
  • Thread-safe@Inject's lazy form is backed by Mutex<T?> from the Synchronization framework
  • Three retain policies.transient, .singleton, .weak
  • Modular registration via ForgeRegisterProtocol for feature-based organization
  • Explicit error handling via ForgeContainerError

Requirements

  • iOS 18+
  • macOS 15+
  • Swift 6.3+ (Xcode 26 or later)

Installation

Xcode

  1. File → Add Package Dependencies…
  2. Paste https://github.com/stefanprojchev/ForgeInject.git
  3. Set rule to Up to Next Major from 1.0.0

Package.swift

dependencies: [
    .package(url: "https://github.com/stefanprojchev/ForgeInject.git", from: "1.0.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: ["ForgeInject"]
    )
]

Quick Start

1. Register dependencies

import ForgeInject

struct AppDependencies: ForgeRegisterProtocol {
    func registerDependencies(in container: ForgeContainerProtocol) {
        container.register(with: .singleton) { _ in
            NetworkService() as NetworkServiceProtocol
        }
        container.register(with: .singleton) { _ in
            UserRepository() as UserRepositoryProtocol
        }
    }
}

2. Bootstrap at app launch

import SwiftUI
import ForgeInject

@main
struct TaskFlowApp: App {
    init() {
        let container = ForgeContainer()
        AppDependencies().registerDependencies(in: container)
        ForgeContainer.shared = container
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

3. Inject dependencies

import ForgeInject

@Injectable
@Observable
final class ProfileViewModel {
    let network: NetworkServiceProtocol
    let repository: UserRepositoryProtocol

    var user: User?

    func loadProfile() async {
        user = try? await repository.fetchCurrentUser()
    }
}

// Production — zero-arg init resolves from the container
let vm = ProfileViewModel()

// Tests — pass mocks directly, no container touching
let vm = ProfileViewModel(
    network: MockNetworkService(),
    repository: MockUserRepository()
)

Which macro to use

@Injectable — start here. Generates a matching init with default values resolved from ForgeContainer.shared. Tests override dependencies via normal init arguments — no container manipulation required.

@Injectable
final class ImageProcessor {
    let filter: FilterService
    let storage: StorageService
}

@Inject — lazy, cached, thread-safe. Use for heavy dependencies, circular dependency breaks, or late-bootstrapped services.

final class AdvancedViewModel {
    @Inject var heavyService: HeavyService          // lazy (default)
    @Inject(lazy: false) var logger: LoggerService   // eager, no Mutex overhead
}

#inject() — freestanding, works anywhere a value is expected. Local variables, default parameters, lazy var initializers.

struct RequestHandler {
    let database: Database = #inject()

    func handle() async {
        let logger: Logger = #inject()
        logger.info("handling request")
    }
}

See the Macros section above for when to use which.

Retain policies

// Singleton — shared for the container's lifetime
container.register(with: .singleton) { _ in
    DatabaseService() as DatabaseServiceProtocol
}

// Transient — new instance every resolve
container.register(with: .transient) { _ in
    FormValidator()
}

// Weak — shared while held, recreated after release
container.register(with: .weak) { _ in
    ImageCache() as ImageCacheProtocol
}

The Forge Family

ForgeInject is part of the Forge family of Swift packages for iOS.

Package Description
ForgeCore Thread-safe primitives for iOS Swift packages.
ForgeInject Dependency injection with constructor and property wrapper support.
ForgeObservers Reactive system observers — connectivity, lifecycle, keyboard, and more.
ForgeStorage Type-safe key-value, file, and Keychain storage.
ForgeDB Type-safe repository pattern and GRDB-backed SQLite persistence.
ForgeOrchestrator Orchestrate app flows — startup gates, data pipelines, and continuous monitors.
ForgePush Push notification management — permissions, tokens, and routing.
ForgeLocation Location triggers — geofencing, significant changes, and visits.
ForgeBackgroundTasks Background task scheduling and dispatch.
ForgeNetworking Typed, async/await-first HTTP networking with auth, retry, and background transfers.
ForgeLog Structured logging with pluggable providers and a built-in inspector UI.
ForgeAccess Subscription-aware feature gating with override channels and debug UI.

License

ForgeInject is released under the MIT License. See LICENSE.