Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-app-shortcuts-ref/.openskills.json
Normal file
7
.claude/skills/axiom-app-shortcuts-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-app-shortcuts-ref",
|
||||
"installedAt": "2026-04-12T08:05:45.553Z"
|
||||
}
|
||||
828
.claude/skills/axiom-app-shortcuts-ref/SKILL.md
Normal file
828
.claude/skills/axiom-app-shortcuts-ref/SKILL.md
Normal file
@@ -0,0 +1,828 @@
|
||||
---
|
||||
name: axiom-app-shortcuts-ref
|
||||
description: Use when implementing App Shortcuts for instant Siri/Spotlight availability, configuring AppShortcutsProvider, adding suggested phrases, or debugging shortcuts not appearing - covers complete App Shortcuts API for iOS 16+
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# App Shortcuts Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive guide to App Shortcuts framework for making your app's actions instantly available in Siri, Spotlight, Action Button, Control Center, and other system experiences. App Shortcuts are pre-configured App Intents that work immediately after app install—no user setup required.
|
||||
|
||||
**Key distinction** App Intents are the actions; App Shortcuts are the pre-configured "surface" that makes those actions instantly discoverable system-wide.
|
||||
|
||||
---
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing AppShortcutsProvider for your app
|
||||
- Adding suggested phrases for Siri invocation
|
||||
- Configuring instant Spotlight availability
|
||||
- Creating parameterized shortcuts (skip Siri clarification)
|
||||
- Using NegativeAppShortcutPhrase to prevent false positives (iOS 17+)
|
||||
- Promoting shortcuts with SiriTipView
|
||||
- Updating shortcuts dynamically with updateAppShortcutParameters()
|
||||
- Debugging shortcuts not appearing in Shortcuts app or Spotlight
|
||||
- Choosing between App Intents and App Shortcuts
|
||||
|
||||
Do NOT use this skill for:
|
||||
- General App Intents implementation (use app-intents-ref)
|
||||
- Core Spotlight indexing (use core-spotlight-ref)
|
||||
- Overall discoverability strategy (use app-discoverability)
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **app-intents-ref** — Complete App Intents implementation reference
|
||||
- **app-discoverability** — Strategic guide for making apps discoverable
|
||||
- **core-spotlight-ref** — Core Spotlight and NSUserActivity integration
|
||||
|
||||
---
|
||||
|
||||
## App Shortcuts vs App Intents
|
||||
|
||||
| Aspect | App Intent | App Shortcut |
|
||||
|--------|-----------|--------------|
|
||||
| **Discovery** | Must be found in Shortcuts app | Instantly available after install |
|
||||
| **Configuration** | User configures in Shortcuts | Pre-configured by developer |
|
||||
| **Siri activation** | Requires custom phrase setup | Works immediately with provided phrases |
|
||||
| **Spotlight** | Requires donation or IndexedEntity | Appears automatically |
|
||||
| **Action button** | Not directly accessible | Can be assigned immediately |
|
||||
| **Setup time** | Minutes per user | Zero |
|
||||
|
||||
**When to use App Shortcuts** Every app should provide App Shortcuts for core functionality. They dramatically improve discoverability with zero user effort.
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### AppShortcutsProvider Protocol
|
||||
|
||||
**Required conformance** Your app must have exactly one type conforming to `AppShortcutsProvider`.
|
||||
|
||||
```swift
|
||||
struct MyAppShortcuts: AppShortcutsProvider {
|
||||
// Required: Define your shortcuts
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] { get }
|
||||
|
||||
// Optional: Branding color
|
||||
static var shortcutTileColor: ShortcutTileColor { get }
|
||||
|
||||
// Optional: Dynamic updates
|
||||
static func updateAppShortcutParameters()
|
||||
|
||||
// Optional: Negative phrases (iOS 17+)
|
||||
static var negativePhrases: [NegativeAppShortcutPhrase] { get }
|
||||
}
|
||||
```
|
||||
|
||||
**Platform support** iOS 16+, iPadOS 16+, macOS 13+, tvOS 16+, watchOS 9+
|
||||
|
||||
---
|
||||
|
||||
### AppShortcut Structure
|
||||
|
||||
Associates an `AppIntent` with spoken phrases and metadata.
|
||||
|
||||
```swift
|
||||
AppShortcut(
|
||||
intent: StartMeditationIntent(),
|
||||
phrases: [
|
||||
"Start meditation in \(.applicationName)",
|
||||
"Begin mindfulness with \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Meditate",
|
||||
systemImageName: "figure.mind.and.body"
|
||||
)
|
||||
```
|
||||
|
||||
**Components:**
|
||||
- `intent` — The App Intent to execute
|
||||
- `phrases` — Spoken/typed phrases for Siri/Spotlight
|
||||
- `shortTitle` — Short label for Shortcuts app tiles
|
||||
- `systemImageName` — SF Symbol for visual representation
|
||||
|
||||
---
|
||||
|
||||
### AppShortcutPhrase (Suggested Phrases)
|
||||
|
||||
**String interpolation** Phrases use `\(.applicationName)` to dynamically include your app's name.
|
||||
|
||||
```swift
|
||||
phrases: [
|
||||
"Start meditation in \(.applicationName)",
|
||||
"Meditate with \(.applicationName)"
|
||||
]
|
||||
```
|
||||
|
||||
**User sees in Siri/Spotlight:**
|
||||
- "Start meditation in Calm"
|
||||
- "Meditate with Calm"
|
||||
|
||||
**Why this matters** The system uses these exact phrases to trigger your intent via Siri and show suggestions in Spotlight.
|
||||
|
||||
---
|
||||
|
||||
### @AppShortcutsBuilder
|
||||
|
||||
Result builder for defining shortcuts array.
|
||||
|
||||
```swift
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(intent: OrderIntent(), /* ... */)
|
||||
AppShortcut(intent: ReorderIntent(), /* ... */)
|
||||
|
||||
if UserDefaults.standard.bool(forKey: "premiumUser") {
|
||||
AppShortcut(intent: CustomizeIntent(), /* ... */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result builder features:**
|
||||
- Conditional shortcuts (if/else)
|
||||
- Loop-generated shortcuts (for-in)
|
||||
- Inline array construction
|
||||
|
||||
---
|
||||
|
||||
## Phrase Template Patterns
|
||||
|
||||
### Basic Phrases (No Parameters)
|
||||
|
||||
```swift
|
||||
AppShortcut(
|
||||
intent: StartWorkoutIntent(),
|
||||
phrases: [
|
||||
"Start workout in \(.applicationName)",
|
||||
"Begin exercise with \(.applicationName)",
|
||||
"Work out in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Start Workout",
|
||||
systemImageName: "figure.run"
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Simple, discoverable
|
||||
- Works for all users
|
||||
- No parameter ambiguity
|
||||
|
||||
**Use when** Intent has no required parameters or parameters have defaults.
|
||||
|
||||
---
|
||||
|
||||
### Parameterized Phrases (Skip Clarification)
|
||||
|
||||
Pre-configure intents with specific parameter values to skip Siri's clarification step.
|
||||
|
||||
```swift
|
||||
// Intent with parameters
|
||||
struct StartMeditationIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Start Meditation"
|
||||
|
||||
@Parameter(title: "Type")
|
||||
var meditationType: MeditationType?
|
||||
|
||||
@Parameter(title: "Duration")
|
||||
var duration: Int?
|
||||
}
|
||||
|
||||
// Shortcuts with different parameter combinations
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
// Generic version (will ask for parameters)
|
||||
AppShortcut(
|
||||
intent: StartMeditationIntent(),
|
||||
phrases: ["Start meditation in \(.applicationName)"],
|
||||
shortTitle: "Meditate",
|
||||
systemImageName: "figure.mind.and.body"
|
||||
)
|
||||
|
||||
// Specific versions (skip parameter step)
|
||||
AppShortcut(
|
||||
intent: StartMeditationIntent(
|
||||
meditationType: .mindfulness,
|
||||
duration: 10
|
||||
),
|
||||
phrases: [
|
||||
"Start quick mindfulness in \(.applicationName)",
|
||||
"10 minute mindfulness in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Quick Mindfulness",
|
||||
systemImageName: "brain.head.profile"
|
||||
)
|
||||
|
||||
AppShortcut(
|
||||
intent: StartMeditationIntent(
|
||||
meditationType: .sleep,
|
||||
duration: 20
|
||||
),
|
||||
phrases: [
|
||||
"Start sleep meditation in \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Sleep Meditation",
|
||||
systemImageName: "moon.stars.fill"
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- One-phrase completion (no follow-up questions)
|
||||
- Better user experience for common use cases
|
||||
- Spotlight shows specific shortcuts
|
||||
|
||||
**Trade-off** More shortcuts = more visual clutter in Shortcuts app. Balance common cases (3-5 shortcuts) vs flexibility (generic shortcut with parameters).
|
||||
|
||||
---
|
||||
|
||||
## NegativeAppShortcutPhrase (iOS 17+)
|
||||
|
||||
Train the system to NOT invoke your app for certain phrases.
|
||||
|
||||
```swift
|
||||
struct MeditationAppShortcuts: AppShortcutsProvider {
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(
|
||||
intent: StartMeditationIntent(),
|
||||
phrases: ["Start meditation in \(.applicationName)"],
|
||||
shortTitle: "Meditate",
|
||||
systemImageName: "figure.mind.and.body"
|
||||
)
|
||||
}
|
||||
|
||||
// Prevent false positives
|
||||
static var negativePhrases: [NegativeAppShortcutPhrase] {
|
||||
NegativeAppShortcutPhrases {
|
||||
"Stop meditation"
|
||||
"Cancel meditation"
|
||||
"End session"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Phrases that sound similar to your shortcuts but mean the opposite
|
||||
- Common phrases users might say that shouldn't trigger your app
|
||||
- Disambiguation when multiple apps have similar capabilities
|
||||
|
||||
**Platform** iOS 17.0+, iPadOS 17.0+, macOS 14.0+, tvOS 17.0+, watchOS 10.0+
|
||||
|
||||
---
|
||||
|
||||
## Discovery UI Components
|
||||
|
||||
### SiriTipView — Promote Shortcuts In-App
|
||||
|
||||
Display the spoken phrase for a shortcut directly in your app's UI.
|
||||
|
||||
```swift
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
struct OrderConfirmationView: View {
|
||||
@State private var showSiriTip = true
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Order confirmed!")
|
||||
|
||||
// Show Siri tip after successful order
|
||||
SiriTipView(intent: ReorderIntent(), isVisible: $showSiriTip)
|
||||
.siriTipViewStyle(.dark)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- Intent must be used in an AppShortcut (otherwise shows empty view)
|
||||
- isVisible binding controls display state
|
||||
|
||||
**Styles:**
|
||||
- `.automatic` — Adapts to environment
|
||||
- `.light` — Light background
|
||||
- `.dark` — Dark background
|
||||
|
||||
**Best practice** Show after users complete actions, suggesting easier ways next time.
|
||||
|
||||
---
|
||||
|
||||
### ShortcutsLink — Link to Shortcuts App
|
||||
|
||||
Opens your app's page in the Shortcuts app, listing all available shortcuts.
|
||||
|
||||
```swift
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Siri & Shortcuts") {
|
||||
ShortcutsLink()
|
||||
// Displays "Shortcuts" with standard link styling
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Settings screen
|
||||
- Help/Support section
|
||||
- Onboarding flow
|
||||
|
||||
**Benefits** Single tap takes users to see all your app's shortcuts, with suggested phrases visible.
|
||||
|
||||
---
|
||||
|
||||
### ShortcutTileColor — Branding
|
||||
|
||||
Set the color for your shortcuts in the Shortcuts app.
|
||||
|
||||
```swift
|
||||
struct CoffeeAppShortcuts: AppShortcutsProvider {
|
||||
static var shortcutTileColor: ShortcutTileColor = .tangerine
|
||||
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available colors:**
|
||||
| Color | Use Case |
|
||||
|-------|----------|
|
||||
| `.blue` | Default, professional |
|
||||
| `.tangerine` | Energy, food/beverage |
|
||||
| `.purple` | Creative, meditation |
|
||||
| `.teal` | Health, wellness |
|
||||
| `.red` | Urgent, important |
|
||||
| `.pink` | Lifestyle, social |
|
||||
| `.navy` | Business, finance |
|
||||
| `.yellow` | Productivity, notes |
|
||||
| `.lime` | Fitness, outdoor |
|
||||
|
||||
Full list: `.blue`, `.grape`, `.grayBlue`, `.grayBrown`, `.grayGreen`, `.lightBlue`, `.lime`, `.navy`, `.orange`, `.pink`, `.purple`, `.red`, `.tangerine`, `.teal`, `.yellow`
|
||||
|
||||
**Choose color** that matches your app icon or brand identity.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Updates
|
||||
|
||||
### updateAppShortcutParameters()
|
||||
|
||||
Call when parameter options change to refresh stored shortcuts.
|
||||
|
||||
```swift
|
||||
struct MeditationAppShortcuts: AppShortcutsProvider {
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
// Shortcuts can reference dynamic data
|
||||
for session in MeditationData.favoriteSessions {
|
||||
AppShortcut(
|
||||
intent: StartSessionIntent(session: session),
|
||||
phrases: ["Start \(session.name) in \(.applicationName)"],
|
||||
shortTitle: session.name,
|
||||
systemImageName: session.iconName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func updateAppShortcutParameters() {
|
||||
// Called automatically when needed
|
||||
// Override only if you need custom behavior
|
||||
}
|
||||
}
|
||||
|
||||
// In your app, when data changes
|
||||
extension MeditationData {
|
||||
func markAsFavorite(_ session: Session) {
|
||||
favoriteSessions.append(session)
|
||||
|
||||
// Update App Shortcuts to reflect new data
|
||||
MeditationAppShortcuts.updateAppShortcutParameters()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to call:**
|
||||
- User adds/removes favorites
|
||||
- Available options change
|
||||
- App data structure updates
|
||||
|
||||
**Automatic invocation** The system calls this periodically, but you can force updates when you know data changed.
|
||||
|
||||
---
|
||||
|
||||
## Complete Implementation Example
|
||||
|
||||
### Step 1: Define App Intents
|
||||
|
||||
```swift
|
||||
import AppIntents
|
||||
|
||||
struct OrderCoffeeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Order Coffee"
|
||||
static var description = IntentDescription("Orders coffee for pickup")
|
||||
|
||||
@Parameter(title: "Coffee Type")
|
||||
var coffeeType: CoffeeType
|
||||
|
||||
@Parameter(title: "Size")
|
||||
var size: CoffeeSize
|
||||
|
||||
@Parameter(title: "Customizations")
|
||||
var customizations: String?
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Order \(\.$size) \(\.$coffeeType)") {
|
||||
\.$customizations
|
||||
}
|
||||
}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
let order = try await CoffeeService.shared.order(
|
||||
type: coffeeType,
|
||||
size: size,
|
||||
customizations: customizations
|
||||
)
|
||||
|
||||
return .result(
|
||||
value: order,
|
||||
dialog: "Your \(size) \(coffeeType) is ordered for pickup"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReorderLastIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Reorder Last Coffee"
|
||||
static var description = IntentDescription("Reorders your most recent coffee")
|
||||
static var openAppWhenRun: Bool = false
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard let lastOrder = try await CoffeeService.shared.lastOrder() else {
|
||||
throw CoffeeError.noRecentOrders
|
||||
}
|
||||
|
||||
try await CoffeeService.shared.reorder(lastOrder)
|
||||
|
||||
return .result(
|
||||
dialog: "Reordering your \(lastOrder.coffeeName)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum CoffeeType: String, AppEnum {
|
||||
case latte, cappuccino, americano, espresso
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Coffee"
|
||||
static var caseDisplayRepresentations: [CoffeeType: DisplayRepresentation] = [
|
||||
.latte: "Latte",
|
||||
.cappuccino: "Cappuccino",
|
||||
.americano: "Americano",
|
||||
.espresso: "Espresso"
|
||||
]
|
||||
}
|
||||
|
||||
enum CoffeeSize: String, AppEnum {
|
||||
case small, medium, large
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Size"
|
||||
static var caseDisplayRepresentations: [CoffeeSize: DisplayRepresentation] = [
|
||||
.small: "Small",
|
||||
.medium: "Medium",
|
||||
.large: "Large"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Create AppShortcutsProvider
|
||||
|
||||
```swift
|
||||
import AppIntents
|
||||
|
||||
struct CoffeeAppShortcuts: AppShortcutsProvider {
|
||||
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
// Generic order (will ask for parameters)
|
||||
AppShortcut(
|
||||
intent: OrderCoffeeIntent(),
|
||||
phrases: [
|
||||
"Order coffee in \(.applicationName)",
|
||||
"Get coffee from \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Order",
|
||||
systemImageName: "cup.and.saucer.fill"
|
||||
)
|
||||
|
||||
// Common specific orders (skip parameter step)
|
||||
AppShortcut(
|
||||
intent: OrderCoffeeIntent(
|
||||
coffeeType: .latte,
|
||||
size: .medium
|
||||
),
|
||||
phrases: [
|
||||
"Order my usual from \(.applicationName)",
|
||||
"Get my regular coffee from \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Usual Order",
|
||||
systemImageName: "star.fill"
|
||||
)
|
||||
|
||||
// Reorder last
|
||||
AppShortcut(
|
||||
intent: ReorderLastIntent(),
|
||||
phrases: [
|
||||
"Reorder coffee from \(.applicationName)",
|
||||
"Order again from \(.applicationName)"
|
||||
],
|
||||
shortTitle: "Reorder",
|
||||
systemImageName: "arrow.clockwise"
|
||||
)
|
||||
}
|
||||
|
||||
// Branding
|
||||
static var shortcutTileColor: ShortcutTileColor = .tangerine
|
||||
|
||||
// Prevent false positives (iOS 17+)
|
||||
static var negativePhrases: [NegativeAppShortcutPhrase] {
|
||||
NegativeAppShortcutPhrases {
|
||||
"Cancel coffee order"
|
||||
"Stop coffee"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Promote in UI
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
|
||||
struct OrderConfirmationView: View {
|
||||
@State private var showReorderTip = true
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.green)
|
||||
|
||||
Text("Order Placed!")
|
||||
.font(.title)
|
||||
|
||||
Text("Your coffee will be ready in 10 minutes")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Promote reorder shortcut
|
||||
if showReorderTip {
|
||||
SiriTipView(intent: ReorderLastIntent(), isVisible: $showReorderTip)
|
||||
.siriTipViewStyle(.dark)
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
// Link to see all shortcuts
|
||||
Section {
|
||||
ShortcutsLink()
|
||||
} header: {
|
||||
Text("See all available shortcuts")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Where App Shortcuts Appear
|
||||
|
||||
Once implemented, your App Shortcuts are available in:
|
||||
|
||||
| Location | User Experience |
|
||||
|----------|-----------------|
|
||||
| **Siri** | Voice activation with provided phrases |
|
||||
| **Spotlight** | Search for action or phrase → Instant execution |
|
||||
| **Shortcuts app** | Pre-populated shortcuts, zero configuration |
|
||||
| **Action Button** (iPhone 15 Pro) | Assignable to hardware button |
|
||||
| **Apple Watch Ultra** | Action Button assignment |
|
||||
| **Control Center** | Add shortcuts as controls |
|
||||
| **Lock Screen widgets** | Quick actions without unlocking |
|
||||
| **Apple Pencil Pro** | Squeeze gesture assignment |
|
||||
| **Focus Filters** | Contextual filtering |
|
||||
|
||||
**Instant availability** All locations work immediately after app install. No user setup required.
|
||||
|
||||
---
|
||||
|
||||
## Testing & Debugging
|
||||
|
||||
### Verify Shortcuts Appear in Shortcuts App
|
||||
|
||||
1. Build and run your app on device
|
||||
2. Open Shortcuts app
|
||||
3. Tap "+" to create new shortcut
|
||||
4. Search for your app name
|
||||
5. Verify shortcuts appear with correct titles and icons
|
||||
|
||||
**If shortcuts don't appear:**
|
||||
- Ensure AppShortcutsProvider is in your main app target
|
||||
- Check that `isDiscoverable` is true for the AppIntents (default)
|
||||
- Rebuild and reinstall app
|
||||
- Check console for AppShortcuts errors
|
||||
|
||||
---
|
||||
|
||||
### Test Siri Invocation
|
||||
|
||||
1. Invoke Siri
|
||||
2. Say one of your suggested phrases
|
||||
3. Verify Siri executes the intent
|
||||
|
||||
**Example**:
|
||||
- You: "Order coffee in CoffeeApp"
|
||||
- Siri: "What size and type?"
|
||||
- You: "Medium latte"
|
||||
- Siri: "Your medium latte is ordered for pickup"
|
||||
|
||||
**If Siri doesn't recognize phrase:**
|
||||
- Check phrase includes `\(.applicationName)`
|
||||
- Verify phrase is in appShortcuts array
|
||||
- Try simpler phrases (3-6 words ideal)
|
||||
- Avoid complex grammar or rare words
|
||||
|
||||
---
|
||||
|
||||
### Test Spotlight Discovery
|
||||
|
||||
1. Swipe down to open Spotlight
|
||||
2. Type your app name or shortcut phrase
|
||||
3. Verify shortcut appears in results
|
||||
4. Tap to execute
|
||||
|
||||
**If shortcut doesn't appear in Spotlight:**
|
||||
- Wait a few minutes (indexing delay)
|
||||
- Restart device
|
||||
- Check System Settings → Siri & Search → [Your App] → Show App in Search
|
||||
|
||||
---
|
||||
|
||||
### Debug with Console Logs
|
||||
|
||||
```swift
|
||||
#if DEBUG
|
||||
struct CoffeeAppShortcuts: AppShortcutsProvider {
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
let shortcuts = [
|
||||
AppShortcut(/* ... */),
|
||||
// ...
|
||||
]
|
||||
|
||||
print("📱 Registered \(shortcuts.count) App Shortcuts")
|
||||
shortcuts.forEach { shortcut in
|
||||
print(" - \(shortcut.shortTitle)")
|
||||
}
|
||||
|
||||
return shortcuts
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
**Check Xcode console** after app launch to verify shortcuts are registered.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Phrase Design
|
||||
|
||||
#### ❌ DON'T: Long, complex phrases
|
||||
```swift
|
||||
phrases: [
|
||||
"I would like to order a coffee from \(.applicationName) please"
|
||||
]
|
||||
```
|
||||
|
||||
#### ✅ DO: Short, natural phrases
|
||||
```swift
|
||||
phrases: [
|
||||
"Order coffee in \(.applicationName)",
|
||||
"Get coffee from \(.applicationName)"
|
||||
]
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
- 3-6 words ideal
|
||||
- Start with verb (Order, Start, Get, Show)
|
||||
- Include `\(.applicationName)` for disambiguation
|
||||
- Use natural language users would actually say
|
||||
|
||||
---
|
||||
|
||||
### 2. Shortcut Quantity
|
||||
|
||||
#### ❌ DON'T: Provide 20+ shortcuts
|
||||
```swift
|
||||
// Bad: Overwhelming
|
||||
AppShortcut for every possible combination
|
||||
```
|
||||
|
||||
#### ✅ DO: Focus on 3-5 core actions
|
||||
```swift
|
||||
// Good: Focused on common tasks
|
||||
AppShortcut(intent: OrderIntent(), /* ... */)
|
||||
AppShortcut(intent: ReorderIntent(), /* ... */)
|
||||
AppShortcut(intent: ViewOrdersIntent(), /* ... */)
|
||||
```
|
||||
|
||||
**Why** Too many shortcuts creates clutter. Focus on high-value, frequently-used actions.
|
||||
|
||||
---
|
||||
|
||||
### 3. Parameter Combinations
|
||||
|
||||
#### ❌ DON'T: Parameterize every variant
|
||||
```swift
|
||||
// Bad: Creates 12 shortcuts (3 sizes × 4 types)
|
||||
for size in CoffeeSize.allCases {
|
||||
for type in CoffeeType.allCases {
|
||||
AppShortcut(intent: OrderIntent(type: type, size: size), /* ... */)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ DO: Provide generic + top 2-3 common cases
|
||||
```swift
|
||||
// Good: Generic + common specific cases
|
||||
AppShortcut(intent: OrderIntent(), /* ... */) // Generic
|
||||
AppShortcut(intent: OrderIntent(type: .latte, size: .medium), /* ... */) // Usual
|
||||
AppShortcut(intent: OrderIntent(type: .espresso, size: .small), /* ... */) // Quick
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Short Titles
|
||||
|
||||
#### ❌ DON'T: Verbose or redundant
|
||||
```swift
|
||||
shortTitle: "Order Coffee from Coffee App"
|
||||
```
|
||||
|
||||
#### ✅ DO: Concise and clear
|
||||
```swift
|
||||
shortTitle: "Order"
|
||||
```
|
||||
|
||||
**Context** App name already appears in Shortcuts app, so no need to repeat.
|
||||
|
||||
---
|
||||
|
||||
### 5. System Images
|
||||
|
||||
#### ❌ DON'T: Use custom images
|
||||
```swift
|
||||
// Not supported
|
||||
shortImage: UIImage(named: "custom")
|
||||
```
|
||||
|
||||
#### ✅ DO: Use SF Symbols
|
||||
```swift
|
||||
systemImageName: "cup.and.saucer.fill"
|
||||
```
|
||||
|
||||
**Why** SF Symbols scale properly, support dark mode, and integrate with system UI.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2022-10170, 2022-10169, 260
|
||||
|
||||
**Docs**: /appintents/appshortcutsprovider, /appintents/appshortcut, /appintents/app-shortcuts
|
||||
|
||||
**Skills**: axiom-app-intents-ref, axiom-app-discoverability, axiom-core-spotlight-ref
|
||||
|
||||
---
|
||||
|
||||
**Remember** App Shortcuts make your app's functionality instantly available across iOS. Define 3-5 core shortcuts with natural phrases, promote them in your UI with SiriTipView, and users can invoke them immediately via Siri, Spotlight, Action Button, and more.
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "App Shortcuts Reference"
|
||||
short_description: "Implementing App Shortcuts for instant Siri/Spotlight availability, configuring AppShortcutsProvider, adding suggeste..."
|
||||
Reference in New Issue
Block a user