Highlight
通过 App Intents 框架和 App Schemas,开发者可以让应用的内容和操作通过 Siri 访问,用户可以用自然语言查询日历事件、修改时间、发送消息,无需编写自定义自然语言处理代码。
核心内容
以前让应用与 Siri 配合需要大量工作。要定义意图、编写训练短语、处理自然语言解析,每个新功能都要重复这些步骤。
Apple 推出了 App Schemas(应用架构),用预定义的结构描述应用的内容和操作。开发者不需要处理自然语言,只需告诉系统”这是一个日历事件”或”这是一个日程操作”,Siri 就能理解。
这个 code-along 视频用 CometCal 项目演示完整流程。CometCal 是一个 SwiftUI 日历应用,支持显示今日事件、查看编辑、创建新事件、管理不同日历。视频一步步把它从只能触摸操作的应用,变成完全支持 Siri 语音控制的版本。
关键是三个步骤:让 Siri 理解应用内容(实体)、让 Siri 能执行操作(意图)、让 Siri 知道屏幕上显示什么(屏幕感知)。
详细内容
第一步:定义应用实体 (03:12)
App Schemas 将内容组织成域(Domain)。日历域(Calendar Domain)涵盖所有与日程相关的内容:事件、日历、参与者,以及操作它们的动作。
第一个实体是 CalendarEntity,代表日历本身。创建方式很直接:
import AppIntents
struct CalendarEntity: calendar_calendar {
@AppEntity(title: "Calendar")
static var entityQuery = CalendarEntityQuery()
var id: UUID
var title: String
// DisplayRepresentation 和其他必需属性
}
关键点:
calendar_calendar是日历域中的日历架构,Xcode 会自动补全所有可用架构@AppEntity宏标记这是一个应用实体,Siri 可以对其进行推理IndexedEntity协议让实体可以通过 Spotlight 索引捐赠,获得语义理解能力
为了让 Siri 能找到这些实体,需要实现 EntityQuery 协议:
struct CalendarEntityQuery: EntityQuery {
@Dependency private var calendarManager: CalendarManager
func entities(for identifiers: [UUID]) async throws -> [CalendarEntity] {
// 通过 ID 获取日历
return try await calendarManager.calendars(for: identifiers)
.map { $0.entity }
}
func allEntities() async throws -> [CalendarEntity] {
// 返回所有日历,供 Siri 作为选项提供
return try await calendarManager.allCalendars()
.map { $0.entity }
}
}
关键点:
@Dependency属性包装器注入共享资源,App Intents 会提供注册过的同一实例allEntities()让 Siri 知道有哪些日历可用,创建事件时可以提供选项
实体定义完成后,需要将其捐赠给系统索引。在 CalendarManager 中:
func createCalendar(_ title: String) throws -> CalendarModel {
let calendar = CalendarModel(title: title)
modelContext.insert(calendar)
// 捐赠实体到 Spotlight 索引
searchableIndex.indexAppEntities(
identifiers: [calendar.id],
type: CalendarEntity.self
)
return calendar
}
func updateCalendar(_ calendar: CalendarModel) throws {
// 更新后重新索引
searchableIndex.indexAppEntities(
identifiers: [calendar.id],
type: CalendarEntity.self
)
}
func deleteCalendar(_ calendar: CalendarModel) throws {
modelContext.delete(calendar)
// 从索引中删除
searchableIndex.deleteAppEntities(
identifiers: [calendar.id],
type: CalendarEntity.self
)
}
第二个实体:参与者 (07:59)
AttendeeEntity 代表事件中的参与者,但它使用 TransientAppEntity 协议而非 IndexedEntity:
struct AttendeeEntity: calendar_attendee {
var person: IntentPerson
var status: AttendeeStatusEntity
var type: AttendeeTypeEntity
var isOptional: Bool
}
关键点:
TransientAppEntity表示临时实体,不需要唯一标识符,不用于独立查询- 参与者依附于事件存在,同一人可能参加多个事件,分别索引会造成重复结果
IntentPerson是系统标准类型,包含姓名和联系人信息,便于跨应用共享
参与者使用两个 @AppEnum 类型:
enum AttendeeStatusEntity: calendar_attendeeStatus {
case accepted
case declined
case tentative
case pending
}
enum AttendeeTypeEntity: calendar_attendeeType {
case person
}
关键点:
- App Schema 定义可能的枚举值,应用选择适用的那些
- 如果应用使用不同术语,只需将现有模型映射到架构的枚举值
第三个实体:事件 (10:22)
EventEntity 是最核心的实体,同样使用 IndexedEntity:
struct EventEntity: calendar_event {
var id: UUID
var title: String
var startDate: Date
var endDate: Date
var calendar: CalendarEntity
var attendees: [AttendeeEntity]
var location: EventLocation?
var note: String?
var recurrence: Calendar.RecurrenceRule?
var alarms: [EventAlarm]?
var status: EventStatusEntity
}
关键点:
- 属性很多,但模式与之前相同
- 必需属性如
title、startDate直接连接 - 不用的可选属性如
travelTime可以留空 - 非架构属性如
isFavorite也可以添加
EventEntity 组合了其他实体:
var calendar: CalendarEntity // 所属日历
var attendees: [AttendeeEntity] // 参与者列表
Siri 通过 App Schemas 理解这些关系。
重复事件使用 Foundation 的 Calendar.RecurrenceRule:
var recurrence: Calendar.RecurrenceRule?
// 转换逻辑
recurrence: eventModel.frequency.map { frequency in
Calendar.RecurrenceRule(
frequency: frequency.toRecurrenceFrequency(),
interval: 1
)
}
位置和告警是联合值(Union Value),可以存储多种类型之一:
typealias EventLocation = @CalendarEventLocation.PlaceDescriptorOrString
typealias EventAlarm = @CalendarEventAlarm.DateOrDuration
location: location.map { .placeDescriptor($0) }
alarms: alarms.map { alarm in
switch alarm {
case .date(let date): return .date(date)
case .duration(let duration): return .duration(duration)
}
}
打开特定事件 (14:40)
定义实体后,Siri 可以查询内容,但点击结果只打开应用主页。需要 OpenEventIntent 来导航到具体事件:
struct OpenEventIntent: system_open {
static var title: LocalizedStringResource = "Open Event"
static var openProviderStyle: OpenProviderStyle = .siriAndSpotlight
@Parameter(title: "Event")
var target: EventEntity?
@Dependency private var navigationManager: NavigationManager
func perform() async throws -> some IntentResult {
guard let event = target else { return .result() }
navigationManager.open(event.id)
return .result()
}
}
关键点:
system.open是系统架构,用于打开实体openProviderStyle指定在 Siri 和 Spotlight 中可用- 点击事件结果时调用此意图,导航到详情视图
屏幕感知 (15:42)
用户可能不想说完整事件标题,而是说”这个事件”或”第三个事件”。需要两个视图修饰符:
在列表视图中:
List(events) { event in
EventRow(event: event)
.appEntityIdentifier(event.entityIdentifier)
}
在详情视图中:
EventDetail(event: event)
.userActivity(.entityIdentifier(event.entityIdentifier))
关键点:
.appEntityIdentifier让系统知道列表中显示哪些事件.userActivity告诉系统当前聚焦的具体事件- 只需两个修饰符,Siri 就能理解”这个事件”指的是哪个
创建事件意图 (17:16)
让 Siri 能执行操作,使用 calendar_createEvent 架构:
struct CreateEventIntent: calendar_createEvent {
static var title: LocalizedStringResource = "Create Event"
static var openAppWhenRun: Bool = false
@Parameter(title: "Title")
var title: String
@Parameter(title: "Start Date")
var startDate: Date
@Parameter(title: "End Date")
var endDate: Date
@Parameter(title: "Calendar")
var calendar: CalendarEntity?
@Parameter(title: "Location")
var location: EventLocation?
@Parameter(title: "Note")
var note: String?
@Dependency private var calendarManager: CalendarManager
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<EventEntity> {
// 解析参数
let resolvedLocation: String? = try location?.resolve()
// 执行创建
let event = try await calendarManager.createEvent(
title: title,
startDate: startDate,
endDate: endDate,
calendarId: calendar?.id,
location: resolvedLocation,
note: note
)
return .result(with: event.entity)
}
}
关键点:
- 架构定义参数列表,直接使用
@MainActor确保在主线程执行- 返回
EventEntity让 Siri 知道创建结果 - Siri 自动处理语言理解、询问澄清、确认细节
更新事件意图 (20:17)
更新意图使用 calendar_updateEvent 架构:
struct UpdateEventIntent: calendar_updateEvent {
@Parameter(title: "Event")
var event: EventEntity?
@Parameter(title: "Start Date")
var startDate: Date?
@Parameter(title: "End Date")
var endDate: Date?
@Parameter(title: "Location")
var location: EventLocation?
// ... 其他可选参数
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<EventEntity> & ShowsSnippetView {
guard let event = event else { return .result() }
var updates: EventUpdates = [:]
// 处理可选参数
if let startDate = startDate {
updates[.startDate] = startDate
}
// 处理可能被清除的参数
switch location.valueState {
case .set(let value):
updates[.location] = value
case .set(nil):
updates[.location] = nil // 明确清除
case .unset:
break // 不更新
}
let updated = try await calendarManager.updateEvent(event.id, updates: updates)
return .result(with: updated.entity, snippet: EventSnippetView(event: updated.entity))
}
}
关键点:
- 大多数参数是可选的,用户可能只改一两项
valueState区分三种情况:设置新值、明确清除为 nil、完全未设置ShowsSnippetView返回自定义 SwiftUI 视图而非默认卡片
删除事件意图 (22:30)
删除是最简单的操作意图:
struct DeleteEventIntent: calendar_deleteEvent {
@Parameter(title: "Event")
var event: EventEntity?
@Parameter(title: "Span")
var span: EventSpan?
@MainActor
func perform() async throws -> IntentResult {
guard let event = event else { return .result() }
if let span = span {
switch span {
case .thisEvent:
try calendarManager.deleteEvent(event.id)
case .futureEvents:
try calendarManager.deleteRecurringEvent(from: event.id, future: true)
}
}
return .result()
}
}
关键点:
- Siri 自动显示确认对话框
- 重复事件支持删除单次或所有未来事件
- 多个匹配时自动消歧
自定义结果视图 (21:21)
默认结果卡片来自 DisplayRepresentation。可以自定义:
struct EventSnippetView: SnippetView {
var event: EventEntity
var body: some Snippet {
Snippet(
image: event.icon,
title: event.title,
subtitle: formattedDateRange(event.startDate, event.endDate)
) {
// 自定义布局
}
}
}
在意图中返回:
func perform() async throws -> some IntentResult & ReturnsValue<EventEntity> & ShowsSnippetView {
// ... 执行逻辑
return .result(with: updated.entity, snippet: EventSnippetView(event: updated.entity))
}
关键点:
- SwiftUI 视图保持简单轻量
- 体现应用个性,如渐变、图标、颜色
- 适用于所有返回结果的意图
核心启发
-
智能日历助手:基于 EventEntity 的语义索引,用户可以问”下周有哪些与客户相关的会议?”、“本季度有多少次团队会议?“,Siri 直接从事件标题和备注中搜索答案。
-
语音快速创建:创建意图让用户可以快速说”创建明天下午 3 点和 Alex 的咖啡会议”,无需打开应用填写表单。结合位置解析,可以自动选择附近咖啡店。
-
批量修改助手:更新意图支持自然语言批量修改,如”把这个月所有周二的双周会改到周三”、“把这周所有线上会议改成线下”。利用重复规则和参数状态判断。
-
智能提醒整合:告警联合值可以与系统提醒整合,用户说”提前 30 分钟提醒我准备会议材料”,自动在事件中添加告警。
-
屏幕上下文操作:用户在查看事件详情时可以直接说”给参与者发消息确认出席”、“把地点分享给某人”。屏幕感知让操作更自然。
关联 Session
- 104 - Get to know App Intents — App Intents 核心概念入门,介绍 intents、entities、queries 及其与 Siri 和 Shortcuts 的连接
- 343 - App Intents: Advanced topics and best practices — App Intents 高级功能和最佳实践,深入优化与 Siri 配合的方式
- 242 - Agentic apps: Powered by Apple Intelligence — 代理式应用架构,了解如何让应用主动与用户协作
- 246 - Core Spotlight: LLM-powered search in your app — Core Spotlight 与 LLM 集成,强化语义搜索能力
- 295 - Testing your App Intents at scale — AppIntentsTesting 框架,自动化测试 App Intents
评论
GitHub Issues · utterances