Skip to main content

iOS Configuration

Required Setup: Static App Intents in Main App Target

⚠️ Important: iOS App Intents framework requires static intent declarations in your main app target, not just dynamic registration from the plugin.

Add this code to your iOS app's AppDelegate.swift:

import Flutter
import UIKit
import AppIntents
import flutter_app_intents

@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

// Static App Intents that bridge to Flutter handlers
@available(iOS 16.0, *)
struct MyCounterIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Counter"
static var description = IntentDescription("Increment the counter by one")
static var isDiscoverable = true
static var openAppWhenRun = true

func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "increment_counter",
parameters: [:]
)

if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Counter incremented"
return .result(value: value)
} else {
let errorMessage = result["error"] as? String ?? "Failed to increment counter"
throw AppIntentError.executionFailed(errorMessage)
}
}
}

// Error handling for App Intents
enum AppIntentError: Error {
case executionFailed(String)
}

// AppShortcutsProvider for Siri/Shortcuts discovery
@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
return [
AppShortcut(
intent: MyCounterIntent(),
phrases: [
"Increment counter with \(.applicationName)",
"Add one with \(.applicationName)",
"Count up using \(.applicationName)"
],
shortTitle: "Increment",
systemImageName: "plus.circle"
)
]
}
}

Info.plist Configuration

Add these permissions and configuration to your iOS Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>This app uses microphone for Siri integration</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This app uses speech recognition for Siri integration</string>

<!-- App Intents Configuration -->
<key>NSAppIntentsConfiguration</key>
<dict>
<key>NSAppIntentsPackage</key>
<string>your_app_bundle_id</string>
</dict>
<key>NSAppIntentsMetadata</key>
<dict>
<key>NSAppIntentsSupported</key>
<true/>
</dict>

Minimum Deployment Target

Ensure your iOS deployment target is set to 16.0 or later:

# ios/Podfile
platform :ios, '16.0'

For navigation intents that should open your app, use this pattern:

@available(iOS 16.0, *)
struct OpenProfileIntent: AppIntent {
static var title: LocalizedStringResource = "Open Profile"
static var description = IntentDescription("Open user profile page")
static var isDiscoverable = true
static var openAppWhenRun = true

@Parameter(title: "User ID")
var userId: String?

func perform() async throws -> some IntentResult & ReturnsValue<String> & OpensIntent {
let plugin = FlutterAppIntentsPlugin.shared
let result = await plugin.handleIntentInvocation(
identifier: "open_profile",
parameters: ["userId": userId ?? "current"]
)

if let success = result["success"] as? Bool, success {
let value = result["value"] as? String ?? "Profile opened"
return .result(value: value) // This opens/focuses the app
} else {
let errorMessage = result["error"] as? String ?? "Failed to open profile"
throw AppIntentError.executionFailed(errorMessage)
}
}
}

Adding App Shortcuts

Add navigation shortcuts to your AppShortcutsProvider:

@available(iOS 16.0, *)
struct AppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
return [
// Navigation shortcuts
AppShortcut(
intent: OpenProfileIntent(),
phrases: [
"Open my profile in ${applicationName}",
"Show profile using ${applicationName}",
"Go to profile with ${applicationName}"
]
),
AppShortcut(
intent: OpenChatIntent(),
phrases: [
"Chat with \\(.contactName) using ${applicationName}",
"Open chat with \\(.contactName) in ${applicationName}",
"Message \\(.contactName) with ${applicationName}"
]
)
]
}
}

App Shortcuts Phrase Best Practices

When defining phrases for your App Shortcuts, follow these best practices for optimal user experience and Siri recognition:

⚠️ Important Limitation: Static Phrases Only

App Shortcuts phrases CANNOT be defined dynamically. This is a fundamental limitation of Apple's App Intents framework:

// ❌ DOES NOT WORK - phrases must be static literals
phrases: [
"\(userDefinedPhrase) with \(.applicationName)", // Won't compile
dynamicPhraseVariable, // Won't compile
generatePhrase() // Won't compile
]

