测试助手
- 作者仓库星标 2,844
- 许可证 MIT
- 作者更新于 实时读取
- 作者仓库 Factory
- 领域
- 安全
- 兼容 Agent
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- 信任分
- 94 / 100 · 已通过审计
- 作者 / 版本 / 许可
- @hmlongco · MIT
- Token 消耗评级
- 中等消耗
- 接入复杂程度
- 需简单配置
- 是否需要外部 API Key
- 不需要
- 兼容的系统
- 未声明(默认跨平台)
- 底层运行要求
- 无特殊要求
- 文件与系统权限
-
- 只读
- 允许写入 / 修改
- Shell 执行
- 网络行为
- 仅限本地
- 安装命令数
- 26 条
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
需要注意: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: factory
description: In-depth guide to Factory, a container-based dependency injection system for Swift and SwiftUI.…
category: 安全
runtime: 无特殊运行时
---
# factory 输出预览
## PART A: 任务判断
- 适用问题:安全审计、密钥扫描、权限检查或风险分析。
- 输入要求:目标材料、限制条件、期望输出和验收方式。
- 证据边界:围绕“Module names / Core mental model / Defining factories”读取原文规则,不把推断写成作者承诺。
## PART B: 执行结果
- **01** 任务判断:确认你的需求是否属于安全审计、密钥扫描、权限检查或风险分析,并标出输入、限制和预期结果。
- **02** 执行计划:优先按“Module names / Core mental model / Defining factories”拆成步骤,说明每一步会读取什么、修改什么、产出什么。
- **03** 交付结果:给出可复制的命令、文件改动、检查清单或内容草稿,并说明如何继续迭代。
- **04** 风险边界:结合 读取文件、写入/修改文件、执行终端命令、主要在本地完成、通常不需要额外 API Key 给出执行前确认项。
## Running Rules
- 读取文件、写入/修改文件、执行终端命令;主要在本地完成;通常不需要额外 API Key。
- 先小样例验证,再放大到真实任务。
- 交付时同时给结果、检查口径和下一步迭代建议。 原文没有稳定的斜杠命令要求。安装验证后通常全局生效,直接在对话里点名这个 Skill 并描述任务即可。
告诉 Agent 目标文件或材料、期望结果、不可改范围、是否允许联网或执行命令。本 Skill 的权限画像是:读取文件、写入/修改文件、执行终端命令。
先用一个小任务确认它会围绕“Module names / Core mental model / Defining factories”工作;涉及文件或命令时,先看 diff、日志、预览或测试结果。
检查最终产物是否包含明确结果、必要证据和下一步动作;如果输出泛泛而谈,就补充输入、边界和验收标准后重跑。
---
name: factory
description: In-depth guide to Factory, a container-based dependency injection system for Swift and SwiftUI.…
category: 安全
source: hmlongco/Factory
---
# factory
## 什么时候使用
- 用于审阅代码、文档或方案并给出可执行反馈 适合处理安全审计、密钥扫描、权限检查和风险分析,核心价值是把输入、判断、执行、验证和交付边界固定下来,避免 Agent 泛泛回答。 把任务拆成可执行、可检查、可继续迭代的步骤;通常不需要额外 A…
- 面向安全审计、密钥扫描、权限检查或风险分析,优先处理能明确输入、步骤和验收标准的工作。
## 需要提供什么
- 目标材料、目录范围、期望结果和不可改动内容。
- 是否允许联网、执行命令、读写文件或调用外部服务。
## 执行规则
- 围绕「Module names / Core mental model / Defining factories」组织步骤,不把推断写成作者事实。
- 读取文件、写入/修改文件、执行终端命令;主要在本地完成;通常不需要额外 API Key。
- 先跑小样例,确认结果可检查后再扩大任务范围。
## 输出要求
- 给出最终产物、关键证据、验证方式和下一步动作。
- 信息不足时标记 unknown,不编造命令、平台或依赖。 作者原文负责流程事实;仓库文件负责来源和命令;流狐只补充适用场景、限制和质量判断。
skill "factory" {
输入层 -> 用户目标 + 目标文件 + 禁止范围 + 验收标准
上下文层 -> Module names / Core mental model / Defining factories
规则层 -> SKILL.md 触发条件 / 执行顺序 / 输出格式
运行层 -> 无特殊运行时 | 读取文件、写入/修改文件、执行终端命令 | 主要在本地完成
安全层 -> 通常不需要额外 API Key + 小任务验证 + diff / 日志复核
输出层 -> 可复制结果 + 检查清单 + 下一步迭代
} Factory
Factory is a compile-time-safe, container-based DI system for Swift. It avoids codegen and runtime registration ceremony by making each registration a computed property on a container — if the property doesn't exist, the call site doesn't compile.
Use this skill as the reference when working with Factory code. The authoritative source is the DocC catalog at Sources/FactoryKit/FactoryKit.docc/; this skill condenses and organizes it.
Module names
The package ships two products:
FactoryKit— the library. Import this in app/library code:import FactoryKit.FactoryTesting— Swift Testing traits. Import only in test targets:import FactoryTesting.
The library is named FactoryKit (not Factory) so the import doesn't collide with the Factory type. Don't import FactoryKit into a test target that also imports FactoryTesting — that creates duplicate factories and indeterminate behavior.
Core mental model
Three things to keep straight:
- A
Factory<T>is a transient value — a struct that's built fresh every time you read its computed property and discarded right after. Treat it like a SwiftUIView. Do not cache it in alazy var; that creates a retain cycle on the container. - The container (not the Factory) owns registrations and scope caches. If the container deallocs, its registrations and caches go with it. Singletons are the exception — they're global.
- Modifiers (
.singleton,.cached,.onTest, etc.) re-apply on every read of the computed property. The innermost (factory-defined) value wins by default. See "The Factory wins" below.
Defining factories
Standard factory
import FactoryKit
extension Container {
var myService: Factory<MyServiceType> {
self { MyService() } // sugar — ContainerManager.callAsFunction
}
}
Equivalent long form:
extension Container {
var myService: Factory<MyServiceType> {
Factory(self) { MyService() }
}
}
The sugar (self { ... }) is @inlinable @inline(__always), so there's no perf cost.
Constructor injection
Factories can pull their own dependencies from the same container:
extension Container {
var repository: Factory<Repository> {
self { Repository(network: self.network()) }
}
var network: Factory<Networking> {
self { LiveNetwork() }.singleton
}
}
Parameters — ParameterFactory<P, T>
When a service needs a runtime value:
extension Container {
var paramService: ParameterFactory<Int, ParamService> {
self { ParamService(value: $0) }
}
}
let svc = Container.shared.paramService(42)
Multi-parameter: use a tuple, dict, or struct.
var twoArg: ParameterFactory<(Int, String), Service> {
self { (a, b) in Service(a: a, b: b) }
}
@Injecteddoes not work withParameterFactory— there's no way to feed parameters into the property wrapper at init. Resolve from the container directly, or use the globaldependency(\.path, parameter: ...).
By default, scoping a ParameterFactory caches the first resolved value and returns it for all subsequent calls regardless of parameter. To cache per-parameter (parameter must be Hashable):
var paramService: ParameterFactory<Int, ParamService> {
self { ParamService(value: $0) }.scopeOnParameters.cached
}
Optional / promised factories (cross-module pattern)
When a protocol is declared in module P but the implementation lives in module B that P can't see, declare an optional factory in P and wire it from the app:
// Module P (protocol-only)
extension Container {
var accountLoader: Factory<AccountLoading?> { promised() }
}
// App target — has visibility into both P and B
extension Container: @retroactive AutoRegistering {
func autoRegister() {
accountLoader.register { AccountLoader() } // from module B
}
}
promised() returns nil in release if no registration exists, and fatalErrors in DEBUG to surface the wiring bug. Prefer promised() over Factory<T?> { self { nil } } and never over Factory<T?> { self { fatalError() } } — promise is the safe alternative to fatal-on-resolve.
A ParameterFactory overload of promised() exists for parameterized cross-module factories.
Same-type / multiple instances
Factory keys are derived from the property name (via #function), so this works fine:
extension Container {
var heading: Factory<String> { self { "Heading" } }
var subhead: Factory<String> { self { "Subhead" } }
}
Resolving
Five ways to get an instance, all equivalent for unscoped factories:
// 1. Service-locator on shared container
let svc = Container.shared.myService()
// 2. From a passed container instance
init(container: Container) { self.svc = container.myService() }
// 3. Property wrapper (resolves on init)
@Injected(\.myService) var svc
// 4. Lazy property wrapper (resolves on first access)
@LazyInjected(\.myService) var svc
// 5. Global function (handy in nonisolated classes)
let svc: MyServiceType = dependency(\.myService)
let p = dependency(\.paramService, parameter: 42)
callAsFunction() is the sugar; the explicit form is myService.resolve().
Property wrappers
| Wrapper | Resolves | Notes |
|---|---|---|
@Injected(\.x) |
At init | Eager. Standard choice. |
@LazyInjected(\.x) |
First access | Use when the dep is heavy or might not be needed. Safe for breaking circular deps. |
@WeakLazyInjected(\.x) |
First access | Holds weakly. Use for delegate/parent refs to avoid retain cycles. Wrapped value is T?. |
@DynamicInjected(\.x) |
Every access | Re-resolves the factory each time the property is read. If the dep is stateful, give it a .cached/.singleton scope or you'll get a fresh instance per access. |
@InjectedObject(\.x) |
At init | SwiftUI only. Wraps StateObject<T: ObservableObject>. View owns the object. |
@InjectedObservable(\.x) |
First access | iOS 17+. For @Observable types. Backed by @State; thunked so it's resolved once per view lifetime. |
@InjectedContainer / @InjectedContainer(MyContainer.self) |
At init | Inject a container reference. |
@InjectedType |
At init | Type-only resolution; requires a Resolving container (see Resolver mode). Optional T?. |
All wrappers accept either \.x (default Container) or \CustomContainer.x.
The projected value of @Injected/@LazyInjected/@WeakLazyInjected exposes .factory, .resolve(reset:), and (for the lazy ones) .resolvedOrNil().
deinit { $myService.resolvedOrNil()?.cleanup() } // doesn't force resolution
When using @Observable (Observation framework), Factory's wrappers must be marked @ObservationIgnored:
@MainActor @Observable
class ContentViewModel {
@ObservationIgnored @Injected(\.myService) private var service
var results: Results = .empty
}
Scopes
Scope = lifetime of resolved instances.
| Scope | Behavior | Reset by |
|---|---|---|
.unique (default) |
New instance every resolve | n/a |
.cached |
One per container, until cache reset | container.manager.reset(scope: .cached) |
.shared |
Weak ref per container; lives only while someone holds strong ref | release strong refs |
.singleton |
One globally — not tied to any container | Scope.singleton.reset() |
.graph |
One per resolution cycle | automatic at cycle end |
.scope(.custom) |
User-defined Cached() instance |
container.manager.reset(scope: .custom) |
Apply with modifier syntax:
self { MyService() }.cached
self { MyService() }.singleton
self { Reachability() }.shared.decorator { print("created \($0)") }
.graph — single-resolution-cycle caching
Use when one concrete type implements multiple protocols and you want a single instance shared across the protocol-typed factories during one resolve:
extension Container {
var consumer: Factory<Consumer> { self { Consumer() } }
var idProvider: Factory<IDProviding> { self { commonImpl() } }
var valueProvider: Factory<ValueProviding> { self { commonImpl() } }
private var commonImpl: Factory<IDProviding & ValueProviding> {
self { CommonImpl() }.graph
}
}
Resolving consumer() will inject the same CommonImpl into both idProvider and valueProvider. Resolving the wrappers separately (e.g. via @Injected in two properties on a hand-constructed Consumer) starts two cycles and gets two instances. The graph requires a single root resolve.
Custom scopes
extension Scope {
static let session = Cached()
}
extension Container {
var authenticatedUser: Factory<User> {
self { User() }.scope(.session)
}
}
func logout() {
Container.shared.manager.reset(scope: .session)
}
Define custom scopes with
let, notstatic var— the latter raises Swift concurrency warnings.
Time to live
self { AuthSession() }.scope(.session).timeToLive(60 * 20) // 20 min
The cached value is discarded on the next resolve after the TTL expires. A successful resolve before expiry refreshes the timestamp.
Default scope per container
extension Container: @retroactive AutoRegistering {
func autoRegister() {
manager.defaultScope = .cached // any unscoped factory becomes cached
}
}
Singleton caveats
- Singletons survive
Container.shared.reset()because they're global. .register { ... }on a singleton normally clears its cache. Inside anautoRegisterblock this clearing is suppressed (otherwise multi-container apps would defeat the singleton).- For test isolation under Swift Testing, the
.containertrait wrapsScope.$singleton.withValue(Scope.singleton.clone())so singletons are isolated per test.
Containers
The default Container
public final class Container: SharedContainer, @unchecked Sendable {
@TaskLocal public static var shared = Container()
public let manager: ContainerManager = ContainerManager()
public init() {}
}
Container.shared is @TaskLocal. That's what makes parallel Swift Testing work — TaskLocal.withValue(...) swaps the shared container for the duration of a task.
Custom containers
public final class PaymentsContainer: SharedContainer {
@TaskLocal public static var shared = PaymentsContainer()
public let manager = ContainerManager()
}
extension PaymentsContainer {
var processor: Factory<PaymentProcessing> { self { Stripe() }.singleton }
}
Rules:
final class- conforms to
SharedContainer(which extendsManagedContainer) @TaskLocal public static var shared- public
let manager = ContainerManager()
Use \CustomContainer.x keypaths to inject from it: @Injected(\PaymentsContainer.processor).
You can also extend SharedContainer itself to expose a factory on every container type:
extension SharedContainer {
var common: Factory<Common> { self { Common() } }
}
Reaching across containers
Just spell out the path:
extension PaymentsContainer {
var something: Factory<Something> {
self { Something(net: Container.shared.network()) }
}
}
AutoRegistering
autoRegister() runs once per container instance, before the first factory on that container resolves. Use it to set up overrides, contexts, default scope, cross-module wiring.
extension Container: @retroactive AutoRegistering {
public func autoRegister() {
accountLoader.register { AccountLoader() }
myService.onArg("mock1") { MockServiceN(1) }
manager.defaultScope = .cached
}
}
The @retroactive is required (Swift 6) since AutoRegistering and Container come from the same imported module — it silences the conformance warning.
reset(options: .all) re-arms the auto-register flag, so autoRegister() runs again after a full reset.
Reset / push / pop
container.myService.reset() // single factory: registration + scope
container.manager.reset() // everything (== container.reset())
container.manager.reset(options: .registration) // factories only, keep caches
container.manager.reset(options: .scope) // caches only, keep registrations
container.manager.reset(options: .context) // contexts only
container.manager.reset(scope: .cached) // one specific scope cache
container.manager.push() // snapshot
container.manager.pop() // restore most recent snapshot
reset() with no args clears everything including contexts. When you only want to clear caches after changing a context, use .reset(.scope).
Modifiers and ordering — "the factory wins"
Modifiers are re-applied every time the computed property runs. The internal definition is applied last, so it wins.
extension Container {
var myService: Factory<MyService> {
self { MyService() }
.singleton
.onTest { MockAnalytics() } // baked-in onTest
}
}
// Later, in a test, this looks like it should override:
Container.shared.myService.onTest { NullAnalytics() }
let svc = Container.shared.myService() // → MockAnalytics, not Null
Why: reading myService rebuilds the Factory and re-applies .onTest { MockAnalytics() } on top of your override.
Three ways to deal with it:
- Don't bake mutable concerns into the Factory definition. Move contexts to
autoRegister().
extension Container {
var myService: Factory<MyService> { self { MyService() }.singleton }
}
extension Container: @retroactive AutoRegistering {
func autoRegister() {
#if DEBUG
myService.onTest { MockAnalytics() }
#endif
}
}
Chain at the call site:
Container.shared.myService.onTest { Null() }()— but you'd have to do this everywhere..once()— anything before.once()only applies on the first construction:
self { MyService() }
.singleton
.onTest { MockAnalytics() }
.once()
Now external .onTest { ... } calls stick. .once() is the escape hatch; rule of thumb is "prefer option 1".
When you change a context on a scoped factory after first resolution, you must also clear its cached instance:
Container.shared.myService.onTest { NullAnalytics() }.reset(.scope)
Contexts
A context is a runtime condition that selects a registration override. Defined contexts:
| Context | When | Available in Release? |
|---|---|---|
.arg("x") |
ProcessInfo.arguments contains "x" or FactoryContext.setArg("x", forKey:) was called |
Yes |
.args(["x", "y"]) |
any of the listed args | Yes |
.preview |
Xcode SwiftUI Previews | DEBUG only |
.test |
XCTest / Swift Testing process | DEBUG only |
.debug |
DEBUG build | DEBUG only |
.simulator |
running in simulator | Yes |
.device |
running on device | Yes |
Shortcuts: .onArg(_:), .onArgs(_:), .onPreview, .onTest, .onDebug, .onSimulator, .onDevice.
container.analytics
.onTest { MockAnalytics() }
.onPreview { MockAnalytics() }
.onArg("mock1") { MockServiceN(1) }
Precedence (highest → lowest)
arg/argspreview(DEBUG only)test(DEBUG only)simulatordevicedebug(DEBUG only)- registered factory (
.register { ... }) - original factory closure
Runtime args
FactoryContext.setArg("dark", forKey: "theme")
FactoryContext.removeArg(forKey: "theme")
theme
.onArg("light") { LightTheme() }
.onArg("dark") { DarkTheme() }
SwiftUI
View models
If the VM uses @Injected internally, just @StateObject-construct it normally:
struct ContentView: View {
@StateObject private var vm = ContentViewModel() // VM injects its own deps
}
If you want the container to construct the VM (e.g. constructor injection of services into the VM), use @InjectedObject:
extension Container {
var contentViewModel: Factory<ContentViewModel> {
self { ContentViewModel(service: self.myService()) }
}
}
struct ContentView: View {
@InjectedObject(\.contentViewModel) private var vm
}
@InjectedObject wraps a StateObject — the view owns the VM's lifecycle.
Observation (@Observable, iOS 17+)
@MainActor @Observable
class ContentViewModel {
@ObservationIgnored @Injected(\.myService) private var service
var results: Results = .empty
}
extension Container {
@MainActor
var contentViewModel: Factory<ContentViewModel> { self { ContentViewModel() } }
}
struct ContentView: View {
@InjectedObservable(\.contentViewModel) var vm
}
@InjectedObservable is backed by @State<ThunkedValue<T>>; the dep is created lazily on first read of wrappedValue, then memoized for the view's lifetime. Its projected value is a Binding<T> (read-only setter).
In Factory 3.0, a @MainActor factory only needs the annotation on the property — not on the closure. (2.x required self { @MainActor in ... }; that form is no longer needed.)
Previews
#Preview {
Container.shared.myService.preview { MockServiceN(4) }
ContentView()
}
.preview { ... } wraps register and returns EmptyView, so let _ = is unnecessary. For multiple registrations:
#Preview {
Container.preview {
$0.myService.register { MockServiceN(4) }
$0.anotherService.register { MockAnother() }
}
ContentView()
}
A common pattern is a setupMocks() extension shared across previews and tests:
extension Container {
func setupMocks() {
myService.register { MockServiceN(4) }
sharedService.register { MockShared() }
}
}
Testing
Swift Testing (preferred — supports parallel execution)
Add FactoryTesting to the test target. Use the .container trait:
import Testing
import FactoryTesting
@Suite(.container)
struct AccountTests {
@Test func loaded() async {
Container.shared.accountProvider.register { MockProvider(.sample) }
let vm = Container.shared.accountsViewModel()
await vm.load()
#expect(vm.isLoaded)
}
@Test(.container, arguments: Parameters.allCases)
func parameterized(p: Parameters) async {
Container.shared.someService.register { MockService(parameter: p) }
#expect(Container.shared.someService().parameter == p)
}
}
Each test gets:
- a fresh
Container()set asContainer.sharedfor that task (viaTaskLocal.withValue) - a cloned singleton scope (via
Scope.$singleton.withValue(Scope.singleton.clone()))
Tests run in parallel without stomping on each other.
The trait can take a transforming closure for setup right next to the trait:
@Test(.container {
$0.someService.register { ErrorService() }
await $0.mainActorService.register { MockMainActor() } // await for actor-isolated factories
}) func t() async { ... }
Custom container traits
public final class CustomContainer: SharedContainer {
@TaskLocal public static var shared = CustomContainer()
public let manager = ContainerManager()
}
extension Trait where Self == ContainerTrait<CustomContainer> {
public static var customContainer: ContainerTrait<CustomContainer> {
.init(shared: CustomContainer.$shared, container: .init())
}
}
@Test(.customContainer) func t() async { ... }
@Test(.container, .customContainer) func t() async { ... } // multiple
XCTest
No TaskLocal magic — tests don't run in parallel by default with XCTest's classic scheduler. Manage state with reset or push/pop:
final class AccountTests: XCTestCase {
override func setUp() {
super.setUp()
Container.shared.manager.push()
Container.shared.setupMocks()
}
override func tearDown() {
Container.shared.manager.pop()
super.tearDown()
}
func testLoaded() async {
Container.shared.accountLoading.register { MockNoAccounts() }
let vm = Container.shared.accountsViewModel()
await vm.load()
XCTAssertTrue(vm.isEmpty)
}
}
Or with an injected container:
final class AccountTests: XCTestCase {
var container: Container!
override func setUp() { container = Container(); container.setupMocks() }
func test() {
container.someService.register { MockService() }
let vm = AccountsViewModel(container: container)
...
}
}
If any dep is a .singleton, container injection alone isn't enough — singletons are global. Use .container trait or Scope.singleton.reset().
UI testing
Pass a launch arg, react via context:
// UI test
let app = XCUIApplication()
app.launchArguments.append("mock1")
app.launch()
// App
extension Container: @retroactive AutoRegistering {
public func autoRegister() {
#if DEBUG
myServiceType.onArg("mock1") { MockServiceN(1) }
#endif
}
}
Cross-module wiring
The recurring problem: protocol in module P, impl in module B, consumer in module A — A and B can't see each other.
Solution: P (or a thin Services module above P) declares the Factory; the app target wires the impl. Three flavors:
// 1. Optional via promised() — preferred
extension Container {
public var loader: Factory<AccountLoading?> { promised() }
}
// 2. Optional with explicit nil
extension Container {
public var loader: Factory<AccountLoading?> { self { nil } }
}
// 3. Optional with fatalError — fail-fast in dev, but ships crashes
extension Container {
public var loader: Factory<AccountLoading?> { self { fatalError() } }
}
Use promised() unless you have a reason. It crashes in DEBUG (developer notices immediately), returns nil in release (feature degrades, app survives).
App target wires it:
import ModuleP
import ModuleA
import ModuleB
extension Container: @retroactive AutoRegistering {
func autoRegister() {
loader.register { AccountLoader() }
}
}
If the protocol module is in the same target as the impl, the simpler "public protocol + public Factory + private impl" pattern is fine — no nullable needed.
For tagged groups of dependencies (Factory has no built-in tag system), maintain a KeyPath array:
extension Container {
static var processors: [KeyPath<Container, Factory<Processor>>] = [
\.processor1, \.processor2,
]
func processors() -> [Processor] {
Container.processors.map { self[keyPath: $0]() }
}
}
Concurrency
- The package builds under Swift 6 with strict concurrency.
Container.sharedis@TaskLocal var. Reading it across a Task suspension point is fine; writing it directly isn't supported (and as of 2.2 the defaultContainer.sharedcan't be reassigned). UseContainer.$shared.withValue(...)to set scoped values.- For an actor-isolated dep, annotate the Factory property:
extension Container {
@MainActor var vm: Factory<ContentViewModel> { self { ContentViewModel() } }
}
Don't put @MainActor inside the closure as in 2.x — 3.0 simplified that.
- For
nonisolatedconsumers under Swift 6.2 where the property wrappers misbehave, use the globaldependencyfunction:
nonisolated final class NetworkService: Sendable {
let prefs: Preferences = dependency(\.preferences)
lazy var svc: Service = dependency(\.service, parameter: Mode.secret)
}
This is also useful when you want to wrap Factory behind your own seam — dependency keypaths can be rewritten to a different DI system later.
Functional injection
Factories can return closures, not just objects. This sidesteps protocol-based mocking entirely for one-method services.
typealias AccountProviding = () async throws -> [Account]
extension Container {
var accountProvider: Factory<AccountProviding> {
self {{ try await Network.get(path: "/accounts") }} // double braces!
}
}
class AccountVM {
@Injected(\.accountProvider) var provide
@MainActor func load() async {
accounts = (try? await provide()) ?? []
}
}
// In tests
Container.shared.accountProvider.register {{ Account.mocks }}
Container.shared.accountProvider.register {{ throw APIError.network }}
The double braces are unavoidable: the outer braces are the factory closure, the inner braces are the closure being returned.
Debugging
Resolution trace
Container.shared.manager.trace.toggle()
let svc = Container.shared.someRoot()
Output:
0: FactoryKit.Container.cycleDemo<CycleDemo> = N:1055...696
1: FactoryKit.Container.aService<AServiceType> = N:1055...680
2: FactoryKit.Container.implementsAB<AServiceType & BServiceType> = N:1055...680
3: FactoryKit.Container.networkService<NetworkService> = N:1055...688
1: FactoryKit.Container.bService<BServiceType> = N:1055...680
2: FactoryKit.Container.implementsAB<AServiceType & BServiceType> = C:1055...680
N: = newly created. C: = pulled from cache. The integer is the depth in the resolution cycle.
Trace is global (covers all containers). Custom logger:
Container.shared.manager.logger = { msg in MyLogger.debug("Factory: \(msg)") }
Trace is DEBUG-only.
Circular dependency detection
DEBUG-only. If A → B → C → A, Factory hits a fatalError with the chain. To investigate, turn on trace before resolving and read the depth indentation. Common fix: switch one of the wrappers to @LazyInjected or @WeakLazyInjected, or — better — extract a third type that the cycle's two endpoints both depend on.
Disable detection:
Container.shared.manager.circularDependencyTesting = false
Decorators
Run code on every resolution (cached or fresh):
self { ParentChildService() }
.decorator { instance in instance.child.parent = instance }
self { Service() }
.decorator { (instance, isNew) in if isNew { logger.log(instance) } }
Container-wide decorator (sees every dep resolved by the container):
Container.shared.decorator { resolved in print("resolved: \(type(of: resolved))") }
Resolver mode (typed registration)
Opt-in Resolving protocol gives you Resolver-style runtime register/resolve by T.Type:
extension Container: Resolving {}
Container.shared.register { MyService() as MyServiceType }
let svc: MyServiceType? = Container.shared.resolve()
This is provided for migration from Resolver and isn't the recommended idiom. Stick with keypaths — they're compile-time safe.
Common gotchas (checklist)
When debugging Factory code, walk this list:
- Did you import
FactoryKit(notFactory)? In the test target, did you importFactoryTestinginstead? - Does the registration live on the correct container?
@Injected(\.x)looks atContainer.shared; for a custom container use@Injected(\CustomContainer.x). - Is the override unexpectedly losing? If the Factory definition bakes in
.onTest/.singletonand you're trying to override at a call site, re-read "the factory wins" — move the override toautoRegister(), or add.once(), or chain at the resolve site. - Did you
registeron a singleton at runtime and not see the change?registerclears scope normally, but insideautoRegisterit doesn't (singletons must survive container instantiation). - Did
reset()wipe more than you wanted?reset()≡reset(options: .all)— clears registrations, caches, and contexts. Usereset(.scope)after a context change. @MainActorwarning on a Factory? Annotate the computed property with@MainActor(3.0). Do not add@MainActor ininside the closure (that was 2.x).lazy varFactory on a custom container → retain cycle. Use a computed property.- Test bleed between Swift Testing tests? Use the
.containertrait. Make sure your custom container'ssharedis@TaskLocal. - Singleton not reset between tests? Singletons are global. Use
.containertrait (it clones the singleton scope) or callScope.singleton.reset()explicitly. ParameterFactorycached but parameter ignored? Default scope behavior caches the first resolved value. Add.scopeOnParameters(P must beHashable).- Cross-module wire missing in production? Prefer
promised()overfatalError()factories — degrades gracefully. - Optional
@Injected(\.x)for aFactory<T?>? Works directly — there's no@OptionalInjected. Just spell the property type asT?(or let inference handle it).
Where each topic is documented in the package
| Topic | File |
|---|---|
| Quickstart | Sources/FactoryKit/FactoryKit.docc/Basics/GettingStarted.md |
| Containers, lifecycle, AutoRegistering | Basics/Containers.md |
| Registration patterns | Basics/Registrations.md |
| Resolution patterns | Basics/Resolutions.md |
| Scopes (incl. graph, TTL, scopeOnParameters) | Basics/Scopes.md |
| SwiftUI integration | Development/SwiftUI.md |
| Previews | Development/Previews.md |
| Testing (Swift Testing + XCTest + UITest) | Development/Testing.md |
| Contexts | Development/Contexts.md |
| Resolution trace + debugging | Development/Debugging.md |
| Resolution cycles + graph scope | Development/Chains.md, Advanced/Cycle.md |
Modifier ordering, .once() |
Advanced/Modifiers.md |
| Multi-module wiring | Advanced/Modules.md |
Optionals + promised() |
Advanced/Optionals.md |
| Functional injection | Advanced/Functional.md |
| Tagging pattern | Advanced/Tags.md |
| Design rationale (why 1.x → 2.x → 3.x) | Advanced/Design.md, Additional/Migration.md |
先判断是否适合
作者设计意图
作者的方法与取舍
边界和复核