Skip to content
ExponaBLOG
SWIFT, SWIFTUI & IOS APPS · DAY 064 / 100

App Intents and Shortcuts: a practical Swift & iOS guide

Expose app actions to Siri and Shortcuts.

Expona Admin7 min read

Why Expose Your App's Functionality?

Most iOS apps live in isolation. Users open them, interact within their own interfaces, and close them. But what if your app's most valuable actions could be triggered from Siri, accessed through system search, or chained together in automation? App Intents and Shortcuts: a practical Swift & iOS guide shows you how to break down those walls and make your app a proper citizen of the broader iOS ecosystem. We'll walk through defining intent types, connecting them to Siri voice commands, and surfacing them in widgets and Spotlight so users discover and use them naturally.

Understanding AppIntent Types

An AppIntent is the foundation of this entire system. It represents a single, discrete action your app can perform. Unlike the older INIntent framework, AppIntent (introduced in iOS 16) is built for SwiftUI and requires far less boilerplate. Think of it as "what your app can do, exposed to the system."

Let's build a concrete example. Suppose we're working on a fitness tracking app that logs workouts. We want users to log a quick run from Siri without opening the app. Here's the intent definition:

swift
import AppIntents

struct LogWorkoutIntent: AppIntent {
    static var title: LocalizedStringResource = "Log Workout"
    static var description: IntentDescription = "Record a new workout session"
    static var openAppWhenRun: Bool = false
    
    @Parameter(title: "Workout Type")
    var workoutType: WorkoutType
    
    @Parameter(title: "Duration", default: 30)
    var duration: Int
    
    func perform() async throws -> some IntentResult {
        let workout = Workout(
            type: workoutType,
            duration: duration,
            timestamp: Date()
        )
        try await WorkoutStore.shared.save(workout)
        
        return .result(value: workout) {
            "Logged \(workoutType.rawValue) for \(duration) minutes"
        }
    }
}

enum WorkoutType: String, AppEnum {
    case running = "Running"
    case cycling = "Cycling"
    case swimming = "Swimming"
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation {
        TypeDisplayRepresentation(name: "Workout Type")
    }
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: LocalizedStringResource(stringLiteral: self.rawValue))
    }
}

Notice the structure: title and description are user-facing. openAppWhenRun controls whether Siri should launch your app after running the intent—often you don't need to. The perform() method is where the work happens, running asynchronously. We return an IntentResult with a confirmation message so the user knows what occurred.

Parameters are marked with @Parameter, and enums conform to AppEnum so the system can display them cleanly in shortcuts and voice commands. This is cleaner than juggling optional parameters and type conversions manually.

Adding Siri Voice Shortcuts

Once you've defined your intent, you can create a shortcut with a custom voice phrase. Users will say "Hey Siri, [your phrase]" and the intent fires. This lives in your app's target settings under the "Shortcuts" section of Build Settings, but more commonly you'll register it programmatically via an AppShortcutsProvider.

swift
struct FitnessShortcuts: AppShortcutsProvider {
    static var allShortcuts: [AppShortcut] {
        AppShortcut(
            intent: LogWorkoutIntent(),
            phrases: [
                "Log a \(.intent.workoutType) session",
                "Record \(.intent.workoutType) for \(.intent.duration) minutes"
            ],
            shortcutName: "Quick Workout Log"
        )
    }
}

The phrases array supports templating via parameter interpolation (the dot notation). Siri learns these patterns and responds to variations. If you specify "Log a \(.intent.workoutType) session," users can say "Log a Running session" and the system fills in the parameter automatically.

For users who prefer explicit shortcuts over voice, the Shortcuts app will discover these intents automatically. They can build complex automations: "When I arrive at the gym, log a 60-minute workout" or chain multiple actions together.

Surfacing Intents in Widgets and Spotlight

Beyond Siri and the Shortcuts app, intents shine in two more places: Lock Screen widgets and Spotlight search.

Lock Screen Widget Actions

Modern lock screen widgets can include interactive buttons backed by app intents. This gives users single-tap access to frequent actions without unlocking.

swift
struct QuickLogWidgetView: View {
    var body: some View {
        VStack(spacing: 8) {
            Text("Quick Log")
                .font(.headline)
            
            HStack(spacing: 8) {
                Button(intent: LogWorkoutIntent(workoutType: .running, duration: 30)) {
                    Label("Run", systemImage: "figure.run")
                }
                .buttonStyle(.bordered)
                
                Button(intent: LogWorkoutIntent(workoutType: .cycling, duration: 30)) {
                    Label("Cycle", systemImage: "figure.outdoor.cycle")
                }
                .buttonStyle(.bordered)
            }
        }
        .padding()
    }
}

