在 WWDC 2026 上,一位 Apple 工程師拿了一款原本只會回應點按的 SwiftUI 行事曆 App,便讓 Siri 得以搜尋其中的事件、依名稱與備註內容回答相關問題、以語音建立與更新事件,並呈現自訂的結果卡片——而達成這一切,只寫了三個 struct、填了寥寥幾段程式碼片段。1 這項轉變背後的機制就是 App Schemas:一種以 Siri 早已理解的語彙來描述 App 內容與動作的方式,開發者端無需訓練語句、也無需自然語言處理。1 這場議程是一段圍繞名為 CometCal 的範例專案進行的 code-along,而宇宙主題之下的核心啟示其實關乎結構。您不必教會 Siri 您的詞彙,而是針對 Siri 早已熟知的形態宣告您的資料與動作,其餘便自然水到渠成。
本文走訪支撐這項成果的三大支柱:schema 領域模型、透過 IndexedEntity 向 Spotlight 進行語義 donation,以及讓語音驅動的更新得以安全進行的螢幕感知與 valueState 之別。以下內容皆直接取自該場議程。本主題與 App Intents 中的背景執行有所不同,後者談的是不啟動 UI 即執行工作;本文聚焦的是 Siri 如何對您的內容進行推理並據以行動。
TL;DR
App Schemas以 Siri 早已理解的語彙描述 App 的實體、動作參數與輸出,並組織成 App Schema Domains;行事曆領域涵蓋事件、行事曆、與會者以及作用於其上的動作,開發者端無需訓練語句、也無需 NLP。1- schema 化的實體源自 Xcode 程式碼片段:輸入像
calendar_這樣的領域前綴並挑選一個片段(例如calendar_calendar),即會搭建出含巨集、屬性、display representation 與 query stub 的實體。1 - 讓實體遵循
IndexedEntity協定,並透過indexAppEntities將其 donate 到CSSearchableIndex(並透過deleteAppEntities移除),Siri 便能依名稱、依屬性或依情境解析它,無需自訂屬性 query。1 - 兩個 view modifier——清單上的
.appEntityIdentifier與詳細檢視上的.userActivity(各自帶有一個EntityIdentifier)——賦予 Siri 螢幕感知,使「寄電子郵件給這個事件中的人」這類請求無需指名事件即可解析。1 - 更新用的 intent 會暴露
IntentParameter.valueState,其中帶值的.set、帶 nil 的.set與.unset分別代表新值、明確清除與缺席的參數,使 Siri 驅動的編輯保持明確無歧義。1
App Schemas 究竟是什麼(Session 344)
來自 Swift Intelligence Frameworks 團隊的 Justin 在開啟 Xcode 之前先說明 App Schemas,從 3:12 開始。
Siri 透過 App Intents 框架觸及 App,而 Apple Intelligence 則為其上的推理提供動力。1 CometCal 的起點問題很單純:「目前 Siri 完全不知道在 CometCal 裡,行事曆或事件代表什麼意義。」1 App Schemas 正是要彌合這道落差。如該場議程所言,它們「以 Siri 早已能理解的語彙描述我 App 的內容與動作。它們定義我實體的結構、我動作的參數以及輸出。我這端無需訓練語句、也無需自然語言處理。」1
其組織單位是 App Schema Domain。行事曆領域「涵蓋與排程相關的一切:事件、行事曆、與會者,以及作用於其上的動作。」1 由於這些形態是預先定義好的,編目工作便由編輯器代勞。工程師建立一個 CalendarEntity 檔案、匯入 AppIntents、輸入 calendar_,於是「Xcode 便在自動完成中直接列出行事曆領域裡的每一個 schema。」1 選取 calendar_calendar 即會填入整套結構:實體巨集、屬性、一個 display representation 與 query stub,產生該場議程所稱「一個 schema 化的實體,一個 Siri 能據以推理的型別。」1
命名慣例值得審慎留意。schema 片段的名稱在編輯器中以小寫、底線前綴的識別字形式出現(calendar_calendar、calendar_attendee、calendar_event、calendar_createEvent、calendar_updateEvent,外加 calendar_attendeeStatus 與 calendar_attendeeType 等列舉片段),而口述的逐字稿也是如此呈現。它們所搭建出的 Swift 型別(一個 @AppEntity 巨集、一個 DisplayRepresentation、query 協定的遵循)則遵循一般的 Swift 大小寫慣例。在據以開發之前,請對照 Apple 的 App Intents 文件與可下載的 CometCal 範例專案,確認每個符號的確切拼寫與大小寫,因為一段口述的 code-along 並非大小寫的精確參照。
schema 模型的回報,是以極少的程式碼換取廣泛的觸及範圍。該場議程把整個內容層概括為「三個 struct,再填上幾段程式碼片段。」1 CometCal 建立了三個豐富度遞增的實體:一個行事曆、一個與會者,以及一個把前兩者匯聚在一起的事件。事件「與先前建立的其他實體組合」:其行事曆是一個 CalendarEntity,其與會者是一個 AttendeeEntity 陣列,而「Siri 透過 App Schemas 理解這些關係。」1 schema 也決定了哪些是必填、哪些是選填。標題或開始日期之類的要項可直接接上;App 未使用的選填 schema 屬性(該場議程點名了行程時間與虛擬地點)可維持未設定;而存在於資料模型卻不在 schema 上的屬性,例如 isFavorite,仍可加進實體。1
事件上還現身了另外兩項 schema 機制。union 值讓單一屬性能持有數種型別其中之一:地點可以是「來自 GeoToolbox 框架的 PlaceDescriptor,也可以是 String」,而鬧鐘可以是 Duration 或 Date。1 重複屬性使用 Foundation 的 Calendar.RecurrenceRule,並針對每日、每週、每月與每年的情況與 CometCal 自身的頻率列舉互相轉換。1 schema 化的列舉(該場議程指向一個它稱為 EventEntityStatus 的事件狀態列舉,以及前述的與會者列舉)從片段中完整到位,App 採用適用的 case 即可;若 App 使用不同的術語,便把既有模型對應到 schema 的 case 上,「好讓 Siri 能辨識這個形態。」1
透過 IndexedEntity 進行語義 donation
schema 給了 Siri 一套語彙。donation 則給了 Siri 可供推理的實際資料。兩者是各自獨立的步驟,而該場議程明確指出,漏掉第二步是很容易發生的:「IndexedEntity 定義了我索引內容的形態,但實體仍然需要被 donate。」1
讓實體遵循 IndexedEntity 協定,正是讓比對得以依意義而非僅依文字進行的關鍵。1 原因在於搜尋索引。遵循該協定「讓我的 App 能運用 Spotlight 索引來 donate 實體,從而獲得語義理解的好處」,而一旦實體被 donate,「Siri 便能依名稱、依屬性或依情境解析它,無需自訂屬性 query。」1 最後這句話正是重點所在。您無需為「組員午餐」或「提及氧氣的事件」撰寫任何專屬的比對器。Siri 直接搜尋 donate 進來的標題與備註內容,並「用 App 的內容回答每一個問題。不需要自訂自然語言……只需要實體與 schema。」1
donation 透過 CSSearchableIndex 進行。CometCal 持有一個 CSSearchableIndex 實例,在其 CalendarManager 的初始化器中以一個 App 專屬的名稱建立。1 該場議程所述的規則是:「每當行事曆——或就此而言任何已索引的實體——有所變更時,索引就必須更新。」1 因此資料層在寫入時即進行 donate:建立路徑在回傳前以 searchable index 呼叫 indexAppEntities,更新路徑為變更後的實體重新索引,刪除路徑則呼叫 deleteAppEntities,「並傳入該實體的 id 與型別。」1 接上行事曆實體後,工程師建立了一個名為「Lunar Orbit Log」的行事曆,滑動至搜尋,便連同其圖示與標題一併找到了它——這正是 donation 已生效的證明。1
並非每個實體都該被索引,而與會者正是闡明此規則的反例。AttendeeEntity 遵循的是 TransientAppEntity 而非 IndexedEntity,亦即「一個不需要唯一識別碼、也並非用來查詢的暫時性實體。」1 其理由在於建模的紀律:在 CometCal 中,與會者代表的是「某人對某個特定事件的參與,而非那個人本身」,同一個人可以參加許多事件,而「把每一次參與分別索引,會在 Spotlight 中製造出重複的結果。」1 既然與會者一律是透過其事件來觸及,便沒有需要維護的獨立查找路徑,而 TransientAppEntity「把這一點講明了……無需撰寫 query,也無需維護索引。」1 與會者還引入了 IntentPerson,亦即「系統用來表示一個具有姓名與聯絡資訊的人的標準方式」,在把與會者的電子郵件交給「郵件」App 草擬訊息時相當有用。1
已索引的實體仍然需要其 query 的接管機制。query 透過 @Dependency 屬性包裝器持有資料層,這「正是 App Intents 將共享資源注入 intent 與 query 的方式」,因此 query 使用的是那個唯一註冊過的 CalendarManager 而非一個全新的實例,且該 query 因為 manager 是 main-actor 而同樣標記為 main-actor。1 必要的 EntityQuery 方法會在系統已知 ID 的情況下依 ID 擷取,而遵循 EnumerableEntityQuery 並提供一個 allEntities 方法,則能讓系統在 Siri 於建立事件時需要提供可用行事曆作為選項時,將其逐一列出。1 一個 DisplayRepresentation(標題加上一張系統行事曆圖像)則告訴 Siri 與 Spotlight 該如何呈現該實體。1
有一道值得點名的導覽接縫,因為單靠 donation 只會把使用者帶到 App 的主畫面。一個遵循 system.open schema、以 EventEntity 為目標、並告知導覽層路由至該處的 OpenEventIntent,便彌合了這道落差:系統會「每當有人在 Spotlight 或 Siri 中點按某個事件結果,或要求 Siri 開啟某個事件時」便加以叫用,於是被點按的結果便直接開啟至該事件的詳細檢視。1
螢幕感知與 valueState 之別
前兩根支柱讓 Siri 能依名稱尋得內容。第三根則讓 Siri 能運用使用者眼前既有之物,繼而毫無歧義地對其行動。
螢幕感知只需「兩個 view modifier。」1 在清單檢視中,.appEntityIdentifier 附加於清單上,「為每一個事件實體傳入一個 EntityIdentifier」,這「把清單與其實體連結起來,於是當有人正在瀏覽清單時,系統便知道哪些事件在螢幕上。」1 在詳細檢視中,.userActivity 為焦點所在的單一事件帶上一個 EntityIdentifier,告知系統「那一個特定事件正居於正中央,好讓 Siri 能把『這個事件』精確解析為正在檢視的那一個。」1 兩者就位後,身處某事件詳細檢視的使用者便可說「寄電子郵件給這個事件中的人,並請某人帶巧克力和棉花糖來」,Siri 便運用它對螢幕上事件的理解去找出與會者,並把他們交給「郵件」App——完全無需提供標題。1
對內容行動,與讀取內容是同一套模式,只是反向執行。intent 同樣源自片段。calendar_createEvent 片段搭建出含巨集、schema、schema 所需參數與一個 perform stub 的 intent。1 perform 邏輯是該場議程直白道出的三步形態:「把 intent 的參數解析成資料層能理解的東西、執行該動作,再把結果以實體形式回傳。」1 以建立為例,這意味著從 union 值中取出地點、在有提供的情況下轉換重複設定、呼叫 manager 的建立方法,再回傳一個 EventEntity。1 由於該 intent 遵循一個 schema,「Siri 便能處理所有繁重工作。詮釋語言、要求釐清以及確認細節」,因此開發者從不需要撰寫那段對話。1
更新則浮現出讓語音驅動的編輯值得信賴的那份微妙之處。calendar_updateEvent 上的多數參數都是選填,因為使用者通常只會更改一兩項,而「事件參數是 Siri 所解析的對象;其餘一切都是選填。」1 單純的 nil 檢查無法回答真正的問題。如該場議程所述:「當重複設定為 nil 時,那究竟代表『別更動它』還是『移除它』?單純的 nil 檢查並不告訴我自己面對的是哪一種情況。」1 答案是 IntentParameter.valueState,之所以得以暴露,是因為 intent 巨集把每個屬性都包進了一個 IntentParameter。這三種狀態各帶不同的意義:「帶有實際值的 .set 代表提供了一個新值。帶有 nil 值的 .set 代表它被明確清除。.unset 代表該參數並非請求的一部分。」1 這項區分「適用於任何『清除其值是一項有意義動作』的選填參數」,這正是為何「別重複這個事件」能可靠地清除重複設定,而非任其原封不動。1
另有兩處收尾之筆讓動作層更趨完整。一張自訂結果卡片取代了 Siri 預設的 display representation 卡片:在 perform 方法的回傳型別上加入 ShowsSnippetView,並傳入一個備妥的 SwiftUI view(該場議程的版本接受一個 EventEntity),便會在 Siri 內部渲染出 App 自身的樣式——這個做法「對任何其他會回傳結果的 intent 同樣適用。」1 而 DeleteEventIntent,亦即「三者中最簡單的一個」,只需接受該事件以及一個給重複事件用的選填區間;Siri 會「在移除任何東西之前自動處理確認對話框」,並在符合的事件不只一個時加以消歧。1
重點整理
給採用 App Intents 的 iOS 開發者:
- 先求助於 schema。在 Xcode 中輸入像
calendar_這樣的領域前綴,讓自動完成列出可用的片段;片段會搭建出巨集、屬性、display representation 與 query stub,於是您填入的是型別與對應關係,而非從頭發明結構。1 - 逐一實體判斷它是否值得一個索引。讓持久、可查詢的內容遵循
IndexedEntity並加以 donate;對於那些一律透過父層觸及、一旦索引只會污染 Spotlight 的參與式紀錄(CometCal 的與會者),則使用TransientAppEntity。1 - 在開發之前,對照 Apple 的 App Intents 文件與 CometCal 範例確認符號的確切拼寫與大小寫,因為 code-along 中的名稱出自一段口述逐字稿。
給設計語音與 Apple Intelligence 流程的團隊:
- 把 donation 當成寫入路徑的職責。在建立與更新時呼叫
indexAppEntities、在刪除時呼叫deleteAppEntities,皆以實體的 id 與型別為鍵,好讓 Siri 的索引絕不與資料脫節。1 - 及早加入螢幕感知:清單上的
.appEntityIdentifier與詳細檢視上的.userActivity(各自帶有一個EntityIdentifier),讓使用者能說「這個事件」而非其標題。1 - 在更新 intent 中明確處理
valueState。針對帶值的.set、帶 nil 的.set與.unset分別分支,好讓一次明確的清除絕不會被讀作「維持不變」。1
常見問題
App Intents 中的 App Schemas 是什麼?
App Schemas 以 Siri 早已理解的語彙描述 App 的內容與動作:它們定義 App 實體的結構、其動作的參數以及輸出,開發者端無需訓練語句、也無需自然語言處理。它們組織成 App Schema Domains;行事曆領域涵蓋事件、行事曆、與會者以及作用於其上的動作。在 Xcode 中,您透過輸入像 calendar_ 這樣的領域前綴並挑選一個如 calendar_calendar 的程式碼片段來採用某個 schema,該片段便會搭建出實體。1
Siri 如何依名稱或情境解析我 App 的內容?
讓實體遵循 IndexedEntity 協定,並透過在建立與更新時呼叫 indexAppEntities、在刪除時以實體的 id 與型別呼叫 deleteAppEntities,將其 donate 到一個 CSSearchableIndex(即 Spotlight 索引)。donation 賦予 Siri「語義理解」,讓它能依名稱、依屬性或依情境解析實體而無需自訂屬性 query,這也包括為「哪些事件提到氧氣?」這類問題搜尋備註內容。1
我何時該使用 TransientAppEntity 而非 IndexedEntity?
對於一個不需要唯一識別碼、也並非用來查詢的暫時性實體,請使用 TransientAppEntity。CometCal 的與會者就符合,因為與會者代表的是某人對某個特定事件的參與,而非那個人;同一個人會參加許多事件,而把每一次參與分別索引會製造出重複的 Spotlight 結果。既然與會者只透過其事件觸及,便沒有獨立的查找路徑,因此這個暫時性實體既無需 query、也無需索引。1
valueState 是什麼,它為何對更新 intent 至關重要?
在一個更新 intent 中,App Intents 巨集把每個屬性都包進一個會暴露 valueState 的 IntentParameter。它能區分 nil 檢查所無法區分的三種情況:帶值的 .set 代表一個新值,帶 nil 的 .set 代表該值被明確清除,而 .unset 代表該參數並非請求的一部分。這項區分讓 Siri 驅動的編輯得以清除某項屬性(例如「別重複這個事件」),而不至於被誤認為「維持不變」。1
我該如何賦予 Siri 對我 App 的螢幕感知?
加入兩個 view modifier。把 .appEntityIdentifier 放在清單檢視上,為每個事件實體傳入一個 EntityIdentifier,好讓系統在瀏覽時知道哪些事件在螢幕上。把帶有 EntityIdentifier 的 .userActivity 放在詳細檢視上,好讓系統知道某個特定事件正居於焦點。兩者合起來,便讓使用者能說「寄電子郵件給這個事件中的人」,並讓 Siri 把「這個事件」精確解析為正在檢視的那一個。1
本文隸屬於一個關於 Apple intelligence 框架的文章群。關於 App Schemas 所立基的那個框架,請從 App Intents:Apple 通往您 App 的全新 API 開始。關於不啟動 UI 即執行 intent 工作——這與本文所談的內容推理是另一回事——請閱讀 App Intents 中的背景執行。關於語義解析背後更宏觀的 Spotlight donation 故事,請參閱 裝置端 AI 與 Spotlight 媒體索引。整個系列的總覽中心是 Apple Ecosystem Series。
參考資料
-
Apple, WWDC 2026 session 344, Code-along: Make your app available to Siri。App Schemas 與 App Schema Domains(行事曆領域;無訓練語句、無 NLP);透過 Xcode 片段建立 schema 化實體(
calendar_calendar、calendar_attendee、calendar_event、calendar_createEvent、calendar_updateEvent、calendar_attendeeStatus、calendar_attendeeType);IndexedEntity以及透過CSSearchableIndex經indexAppEntities/deleteAppEntities進行的 Spotlight donation;依名稱、屬性或情境的解析;TransientAppEntity與與會者的建模理據;IntentPerson;union 值(來自GeoToolbox的PlaceDescriptor、String;Duration或Date鬧鐘)與Calendar.RecurrenceRule;@Dependency包裝器、EntityQuery、EnumerableEntityQuery與DisplayRepresentation;system.open的OpenEventIntent;透過帶有EntityIdentifier的.appEntityIdentifier與.userActivity達成的螢幕感知;IntentParameter.valueState(.set/.unset);ShowsSnippetView自訂結果卡片;DeleteEventIntent的確認與消歧;以及為自動化測試所引用的AppIntentsTesting框架,皆以此為來源。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
相關文章
App Intents 是 Apple 通往你 App 的全新 API
我在 2026 年 2 月 8 日於 Water 中推出了一個 App Intent。本文說明 Apple Intelligence 在 iOS 26 中對第三方 App 有何期待,以及為何 App Intents 才是真正重要的那份契約。
7 分鐘閱讀Instruments 27 在 App 反應性方面的新功能
Instruments 27 新增了 Top Functions、Run Comparisons、Swift executors 工具以及全新的 Inspector 面板,用以診斷 CPU、actor 與系統呼叫造成的卡頓。
4 分鐘閱讀代理取代的是審查者,而非審查本身
一篇2026年的論文主張程式碼代理已終結人類程式碼審查。我實際執行它所開出的流程:審查者這個角色正在消亡,但審查本身正在轉移。
1 分鐘閱讀