I’m working with AppIntents and AppEntity to integrate my app’s data model into Shortcuts and Siri. In the example below, I define a custom FoodEntity and use it as a @Parameter in an AppIntent. I’m providing dynamic options for this parameter via an optionsProvider.
In the Shortcuts app, everything works as expected: when the user runs the shortcut, they get a list of food options (from the dynamic provider) to select from.
However, in Siri, the experience is different. Instead of showing the list of options, Siri asks the user to say the name of the food, and then tries to match it using EntityStringQuery.
I originally assumed this might be a design decision to allow hands-free use with voice, but I found that if you use an AppEnum instead, Siri does present a tappable list of options. So now I’m wondering: why the difference?
Is there a way to get the @Parameter with AppEntity + optionsProvider to show a tappable list in Siri like it does in Shortcuts or with an AppEnum?
Any clarification on how EntityQuery.suggestedEntities() and DynamicOptionsProvider interact with Siri would be appreciated!
struct CaloriesShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: AddCaloriesInteractive(),
phrases: [
"Add to \(.applicationName)"
],
shortTitle: "Calories",
systemImageName: "fork"
)
}
}
struct AddCaloriesInteractive: AppIntent {
static var title: LocalizedStringResource = "Add to calories log"
static var description = IntentDescription("Add Calories using Shortcuts.")
static var openAppWhenRun: Bool = false
static var parameterSummary: some ParameterSummary {
Summary("Calorie Entry SUMMARY")
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(stringLiteral:"Add to calorie log")
}
@Dependency
private var persistenceManager: PersistenceManager
@Parameter(title: LocalizedStringResource("Food"), optionsProvider: FoodEntityOptions())
var foodEntity: FoodEntity
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
return .result(dialog: .init("Added \(foodEntity.name) to calorie log"))
}
}
struct FoodEntity: AppEntity {
static var defaultQuery = FoodEntityQuery()
@Property var name: String
@Property var calories: Int
init(name: String, calories: Int) {
self.name = name
self.calories = calories
}
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Calorie Entry")
}
static var typeDisplayName: LocalizedStringResource = "Calorie Entry"
var displayRepresentation: AppIntents.DisplayRepresentation {
DisplayRepresentation(title: .init(stringLiteral: name), subtitle: "\(calories)")
}
var id: String {
return name
}
}
struct FoodEntityQuery: EntityQuery {
func entities(for identifiers: [FoodEntity.ID]) async throws -> [FoodEntity] {
var result = [FoodEntity]()
for identifier in identifiers {
if let entity = FoodDatabase.allEntities().first(where: { $0.id == identifier }) {
result.append(entity)
}
}
return result
}
func suggestedEntities() async throws -> [FoodEntity] {
return FoodDatabase.allEntities()
}
}
extension FoodEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [FoodEntity] {
return FoodDatabase.allEntities().filter({$0.name.localizedCaseInsensitiveCompare(string) == .orderedSame})
}
}
struct FoodEntityOptions: DynamicOptionsProvider {
func results() async throws -> ItemCollection<FoodEntity> {
ItemCollection {
ItemSection("Section 1") {
for entry in FoodDatabase.allEntities() {
entry
}
}
}
}
}
struct FoodDatabase {
// Fake data
static func allEntities() -> [FoodEntity] {
[
FoodEntity(name: "Orange", calories: 2),
FoodEntity(name: "Banana", calories: 2)
]
}
}