// ✅ WORKS - static phrases with dynamic parameters
phrases: [
"Send message to \(.contactName) with \(.applicationName)", // ✅ Parameter is dynamic
"Set timer for \(.duration) using \(.applicationName)", // ✅ Parameter is dynamic
"Play \(.songName) in \(.applicationName)" // ✅ Parameter is dynamic
]

Why phrases must be static:

  • Compile-time registration: iOS requires phrases for Siri's speech recognition engine at build time
  • App Store review: Apple analyzes all possible voice commands during app review
  • Performance: Siri's recognition is optimized based on the known phrase list
  • Security: Prevents apps from creating potentially malicious or conflicting commands dynamically

📊 Phrase Quantity Limits and Guidelines

While Apple doesn't publish exact hard limits, there are practical constraints on the number of phrases:

Recommended Limits:

  • Per AppShortcut: 3-5 phrases (optimal), up to 8 phrases (maximum recommended)
  • Total per app: 50-100 phrases across all shortcuts (practical limit)
  • Quality over quantity: Focus on natural, distinct variations rather than exhaustive lists
// ✅ GOOD - Focused, natural variations (4 phrases)
AppShortcut(
intent: SendMessageIntent(),
phrases: [
"Send message to \(.contactName) with \(.applicationName)",
"Text \(.contactName) using \(.applicationName)",
"Message \(.contactName) in \(.applicationName)",
"Write to \(.contactName) with \(.applicationName)"
]
)

// ❌ EXCESSIVE - Too many similar phrases (impacts performance)
AppShortcut(
intent: SendMessageIntent(),
phrases: [
"Send message to \(.contactName) with \(.applicationName)",
"Send a message to \(.contactName) with \(.applicationName)",
"Send text message to \(.contactName) with \(.applicationName)",
"Send a text message to \(.contactName) with \(.applicationName)",
// ... 15+ more variations
]
)

Include App Name for Disambiguation

✅ Recommended:

phrases: [
"Increment counter with \(.applicationName)",
"Add one using \(.applicationName)",
"Count up in \(.applicationName)"
]

❌ Avoid:

phrases: [
"Increment counter", // Too generic, conflicts with other apps
"Add one" // Ambiguous without context
]

Use Natural Prepositions

Choose prepositions that sound natural in conversation:

  • "with (.applicationName)" - Most common, works for actions
  • "using (.applicationName)" - Good for tool-like actions
  • "in (.applicationName)" - Natural for location-based commands
  • "from (.applicationName)" - Perfect for queries and data retrieval

Provide Multiple Variations

Offer 3-5 phrase variations to accommodate different user preferences:

phrases: [
"Increment counter with \(.applicationName)", // Formal
"Add one using \(.applicationName)", // Casual
"Count up in \(.applicationName)", // Alternative verb
"Bump counter with \(.applicationName)", // Colloquial
"Increase count using \(.applicationName)" // Descriptive
]

Keep Phrases Concise but Descriptive

  • Ideal length: 3-6 words (excluding app name)
  • Be specific: "Increment counter" vs. "Do something"
  • Avoid filler words: Skip "please", "can you", "I want to"

Common Phrase Patterns by Intent Type

Action Intents:

"[Action] [Object] with \(.applicationName)"
"[Verb] [Noun] using \(.applicationName)"

Query Intents:

"Get [Data] from \(.applicationName)"
"Check [Status] in \(.applicationName)"
"What's [Information] using \(.applicationName)"

Navigation Intents:

"Open [Page] in \(.applicationName)"
"Go to [Section] using \(.applicationName)"
"Show [Content] with \(.applicationName)"

Testing Your Phrases

  • Test with Siri: Speak each phrase to ensure recognition
  • Try variations: Users might not say exactly what you expect
  • Check conflicts: Ensure phrases don't overlap with system commands
  • User feedback: Monitor which phrases users actually use

Important Notes

  • Static intents must match Flutter handlers - ensure identifier consistency
  • Architecture Note: iOS App Intents framework requires static intent declarations at compile time for Siri/Shortcuts discovery. Dynamic registration from Flutter plugins alone is not sufficient.
  • Always use openAppWhenRun = true for intents that should open your app
  • Use ReturnsValue<String> & OpensIntent return type for intents that open the app