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.
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.
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.
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
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.
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.
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()
}
}
} 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.
// 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())