FS
ForgeStorage

With ForgeInject

This example shows how to register all three ForgeStorage stores in a ForgeInject dependency container. Covers modular registration, resolving via @ForgeInject, and swapping in-memory implementations for tests.

Modular Registration

Create a dedicated module that registers all storage dependencies. This keeps your app entry point clean and makes it easy to swap implementations.

StorageModule.swift
import ForgeInject
import ForgeStorage

struct StorageModule: ForgeModule {
    func register(in container: ForgeContainer) {

        // MARK: - Key-Value Storage

        container.register(KVStoring.self, scope: .singleton) {
            UserDefaultsKVStore()
        }

        // Shared store for widgets/extensions
        container.register(KVStoring.self, name: "shared", scope: .singleton) {
            UserDefaultsKVStore(suiteName: "group.com.taskflow.shared")
        }

        // Disk-based store for large values
        container.register(KVStoring.self, name: "disk", scope: .singleton) {
            DiskKVStore(
                directory: .applicationSupport,
                namespace: "taskflow-data",
                fileProtection: .complete
            )
        }

        // MARK: - File Storage

        container.register(FileStoring.self, scope: .singleton) {
            DiskFileStore(directory: .documents, namespace: "taskflow-files")
        }

        container.register(FileStoring.self, name: "cache", scope: .singleton) {
            DiskFileStore(directory: .caches, namespace: "taskflow-cache")
        }

        // MARK: - Keychain Storage

        container.register(CryptStoring.self, scope: .singleton) {
            CryptStore(service: "com.taskflow")
        }

        container.register(CryptStoring.self, name: "shared", scope: .singleton) {
            CryptStore(
                service: "com.taskflow",
                accessGroup: "group.com.taskflow.shared"
            )
        }
    }
}

App Setup

Register the module at app launch.

TaskFlowApp.swift
import SwiftUI
import ForgeInject

@main
struct TaskFlowApp: App {
    init() {
        ForgeContainer.shared.apply(StorageModule())
    }

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

Resolving via @ForgeInject

Use the @ForgeInject property wrapper to resolve dependencies in your services and ViewModels.

Services.swift
import ForgeInject
import ForgeStorage

final class ProjectSettingsService {
    @ForgeInject private var store: KVStoring

    var hasCompletedOnboarding: Bool {
        store.get(TaskKeys.onboarding) ?? false
    }

    func completeOnboarding() throws {
        try store.set(TaskKeys.onboarding, value: true)
    }
}

final class TaskAttachmentService {
    @ForgeInject(name: "cache") private var fileStore: FileStoring

    func cachedAttachment(for taskId: String) async throws -> Data? {
        let path = "attachments/\(taskId).jpg"
        guard fileStore.exists(path) else { return nil }
        return try await fileStore.load(path)
    }

    func cacheAttachment(_ data: Data, for taskId: String) async throws {
        try await fileStore.save(data, to: "attachments/\(taskId).jpg")
    }
}

final class AuthTokenStorage {
    @ForgeInject private var keychain: CryptStoring

    func storeToken(_ token: String) throws {
        try keychain.set(token, for: AuthKeys.accessToken)
    }

    var accessToken: String? {
        get throws { try keychain.get(AuthKeys.accessToken) }
    }

    func logout() throws {
        try keychain.deleteAll()
    }
}

ViewModel with Injected Dependencies

ProjectViewModel.swift
import SwiftUI
import ForgeInject
import ForgeStorage

@Observable
final class ProjectViewModel {
    @ForgeInject private var store: KVStoring
    @ForgeInject(name: "cache") private var fileStore: FileStoring
    @ForgeInject private var keychain: CryptStoring

    var projectName: String = ""
    var avatarData: Data?
    var isLoggedIn: Bool = false

    func load() async {
        projectName = store.get(TaskKeys.projectName) ?? "My Project"
        isLoggedIn = (try? keychain.contains(AuthKeys.accessToken)) ?? false

        if let userId: String = try? keychain.get(AuthKeys.userId) {
            let path = "avatars/\(userId).jpg"
            if fileStore.exists(path) {
                avatarData = try? await fileStore.load(path)
            }
        }
    }