#Preview {
    QuickLogWidgetView()
}

When a user taps "Run," the LogWorkoutIntent executes in the background. Because we set openAppWhenRun: false, the widget remains visible and responsive. Set it to true if you need the app to open and show confirmation details.

Spotlight Integration

You can index app intents so they appear in Spotlight search. Users search for "Log a run" and Siri suggests your intent as a result.

swift
import AppIntents

struct LogWorkoutIntent: AppIntent {
    // ... existing code ...
    
    static var searchScopes: [SearchScope]?
    
    var searchAttributes: SearchAttributes {
        SearchAttributes(
            displayRepresentation: DisplayRepresentation(
                title: "Log Workout"
            ),
            keywords: ["workout", "exercise", "fitness", "run", "cycle"]
        )
    }
}

When users open Spotlight and search for "run" or "workout," your intent appears as a suggestion. Tapping it executes the intent immediately. This is especially powerful for high-frequency actions like "Log workout," "Send message," or "Start timer."

Using Parameters and Entities

Parameters aren't just simple types. You can define complex parameter types using AppEnum and even query dynamic data via EntityQuery. This is useful when parameter values come from your app's local state or a server.

Dynamic Entity Queries

Imagine a project management app where users want to log time against a project. Projects are stored in the app; you don't want to hard-code them. Use EntityQuery:

swift
struct Project: AppEntity {
    static var defaultQuery = ProjectQuery()
    
    let id: UUID
    let name: String
    
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }
    
    static var typeDisplayRepresentation: TypeDisplayRepresentation {
        TypeDisplayRepresentation(name: "Project")
    }
}

struct ProjectQuery: EntityQuery {
    func entities(matching string: String) async throws -> [Project] {
        let allProjects = try await ProjectStore.shared.fetchAll()
        return allProjects.filter { $0.name.localizedCaseInsensitiveContains(string) }
    }
    
    func entities(for identifiers: [UUID]) async throws -> [Project] {
        let allProjects = try await ProjectStore.shared.fetchAll()
        return allProjects.filter { identifiers.contains($0.id) }
    }
}

struct LogTimeIntent: AppIntent {
    static var title: LocalizedStringResource = "Log Time"
    
    @Parameter(title: "Project")
    var project: Project
    
    @Parameter(title: "Hours")
    var hours: Double
    
    func perform() async throws -> some IntentResult {
        let entry = TimeEntry(project: project, hours: hours, date: Date())
        try await TimeEntryStore.shared.save(entry)
        return .result(value: entry) {
            "Logged \(hours) hours on \(project.name)"
        }
    }
}

Now when users interact with "Log Time" in Siri, Shortcuts, or a widget button, they're presented with a live list of their projects fetched from the app's data store. It's dynamic, responsive, and feels native.

Common Mistakes

  • Blocking the perform() method: Never call synchronous network requests or heavy computations in perform() without wrapping in async/await. The system times out after a few seconds. If your intent needs to fetch remote data, do it asynchronously and handle timeout gracefully.
  • Forgetting to conform enums to AppEnum: Parameter types must conform to AppEnum or be simple types like String, Int, Double, or Bool. Missing this conformance causes cryptic build errors or parameter lists don't appear in Siri suggestions.
  • Overly complex phrases: Keep Siri phrases concise. "Log a running session for 30 minutes" is clearer than "I would like to record a running exercise session with a duration of thirty minutes if you would kindly do so." Users won't speak it, and Siri won't match it reliably.
  • Setting openAppWhenRun: true unnecessarily: If your intent can complete in the background (logging data, sending a message), keep it false. Launching the app interrupts the user's workflow and wastes battery.
  • Ignoring error handling: Your perform() method can throw. Always handle failures gracefully and provide meaningful error messages that Siri can read aloud.

Key Takeaways

  • AppIntent is the modern, SwiftUI-friendly way to expose app actions. Define it with a title, parameters, and a perform() method that returns an IntentResult.
  • Register Siri voice phrases via AppShortcutsProvider, using template syntax to let parameters be filled dynamically from user input.
  • Intents automatically appear in the Shortcuts app for complex automations and in Lock Screen widgets as interactive buttons.
  • Use AppEnum for parameter types and EntityQuery to fetch dynamic values (projects, contacts, etc.) from your app's data store at runtime.
  • Keep the system responsive: run async, avoid blocking the main thread, and only launch the app if you absolutely need to show rich confirmation UI.
  • Test your intents with Siri, the Shortcuts app, and on the Lock Screen to ensure the experience is smooth across all entry points.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles