@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/docs/facts.json +5 -5
- package/manifest.json +52 -31
- package/package.json +1 -1
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
|
@@ -122,10 +122,6 @@ types. Framework: `IntentPerson`, `IntentFile`. Custom: any `AppEntity` or
|
|
|
122
122
|
### Common initializer patterns
|
|
123
123
|
|
|
124
124
|
```swift
|
|
125
|
-
// Basic
|
|
126
|
-
@Parameter(title: "Name")
|
|
127
|
-
var name: String
|
|
128
|
-
|
|
129
125
|
// With default
|
|
130
126
|
@Parameter(title: "Count", default: 5)
|
|
131
127
|
var count: Int
|
|
@@ -137,17 +133,11 @@ var volume: Int
|
|
|
137
133
|
// Options provider (dynamic list)
|
|
138
134
|
@Parameter(title: "Category", optionsProvider: CategoryOptionsProvider())
|
|
139
135
|
var category: Category
|
|
140
|
-
|
|
141
|
-
// File with content types
|
|
142
|
-
@Parameter(title: "Document", supportedContentTypes: [.pdf, .plainText])
|
|
143
|
-
var document: IntentFile
|
|
144
|
-
|
|
145
|
-
// Measurement with unit
|
|
146
|
-
@Parameter(title: "Distance", defaultUnit: .miles, supportsNegativeNumbers: false)
|
|
147
|
-
var distance: Measurement<UnitLength>
|
|
148
136
|
```
|
|
149
137
|
|
|
150
|
-
|
|
138
|
+
Options providers, files, measurements, resolvers, disambiguation dialogs, and
|
|
139
|
+
input-connection behavior follow the same shape. See
|
|
140
|
+
[references/appintents-advanced.md](references/appintents-advanced.md) for all initializer variants.
|
|
151
141
|
|
|
152
142
|
## AppEntity
|
|
153
143
|
|
|
@@ -195,52 +185,19 @@ struct SoupEntityQuery: EntityQuery {
|
|
|
195
185
|
|
|
196
186
|
### 2. EntityStringQuery (free-text search)
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
struct SoupStringQuery: EntityStringQuery {
|
|
200
|
-
func entities(matching string: String) async throws -> [SoupEntity] {
|
|
201
|
-
SoupStore.shared.search(string).map { SoupEntity(from: $0) }
|
|
202
|
-
}
|
|
203
|
-
func entities(for identifiers: [String]) async throws -> [SoupEntity] {
|
|
204
|
-
SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
188
|
+
Add `entities(matching string:)` alongside ID resolution for free-text search.
|
|
208
189
|
|
|
209
190
|
### 3. EnumerableEntityQuery (finite set)
|
|
210
191
|
|
|
211
|
-
|
|
212
|
-
struct AllSoupsQuery: EnumerableEntityQuery {
|
|
213
|
-
func allEntities() async throws -> [SoupEntity] {
|
|
214
|
-
SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
|
|
215
|
-
}
|
|
216
|
-
func entities(for identifiers: [String]) async throws -> [SoupEntity] {
|
|
217
|
-
SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
```
|
|
192
|
+
Implement `allEntities()` when the full set is small and enumerable.
|
|
221
193
|
|
|
222
194
|
### 4. UniqueAppEntityQuery (singleton, iOS 18+)
|
|
223
195
|
|
|
224
|
-
Use for single-instance entities like app settings
|
|
196
|
+
Use for single-instance entities like app settings; implement `uniqueEntity()`.
|
|
225
197
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
|
|
230
|
-
var displayRepresentation: DisplayRepresentation { "App Settings" }
|
|
231
|
-
|
|
232
|
-
var id: String { "app-settings" }
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
struct AppSettingsQuery: UniqueAppEntityQuery {
|
|
236
|
-
func uniqueEntity() async throws -> AppSettingsEntity {
|
|
237
|
-
AppSettingsEntity()
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
See [references/appintents-advanced.md](references/appintents-advanced.md) for `EntityPropertyQuery` with
|
|
243
|
-
filter/sort support.
|
|
198
|
+
See [references/appintents-advanced.md](references/appintents-advanced.md) for full `EntityStringQuery`,
|
|
199
|
+
`EnumerableEntityQuery`, and `UniqueAppEntityQuery` examples, plus `EntityPropertyQuery` with
|
|
200
|
+
filter and sort support.
|
|
244
201
|
|
|
245
202
|
## AppEnum
|
|
246
203
|
|
|
@@ -377,36 +334,17 @@ struct LightControlConfig: ControlConfigurationIntent {
|
|
|
377
334
|
static var title: LocalizedStringResource = "Light Control"
|
|
378
335
|
@Parameter(title: "Light", default: .livingRoom) var light: LightEntity
|
|
379
336
|
}
|
|
380
|
-
|
|
381
|
-
struct ToggleLightIntent: AppIntent {
|
|
382
|
-
static var title: LocalizedStringResource = "Toggle Light"
|
|
383
|
-
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
|
|
384
|
-
|
|
385
|
-
@Parameter(title: "Light") var light: LightEntity
|
|
386
|
-
|
|
387
|
-
func perform() async throws -> some IntentResult {
|
|
388
|
-
try await requestConfirmation(
|
|
389
|
-
actionName: .toggle,
|
|
390
|
-
dialog: "Toggle \(light.name)?"
|
|
391
|
-
)
|
|
392
|
-
try await LightService.shared.toggle(light.id)
|
|
393
|
-
return .result()
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
struct LightControl: ControlWidget {
|
|
398
|
-
var body: some ControlWidgetConfiguration {
|
|
399
|
-
AppIntentControlConfiguration(kind: "LightControl", intent: LightControlConfig.self) { config in
|
|
400
|
-
ControlWidgetToggle(config.light.name, isOn: config.light.isOn, action: ToggleLightIntent(light: config.light))
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
337
|
```
|
|
405
338
|
|
|
339
|
+
See [references/appintents-advanced.md](references/appintents-advanced.md) ("Control Center Widget
|
|
340
|
+
Implementation") for the full `ControlWidget` + action-intent wiring with
|
|
341
|
+
confirmation and authentication.
|
|
342
|
+
|
|
406
343
|
## Spotlight and IndexedEntity (iOS 18+)
|
|
407
344
|
|
|
408
345
|
Conform to `IndexedEntity` for Spotlight search. On iOS 26+, use `indexingKey`
|
|
409
|
-
for structured
|
|
346
|
+
to map a property to a `CSSearchableItemAttributeSet` key path for structured
|
|
347
|
+
metadata:
|
|
410
348
|
|
|
411
349
|
```swift
|
|
412
350
|
struct RecipeEntity: IndexedEntity {
|
|
@@ -421,116 +359,43 @@ struct RecipeEntity: IndexedEntity {
|
|
|
421
359
|
var displayRepresentation: DisplayRepresentation {
|
|
422
360
|
DisplayRepresentation(title: "\(name)")
|
|
423
361
|
}
|
|
424
|
-
|
|
425
|
-
var attributeSet: CSSearchableItemAttributeSet {
|
|
426
|
-
let attrs = defaultAttributeSet
|
|
427
|
-
attrs.keywords = ["recipe"]
|
|
428
|
-
return attrs
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
struct RecipeQuery: EntityQuery {
|
|
433
|
-
func entities(for identifiers: [RecipeEntity.ID]) async throws -> [RecipeEntity] {
|
|
434
|
-
identifiers.compactMap { id in
|
|
435
|
-
RecipeStore.shared.recipe(id: id).map(RecipeEntity.init)
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
struct OpenRecipeIntent: OpenIntent {
|
|
441
|
-
static var title: LocalizedStringResource = "Open Recipe"
|
|
442
|
-
@Parameter(title: "Recipe") var target: RecipeEntity
|
|
443
362
|
}
|
|
444
363
|
```
|
|
445
364
|
|
|
446
365
|
`IndexedEntity` describes metadata; still index instances in a named Spotlight
|
|
447
|
-
index, e.g. `CSSearchableIndex(name: "...").indexAppEntities(entities)`.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
```swift
|
|
454
|
-
let recipeIndex = CSSearchableIndex(name: "Recipes")
|
|
455
|
-
try await recipeIndex.indexAppEntities(changedRecipes)
|
|
456
|
-
try await recipeIndex.deleteAppEntities(
|
|
457
|
-
identifiedBy: deletedRecipeIDs,
|
|
458
|
-
ofType: RecipeEntity.self
|
|
459
|
-
)
|
|
460
|
-
```
|
|
366
|
+
index, e.g. `CSSearchableIndex(name: "...").indexAppEntities(entities)`. Provide
|
|
367
|
+
an `OpenIntent` for the type so results open the right content. If you customize
|
|
368
|
+
`attributeSet`, start from `defaultAttributeSet`; returning a fresh attribute set
|
|
369
|
+
replaces display representation and property-derived metadata. Prefer
|
|
370
|
+
`indexingKey` for metadata already exposed on the entity.
|
|
461
371
|
|
|
462
|
-
|
|
463
|
-
|
|
372
|
+
See [references/appintents-advanced.md](references/appintents-advanced.md) for the full entity plus query,
|
|
373
|
+
the available `@Property` / `@ComputedProperty` indexing keys, updating and
|
|
374
|
+
deleting indexed entities, batch indexing with client state for crash recovery,
|
|
375
|
+
and direct Core Spotlight usage.
|
|
464
376
|
|
|
465
377
|
## iOS 26 Additions
|
|
466
378
|
|
|
467
379
|
### SnippetIntent
|
|
468
380
|
|
|
469
|
-
Display interactive snippets in system UI
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return .result(view: OrderStatusSnippetView(status: status))
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
struct CheckOrderStatusIntent: AppIntent {
|
|
481
|
-
static var title: LocalizedStringResource = "Check Order Status"
|
|
482
|
-
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
|
|
483
|
-
.result(snippetIntent: OrderStatusSnippet())
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
The system may call `perform()` multiple times, including after snippet button
|
|
489
|
-
or toggle actions; keep `SnippetIntent.perform()` side-effect-free and do
|
|
490
|
-
mutations in the calling action intent or a separate button/toggle action. A
|
|
491
|
-
snippet-only intent is not discoverable in Shortcuts or Spotlight unless
|
|
492
|
-
`isDiscoverable` is `true`.
|
|
381
|
+
Display interactive snippets in system UI. The system may call `perform()`
|
|
382
|
+
multiple times, including after snippet button or toggle actions; keep
|
|
383
|
+
`SnippetIntent.perform()` side-effect-free and do mutations in the calling action
|
|
384
|
+
intent or a separate button/toggle action. A snippet-only intent is not
|
|
385
|
+
discoverable in Shortcuts or Spotlight unless `isDiscoverable` is `true`. See
|
|
386
|
+
[references/appintents-advanced.md](references/appintents-advanced.md) ("SnippetIntent Implementation")
|
|
387
|
+
for the `ShowsSnippetView` / `ShowsSnippetIntent` example.
|
|
493
388
|
|
|
494
389
|
### IntentValueQuery (Visual Intelligence)
|
|
495
390
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
@available(iOS 26, *)
|
|
505
|
-
struct ShoppingVisualQuery: IntentValueQuery {
|
|
506
|
-
func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
|
|
507
|
-
try Task.checkCancellation()
|
|
508
|
-
async let productMatches = ProductStore.shared.matches(
|
|
509
|
-
labels: input.labels,
|
|
510
|
-
pixelBuffer: input.pixelBuffer,
|
|
511
|
-
limit: 5
|
|
512
|
-
)
|
|
513
|
-
async let storeMatches = StoreStore.shared.matches(
|
|
514
|
-
labels: input.labels,
|
|
515
|
-
pixelBuffer: input.pixelBuffer,
|
|
516
|
-
limit: 3
|
|
517
|
-
)
|
|
518
|
-
let ranked = await rank(productMatches, storeMatches)
|
|
519
|
-
return Array(ranked.prefix(8))
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
Only one `IntentValueQuery` can take `SemanticContentDescriptor`; use
|
|
525
|
-
`@UnionValue` when one query must return multiple app entity types. Treat
|
|
526
|
-
`labels` as high-level English descriptors, not exhaustive synonyms or app
|
|
527
|
-
taxonomy; combine them with `pixelBuffer` when available. Return small, ranked,
|
|
528
|
-
cancellation-friendly results, and provide an `OpenIntent`, URL representation,
|
|
529
|
-
or in-app search handoff for details and more results. Do not implement camera
|
|
530
|
-
capture, Vision `VN*` requests, barcode classification, or Spotlight indexing
|
|
531
|
-
inside the App Intents query; call an existing bounded app search or image-match
|
|
532
|
-
service instead, with explicit result caps and timeouts when work may exceed a
|
|
533
|
-
system UI budget.
|
|
391
|
+
Return app entities for a Visual Intelligence `SemanticContentDescriptor`. Only
|
|
392
|
+
one `IntentValueQuery` can take `SemanticContentDescriptor`; use `@UnionValue`
|
|
393
|
+
when one query must return multiple app entity types. Return small, ranked,
|
|
394
|
+
cancellation-friendly results and provide an opening path (`OpenIntent`, URL, or
|
|
395
|
+
in-app search). Do not run camera capture, Vision `VN*` requests, or Spotlight
|
|
396
|
+
indexing inside the query; call a bounded app search or image-match service
|
|
397
|
+
instead. See [references/appintents-advanced.md](references/appintents-advanced.md) ("Visual Intelligence
|
|
398
|
+
IntentValueQuery") for the full `@UnionValue` + query example and guidance.
|
|
534
399
|
|
|
535
400
|
## Common Mistakes
|
|
536
401
|
|
|
@@ -8,7 +8,11 @@ URL-representable types, and Spotlight indexing.
|
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
10
10
|
- [`@Parameter Initializer Variants`](#parameter-initializer-variants)
|
|
11
|
+
- [EntityQuery Variants (Full Examples)](#entityquery-variants-full-examples)
|
|
11
12
|
- [EntityPropertyQuery (Filter and Sort)](#entitypropertyquery-filter-and-sort)
|
|
13
|
+
- [Control Center Widget Implementation](#control-center-widget-implementation)
|
|
14
|
+
- [SnippetIntent Implementation (iOS 26+)](#snippetintent-implementation-ios-26)
|
|
15
|
+
- [Visual Intelligence IntentValueQuery (iOS 26+)](#visual-intelligence-intentvaluequery-ios-26)
|
|
12
16
|
- [Assistant Schemas (iOS 18+)](#assistant-schemas-ios-18)
|
|
13
17
|
- [Focus Filter Intents](#focus-filter-intents)
|
|
14
18
|
- [SiriKit Migration (CustomIntentMigratedAppIntent)](#sirikit-migration-customintentmigratedappintent)
|
|
@@ -184,6 +188,57 @@ func perform() async throws -> some IntentResult {
|
|
|
184
188
|
}
|
|
185
189
|
```
|
|
186
190
|
|
|
191
|
+
## EntityQuery Variants (Full Examples)
|
|
192
|
+
|
|
193
|
+
Full implementations of the query variants summarized in the main skill. Variant
|
|
194
|
+
1 (base `EntityQuery`) is shown in the skill; variants 2-4 are below.
|
|
195
|
+
|
|
196
|
+
### EntityStringQuery (free-text search)
|
|
197
|
+
|
|
198
|
+
```swift
|
|
199
|
+
struct SoupStringQuery: EntityStringQuery {
|
|
200
|
+
func entities(matching string: String) async throws -> [SoupEntity] {
|
|
201
|
+
SoupStore.shared.search(string).map { SoupEntity(from: $0) }
|
|
202
|
+
}
|
|
203
|
+
func entities(for identifiers: [String]) async throws -> [SoupEntity] {
|
|
204
|
+
SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### EnumerableEntityQuery (finite set)
|
|
210
|
+
|
|
211
|
+
```swift
|
|
212
|
+
struct AllSoupsQuery: EnumerableEntityQuery {
|
|
213
|
+
func allEntities() async throws -> [SoupEntity] {
|
|
214
|
+
SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
|
|
215
|
+
}
|
|
216
|
+
func entities(for identifiers: [String]) async throws -> [SoupEntity] {
|
|
217
|
+
SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### UniqueAppEntityQuery (singleton, iOS 18+)
|
|
223
|
+
|
|
224
|
+
Use for single-instance entities like app settings.
|
|
225
|
+
|
|
226
|
+
```swift
|
|
227
|
+
struct AppSettingsEntity: UniqueAppEntity {
|
|
228
|
+
static let defaultQuery = AppSettingsQuery()
|
|
229
|
+
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
|
|
230
|
+
var displayRepresentation: DisplayRepresentation { "App Settings" }
|
|
231
|
+
|
|
232
|
+
var id: String { "app-settings" }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
struct AppSettingsQuery: UniqueAppEntityQuery {
|
|
236
|
+
func uniqueEntity() async throws -> AppSettingsEntity {
|
|
237
|
+
AppSettingsEntity()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
187
242
|
## EntityPropertyQuery (Filter and Sort)
|
|
188
243
|
|
|
189
244
|
The most powerful query variant. Declare filterable properties and sortable
|
|
@@ -283,6 +338,114 @@ raw `EntityQueryComparator` objects.
|
|
|
283
338
|
| `LessThanOrEqualToComparator` | Comparable properties |
|
|
284
339
|
| `IsBetweenComparator` | Comparable properties supported by Shortcuts |
|
|
285
340
|
|
|
341
|
+
## Control Center Widget Implementation
|
|
342
|
+
|
|
343
|
+
Full `ControlConfigurationIntent` + `ControlWidget` wiring. The configuration
|
|
344
|
+
intent is a parameter contract; state changes run from a separate action intent
|
|
345
|
+
with an appropriate `authenticationPolicy` and `requestConfirmation`.
|
|
346
|
+
|
|
347
|
+
```swift
|
|
348
|
+
struct LightControlConfig: ControlConfigurationIntent {
|
|
349
|
+
static var title: LocalizedStringResource = "Light Control"
|
|
350
|
+
@Parameter(title: "Light", default: .livingRoom) var light: LightEntity
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
struct ToggleLightIntent: AppIntent {
|
|
354
|
+
static var title: LocalizedStringResource = "Toggle Light"
|
|
355
|
+
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
|
|
356
|
+
|
|
357
|
+
@Parameter(title: "Light") var light: LightEntity
|
|
358
|
+
|
|
359
|
+
func perform() async throws -> some IntentResult {
|
|
360
|
+
try await requestConfirmation(
|
|
361
|
+
actionName: .toggle,
|
|
362
|
+
dialog: "Toggle \(light.name)?"
|
|
363
|
+
)
|
|
364
|
+
try await LightService.shared.toggle(light.id)
|
|
365
|
+
return .result()
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
struct LightControl: ControlWidget {
|
|
370
|
+
var body: some ControlWidgetConfiguration {
|
|
371
|
+
AppIntentControlConfiguration(kind: "LightControl", intent: LightControlConfig.self) { config in
|
|
372
|
+
ControlWidgetToggle(config.light.name, isOn: config.light.isOn, action: ToggleLightIntent(light: config.light))
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Parameters without defaults must be optional on a `ControlConfigurationIntent`.
|
|
379
|
+
|
|
380
|
+
## SnippetIntent Implementation (iOS 26+)
|
|
381
|
+
|
|
382
|
+
Display interactive snippets in system UI. The system may call `perform()`
|
|
383
|
+
multiple times, including after snippet button or toggle actions, so keep
|
|
384
|
+
`SnippetIntent.perform()` side-effect-free and do mutations in the calling
|
|
385
|
+
action intent or a separate button/toggle action.
|
|
386
|
+
|
|
387
|
+
```swift
|
|
388
|
+
struct OrderStatusSnippet: SnippetIntent {
|
|
389
|
+
static var title: LocalizedStringResource = "Order Status"
|
|
390
|
+
func perform() async throws -> some IntentResult & ShowsSnippetView {
|
|
391
|
+
let status = await OrderTracker.currentStatus()
|
|
392
|
+
return .result(view: OrderStatusSnippetView(status: status))
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
struct CheckOrderStatusIntent: AppIntent {
|
|
397
|
+
static var title: LocalizedStringResource = "Check Order Status"
|
|
398
|
+
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
|
|
399
|
+
.result(snippetIntent: OrderStatusSnippet())
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
A snippet-only intent is not discoverable in Shortcuts or Spotlight unless
|
|
405
|
+
`isDiscoverable` is `true`.
|
|
406
|
+
|
|
407
|
+
## Visual Intelligence IntentValueQuery (iOS 26+)
|
|
408
|
+
|
|
409
|
+
Only one `IntentValueQuery` can take `SemanticContentDescriptor`; use
|
|
410
|
+
`@UnionValue` when one query must return multiple app entity types.
|
|
411
|
+
|
|
412
|
+
```swift
|
|
413
|
+
@available(iOS 26, *)
|
|
414
|
+
@UnionValue
|
|
415
|
+
enum ShoppingVisualResult {
|
|
416
|
+
case product(ProductEntity)
|
|
417
|
+
case store(StoreEntity)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@available(iOS 26, *)
|
|
421
|
+
struct ShoppingVisualQuery: IntentValueQuery {
|
|
422
|
+
func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
|
|
423
|
+
try Task.checkCancellation()
|
|
424
|
+
async let productMatches = ProductStore.shared.matches(
|
|
425
|
+
labels: input.labels,
|
|
426
|
+
pixelBuffer: input.pixelBuffer,
|
|
427
|
+
limit: 5
|
|
428
|
+
)
|
|
429
|
+
async let storeMatches = StoreStore.shared.matches(
|
|
430
|
+
labels: input.labels,
|
|
431
|
+
pixelBuffer: input.pixelBuffer,
|
|
432
|
+
limit: 3
|
|
433
|
+
)
|
|
434
|
+
let ranked = await rank(productMatches, storeMatches)
|
|
435
|
+
return Array(ranked.prefix(8))
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Treat `labels` as high-level English descriptors, not exhaustive synonyms or app
|
|
441
|
+
taxonomy; combine them with `pixelBuffer` when available. Return small, ranked,
|
|
442
|
+
cancellation-friendly results, and provide an `OpenIntent`, URL representation,
|
|
443
|
+
or in-app search handoff for details and more results. Do not implement camera
|
|
444
|
+
capture, Vision `VN*` requests, barcode classification, or Spotlight indexing
|
|
445
|
+
inside the App Intents query; call an existing bounded app search or image-match
|
|
446
|
+
service instead, with explicit result caps and timeouts when work may exceed a
|
|
447
|
+
system UI budget.
|
|
448
|
+
|
|
286
449
|
## Assistant Schemas (iOS 18+)
|
|
287
450
|
|
|
288
451
|
Assistant schemas define domain-specific intents that Apple Intelligence
|
|
@@ -750,6 +913,21 @@ If your app already creates `CSSearchableItem` values, call
|
|
|
750
913
|
`OpenIntent` for the entity type so Spotlight results can open the right app
|
|
751
914
|
content.
|
|
752
915
|
|
|
916
|
+
### Updating and Deleting Indexed Entities
|
|
917
|
+
|
|
918
|
+
Update and delete changed records in the same named index. For large syncs, use
|
|
919
|
+
`beginBatch()`, `endBatch(withClientState:)`, and `fetchLastClientState()` so
|
|
920
|
+
indexing can resume after a crash or jetsam.
|
|
921
|
+
|
|
922
|
+
```swift
|
|
923
|
+
let recipeIndex = CSSearchableIndex(name: "Recipes")
|
|
924
|
+
try await recipeIndex.indexAppEntities(changedRecipes)
|
|
925
|
+
try await recipeIndex.deleteAppEntities(
|
|
926
|
+
identifiedBy: deletedRecipeIDs,
|
|
927
|
+
ofType: RecipeEntity.self
|
|
928
|
+
)
|
|
929
|
+
```
|
|
930
|
+
|
|
753
931
|
### Hide specific entities from Spotlight UI
|
|
754
932
|
|
|
755
933
|
```swift
|
|
@@ -20,6 +20,9 @@ Material 3 (androidx.compose.material3).
|
|
|
20
20
|
- [Typography Tokens](#typography-tokens)
|
|
21
21
|
- [Shape Tokens](#shape-tokens)
|
|
22
22
|
- [Color Token Mapping](#color-token-mapping)
|
|
23
|
+
- [Two-Layer Design Tokens](#two-layer-design-tokens)
|
|
24
|
+
- [Bridging Tokens to Material 3](#bridging-tokens-to-material-3)
|
|
25
|
+
- [Component Catalog](#component-catalog)
|
|
23
26
|
- [Modifier Ordering](#modifier-ordering)
|
|
24
27
|
- [Do's and Don'ts](#dos-and-donts)
|
|
25
28
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -362,6 +365,46 @@ Card(shape = MaterialTheme.shapes.medium) { /* content */ }
|
|
|
362
365
|
| `outline` | `colorScheme.outline` | Borders, dividers |
|
|
363
366
|
| `outlineVariant` | `colorScheme.outlineVariant` | Subtle borders |
|
|
364
367
|
|
|
368
|
+
## Two-Layer Design Tokens
|
|
369
|
+
|
|
370
|
+
Scale token management with two layers. Layer 1 is a machine-generated raw-token
|
|
371
|
+
source: one file, never hand-edited, the single source of truth for every
|
|
372
|
+
primitive color, size, and font value. Layer 2 is a hand-authored semantic
|
|
373
|
+
wrapper: `@Immutable` color and typography data classes that name each role and
|
|
374
|
+
reference the raw tokens by name, never a literal hex or `sp`. Each semantic
|
|
375
|
+
model exposes a `light()` and a `dark()` variant.
|
|
376
|
+
|
|
377
|
+
The codegen pipeline that produces Layer 1 is a separate concern; see the
|
|
378
|
+
`android-design-tokens-codegen` skill. This skill covers the consumption side:
|
|
379
|
+
the semantic wrappers and how components read them.
|
|
380
|
+
|
|
381
|
+
Full `RawTokens`, `@Immutable AppColors`/`AppTypography` with `light()`/`dark()`:
|
|
382
|
+
[references/patterns.md#two-layer-design-tokens](references/patterns.md#two-layer-design-tokens).
|
|
383
|
+
|
|
384
|
+
## Bridging Tokens to Material 3
|
|
385
|
+
|
|
386
|
+
Bridge the semantic tokens to both worlds. Select an M3 `light`/`darkColorScheme`
|
|
387
|
+
built from the tokens so stock Material components inherit the brand colors, and
|
|
388
|
+
expose the richer token set through `CompositionLocalProvider`
|
|
389
|
+
(`staticCompositionLocalOf`). Read the rich set through a `Theme.colors` /
|
|
390
|
+
`Theme.typography` accessor marked `@ReadOnlyComposable`, so custom components
|
|
391
|
+
reach tokens Material's `colorScheme` cannot express while stock components still
|
|
392
|
+
theme correctly.
|
|
393
|
+
|
|
394
|
+
Full `LocalAppColors`, `toMaterialColorScheme`, `AppTheme`, `Theme` accessor:
|
|
395
|
+
[references/patterns.md#bridging-tokens-to-material-3](references/patterns.md#bridging-tokens-to-material-3).
|
|
396
|
+
|
|
397
|
+
## Component Catalog
|
|
398
|
+
|
|
399
|
+
Add a component-catalog module (for example Showkase) that renders every
|
|
400
|
+
annotated composable and token in a browsable in-app gallery. Keep the annotation
|
|
401
|
+
cheap and universal so it can sit on previews across every module, but gate the
|
|
402
|
+
KSP processor behind a build flag so the browser codegen costs nothing on normal
|
|
403
|
+
builds; build the catalog on demand with a `-P` flag.
|
|
404
|
+
|
|
405
|
+
Full annotated preview, flag-gated `ksp` wiring, catalog entry point:
|
|
406
|
+
[references/patterns.md#component-catalog](references/patterns.md#component-catalog).
|
|
407
|
+
|
|
365
408
|
## Modifier Ordering
|
|
366
409
|
|
|
367
410
|
Modifier order matters. Modifiers are applied outside-in, so the order
|
|
@@ -439,3 +482,8 @@ Box(
|
|
|
439
482
|
- [ ] Collapsible TopAppBar has `nestedScroll` modifier
|
|
440
483
|
- [ ] Dynamic color has API 31 guard with static fallback
|
|
441
484
|
- [ ] No mixing of M2 and M3 imports in the same screen
|
|
485
|
+
- [ ] Raw tokens come from the generated single source; semantic wrappers reference them by name, no literal hex/`sp` in the wrapper
|
|
486
|
+
- [ ] Semantic color/typography models are `@Immutable` with `light()` and `dark()` variants
|
|
487
|
+
- [ ] Rich tokens exposed via `CompositionLocalProvider` and read through a `@ReadOnlyComposable` `Theme.colors`/`Theme.typography` accessor
|
|
488
|
+
- [ ] M3 `light`/`darkColorScheme` derived from the tokens so stock Material components inherit brand colors
|
|
489
|
+
- [ ] Catalog KSP processor gated behind a build flag, annotation kept universal
|