    func updateProjectName(_ name: String) {
        try? store.set(TaskKeys.projectName, value: name)
        projectName = name
    }

    func logout() async {
        try? keychain.deleteAll()
        try? await fileStore.deleteAll()
        try? store.removeAll()
        isLoggedIn = false
        projectName = "My Project"
        avatarData = nil
    }
}

Composing Services

For larger apps, compose services that each resolve their own dependencies.

TaskFlowUserService.swift
final class TaskFlowUserService {
    @ForgeInject private var store: KVStoring
    @ForgeInject private var keychain: CryptStoring
    @ForgeInject(name: "cache") private var fileStore: FileStoring

    func fullLogout() async throws {
        // Clear tokens
        try keychain.deleteAll()

        // Clear cached files
        try await fileStore.deleteAll()

        // Clear preferences
        try store.removeAll()
    }

    func exportUserData() async throws -> TaskFlowDataExport {
        let name: String? = store.get(TaskKeys.projectName)
        let userId: String? = try keychain.get(AuthKeys.userId)
        let avatarData: Data? = if let userId, fileStore.exists("avatars/\(userId).jpg") {
            try await fileStore.load("avatars/\(userId).jpg")
        } else {
            nil
        }

        return TaskFlowDataExport(name: name, userId: userId, avatar: avatarData)
    }
}

struct TaskFlowDataExport {
    let name: String?
    let userId: String?
    let avatar: Data?
}

Testing with In-Memory Implementations

For tests, register in-memory implementations in the container before running tests. This replaces all production stores with fast, isolated alternatives.

TestStorageModule.swift
import Testing
import ForgeInject
import ForgeStorage
@testable import TaskFlow

struct TestStorageModule: ForgeModule {
    func register(in container: ForgeContainer) {
        container.register(KVStoring.self, scope: .singleton) {
            InMemoryKVStore()
        }

        container.register(KVStoring.self, name: "shared", scope: .singleton) {
            InMemoryKVStore()
        }

        container.register(KVStoring.self, name: "disk", scope: .singleton) {
            InMemoryKVStore()
        }

        container.register(FileStoring.self, scope: .singleton) {
            InMemoryFileStore()
        }

        container.register(FileStoring.self, name: "cache", scope: .singleton) {
            InMemoryFileStore()
        }

        container.register(CryptStoring.self, scope: .singleton) {
            InMemoryCrypt()
        }

        container.register(CryptStoring.self, name: "shared", scope: .singleton) {
            InMemoryCrypt()
        }
    }
}
ServiceTests.swift
struct ProjectSettingsServiceTests {
    init() {
        // Reset and apply test module before each test suite
        ForgeContainer.shared.reset()
        ForgeContainer.shared.apply(TestStorageModule())
    }

    @Test func onboardingDefaultState() {
        let service = ProjectSettingsService()
        #expect(!service.hasCompletedOnboarding)
    }

    @Test func completeOnboarding() throws {
        let service = ProjectSettingsService()
        try service.completeOnboarding()
        #expect(service.hasCompletedOnboarding)
    }
}

struct AuthTokenStorageTests {
    init() {
        ForgeContainer.shared.reset()
        ForgeContainer.shared.apply(TestStorageModule())
    }

    @Test func storeAndRetrieveToken() throws {
        let auth = AuthTokenStorage()
        try auth.storeToken("test_token")
        #expect(try auth.accessToken == "test_token")
    }

    @Test func logoutClearsTokens() throws {
        let auth = AuthTokenStorage()
        try auth.storeToken("test_token")
        try auth.logout()
        #expect(try auth.accessToken == nil)
    }
}

Alternative: Direct Injection

If you prefer constructor injection over @ForgeInject, resolve from the container at the call site and pass directly.

DirectInjection.swift
// Resolve and inject manually
let store: KVStoring = ForgeContainer.shared.resolve()
let service = TaskFlowSettingsService(store: store)

// In tests — no container needed
let service = TaskFlowSettingsService(store: InMemoryKVStore())