@capgo/capgo-skills
Version:
55 agent skills for Capacitor mobile development
143 lines (109 loc) • 7.55 kB
Markdown
# iOS 26 and iOS 27 App Intents APIs
Capacitor 8 deploys to iOS 15 and Capacitor 9 to iOS 16, so every API here needs an availability gate. Never drop the older path while the deployment target is below the API's floor.
## Availability
| API | iOS |
|---|---|
| `supportedModes` / `IntentModes` (`.background`, `.foreground`, `.foreground(.immediate / .deferred / .dynamic)`) | 26.0 |
| `continueInForeground(_:alwaysConfirm:)`, `needsToContinueInForegroundError(_:alwaysConfirm:)`, `systemContext.currentMode.canContinueInForeground` | 26.0 |
| `UndoableIntent` | 26.0 |
| `requestChoice(between:dialog:)` / `IntentChoiceOption` | 26.0 |
| `SnippetIntent` (interactive snippets) | 26.0 |
| `IntentValueQuery` for Visual Intelligence (`import VisualIntelligence`) | 26.0 |
| ``, `` (incl. `indexingKey:`) | 26.0 |
| `CancellableIntent` / `IntentCancellationReason` | 26.4 |
| `LongRunningIntent`, `performBackgroundTask(options:operation:)` | 27.0 |
| `allowedExecutionTargets` / `IntentExecutionTargets` | 27.0 |
| `IndexedEntityQuery`, `relatedAppEntityIdentifier` | 27.0 |
| `RelevantEntities` / `AppEntityContext` | 27.0 |
| `SyncableEntity`, `EntityOwnership` | 27.0 |
| `EntityCollection` | 27.0 |
| `AppIntentsTesting` framework | 27.0 (test targets) |
| Baseline for comparison: `OpenIntent` 16.0, `OpenURLIntent` / URL-representable 18.0, `IndexedEntity` 18.0, `(indexingKey:)` 18.4 | |
## `supportedModes` replaces `openAppWhenRun`
`openAppWhenRun` is deprecated in iOS 26. It still works. Keep it while the deployment target is below 26, and add `supportedModes` for iOS 26+:
```swift
@available(iOS 16.0, *)
struct OpenSectionIntent: AppIntent {
static let title: LocalizedStringResource = "Open Section"
static let openAppWhenRun = true // iOS 16-25
@available(iOS 26.0, *)
static var supportedModes: IntentModes { .foreground } // iOS 26+
(title: "Section") var section: AppSection
func perform() async throws -> some IntentResult {
PendingRouteStore.shared.set("/\(section.rawValue)")
return .result()
}
}
```
Build with the current SDK and confirm that the availability-gated witness compiles and the deprecation warning is the only one. Once the deployment target reaches 26, delete `openAppWhenRun`.
Mode guide for Capacitor apps:
- `.background`: native-only work. No WebView exists on a headless launch. This is the default when neither property is set.
- `.foreground` (= `.foreground(.immediate)`): the app comes forward before `perform()`. Use it for "open X" intents that hand a route to the web layer.
- `[.background, .foreground(.dynamic)]`: try headless, then escalate only when the web UI is needed:
```swift
@available(iOS 26.0, *)
struct ReviewOrderIntent: AppIntent {
static let title: LocalizedStringResource = "Review Order"
static var supportedModes: IntentModes { [.background, .foreground(.dynamic)] }
(title: "Order Number") var orderNumber: String
func perform() async throws -> some IntentResult & ProvidesDialog {
let status = try await OrdersAPI.status(orderNumber) // native, headless
guard status.needsReview else { return .result(dialog: "Order is \(status.label).") }
guard systemContext.currentMode.canContinueInForeground else {
throw needsToContinueInForegroundError("Open the app to review this order")
}
try await continueInForeground("Review in the app?", alwaysConfirm: false)
PendingRouteStore.shared.set("/orders/\(orderNumber)/review")
return .result(dialog: "Opening the order.")
}
}
```
Some contexts (voice-only, some widgets) cannot foreground. Always check `canContinueInForeground` before calling `continueInForeground`; that call throws in such contexts.
## Long-running work (iOS 27)
Background intents get about 30 seconds on iOS. `LongRunningIntent` moves the work into a system-managed background task with a Live Activity that shows `progress` automatically:
```swift
@available(iOS 27.0, *)
struct ExportDataIntent: AppIntent, LongRunningIntent {
static let title: LocalizedStringResource = "Export Data"
static var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult {
progress.localizedDescription = "Exporting…"
try await performBackgroundTask {
try await Exporter.run { done, total in
self.progress.totalUnitCount = Int64(total)
self.progress.completedUnitCount = Int64(done)
}
}
return .result()
}
}
```
Add `CancellableIntent` (26.4) to get the `onCancel:` overload with an `IntentCancellationReason` (`.timeout` or `.userCancelled`).
## Execution targets (iOS 27)
`static var allowedExecutionTargets: IntentExecutionTargets` pins the process: `.default`, `.main`, `.appIntentsExtension`, `.widgetKitExtension`. In a Capacitor app, use `.main` only when the intent needs main-process state (the pending-route store, an in-memory cache). It does not create a WebView by itself. Prefer `.default`.
## Spotlight
- `IndexedEntity` (iOS 18) + `CSSearchableIndex.default().indexAppEntities(_:)` puts entities in Spotlight. Re-index when data changes. In Capacitor, call it from a plugin method after JS saves data.
- iOS 26: `(indexingKey:)` / `(indexingKey:)` map derived values to Spotlight attributes.
- iOS 27: `IndexedEntityQuery` lets the system ask you to reindex. `relatedAppEntityIdentifier` links an independently indexed `CSSearchableItem` to an entity.
## Apple Intelligence schemas
`(schema:)`, `(schema:)`, `(schema:)` (macros iOS 18) give an intent a fixed shape the system can call. The older `` / `` / `` family is deprecated and renamed to the `*` forms. Which schema domains are public depends on the SDK. Check the current Apple docs for the domain before you promise a surface to the user.
## Interactive snippets (iOS 26)
Return `.result(snippetIntent:)` with a `SnippetIntent` to show an interactive card with `Button(intent:)` controls. A `SnippetIntent`'s `perform()` may run many times to redraw. Keep it side-effect free.
## Testing with AppIntentsTesting (iOS 27)
`import AppIntentsTesting` runs intents and queries **out of process against the installed app**, as Siri does. It is XCTest-based and links only into test targets:
```swift
import XCTest
import AppIntentsTesting
@available(iOS 27.0, *)
final class IntentTests: XCTestCase {
let definitions = IntentDefinitions(bundleIdentifier: "com.example.app") // the app, not the test bundle
func testOpenOrder() async throws {
let intent = definitions.intents["OpenOrderIntent"]
try await intent.makeIntent(orderNumber: "1042").run()
}
}
```
- Address intents by type name and pass parameters by their `` label.
- There is no in-process dependency injection. Seed data through the app (for example a debug-only seed intent).
- Host the tests in a test target of `App.xcodeproj` (File > New > Target > Unit Testing Bundle or UI Testing Bundle). Set its deployment target to 27, or gate the class with `@available`. `viewAnnotations()` needs a launched `XCUIApplication`, so it requires a UI testing bundle.
- For parameters of enum and entity types, check the AppIntentsTesting docs for how to pass values (`makeReference(identifier:)` for entities).