How do App Intents get their dependencies, and why does @Dependency crash in a unit test?
Predict first
What happens when the dependency is set by hand, and when it is only registered?
import AppIntents
import Foundation
final class NoteStore: @unchecked Sendable { var notes: [String] = [] }
struct AddNote: AppIntent {
static let title: LocalizedStringResource = "Add Note"
@Parameter(title: "Text") var text: String
@Dependency var store: NoteStore
func perform() async throws -> some IntentResult & ReturnsValue<Int> {
store.notes.append(text)
return .result(value: store.notes.count)
}
}
var intent = AddNote()
intent.text = "milk"
intent.store = NoteStore()
print("first:", try await intent.perform().value as Any)
print("second:", try await intent.perform().value as Any)
- first: Optional(1) / second: Optional(2) when assigned by hand
- It crashes even when assigned by hand
- It prints nil
Mental model. An intent is a contractor sent by the system: it arrives with no tools, and the dependency manager is the site cabinet it takes them from, but only once it is on site.
The system builds an intent with its zero-argument init, so a store or service cannot be passed in. @Dependency declares what the intent needs, and the app registers instances at launch with AppDependencyManager.shared.add(dependency:); the system resolves them just before perform(). This is only valid inside the intent perform flow. In a standalone program (an @main type, run once and not part of verify.py) that built the intent, registered the dependency and then called perform() directly, the process trapped (a variant that registered first resolved fine, so the outcome depends on when the system-less instance is created; do not rely on it) with “was not initialized prior to access. Dependency values can only be accessed inside of the intent perform flow … unless the value of the dependency is manually set prior to access.”
That message is also the fix: in a unit test assign the property yourself (intent.store = NoteStore()), as the first check does (values 1 then 2). Design consequences: register dependencies in the app’s init (the intent can run before any UI exists), keep them cheap to create, and remember that an intent running in a widget or App Intents extension process (Q407, Q408) needs its own registration in that process, because the app’s singletons are not there (Q401).
Manually setting the property is allowed outside the perform flow.
Common traps
- Registering in the app and expecting it in the extension. Separate process, separate manager.
- Calling
perform()in a test without setting the property. It traps rather than throws.
They might follow up with
- Can the dependency be a protocol? Register and require a concrete type, or wrap the protocol in a class.
Related questions
- 19.5.1 What are App Intents, App Entities and App Shortcuts, how do they replace SiriKit and Intents extensions, and…
- 19.4.2 How do configurable widgets, interactive buttons and Control Center controls work with App Intents?
- 19.1.1 What is an app extension, how is it launched, and what limits do extensions run under?
How do AppEnum, AppEntity and EntityQuery work together, and how do… →