iOS 27 的 SwiftData:Observation 與 History
SwiftData 隨 iOS 17 推出時,提供了兩種監看資料的方式:在 SwiftUI 視圖中使用 @Query,或為其他所有情況在 ModelContext 上手動串接通知。但這兩者都沒有涵蓋對同步應用程式而言最關鍵的情況,也就是得知另一台裝置何時變更了資料儲存。iOS 27 在同一個版本中一次補上這兩處缺口。ResultsObserver 讓變更追蹤成為一個可在視圖之外持有的一流物件,而 HistoryObserver 則會觀察 SwiftData 的持久化歷史,並在新交易進來時遞增一個可觀察的計數器,讓您的同步程式碼只需拉取最新的變更。iOS 27 將 observation 視為一項基本要素,而非 SwiftUI 的附帶效果。1
這個切入角度與本系列的其餘內容一脈相承:SwiftData 起步便宜,協調起來卻昂貴。在視圖階層之外監看一次提取,意味著得親手重建 @Query 所做的事。讓資料儲存與外部伺服器保持同步,或對來自應用程式擴充功能的寫入做出反應,則意味著得自行走訪持久化歷史並調和各筆交易。iOS 27 為這兩項工作都提供了一個遵循 Observable 的具名型別,因此早已驅動您視圖的那套 SwiftUI 更新機制,如今也同樣驅動您的同步層。
TL;DR/重點摘要
ResultsObserver會觀察並追蹤模型情境中某一組持久化模型集合的變更,在底層資料變動時提供即時更新。它是Observable的,因此 SwiftUI 視圖會自動更新,而且它能在@Query無法觸及的視圖之外運作。2HistoryObserver會觀察 SwiftData 的持久化歷史,並公開一個可觀察的屬性eventCounter,在新交易抵達時遞增。您可依模型型別與交易作者進行篩選,再呼叫ModelContext.fetchHistory來讀取變更。對於與外部伺服器同步,或對應用程式擴充功能的寫入做出反應,它就是結構化的解答。3@Attribute(.codable)會使用屬性的 codable 表示形式來儲存該屬性,讓您能以宣告式的方式持久化一個Codable值型別,而無需ValueTransformer。4- 這三者在 27.0 beta 中橫跨 iOS、iPadOS、macOS、Mac Catalyst、tvOS、visionOS 與 watchOS 同步推出。234
在視圖之外監看一次提取:ResultsObserver
@Query 既出色又受限。它存在於 SwiftUI 視圖中,會在述詞變更時重新執行,並把一個陣列交給視圖。限制在於它的所在位置。視圖模型、同步協調器、匯出工作或背景調和器都擁有會變動的資料,卻沒有可以倚賴的 @Query。在 iOS 27 之前,這些呼叫端會訂閱 ModelContext 的儲存通知並親手重新提取,而這正是手動重建 @Query 內部早已完成的工作。
ResultsObserver 就是 iOS 27 給出的具名解答。其宣告道出了它的本質:2
final class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable
這個類別會自動監看符合指定提取條件的模型變更,並維護一組已提取的結果,這使它成為讓任何消費端與持久化資料保持同步的工具,而不僅限於視圖。2 您可以用兩種方式設定它:用一個完整的 FetchDescriptor,或用個別的篩選述詞與排序描述器。2 兩條 SectionName 路徑對分組清單很重要;當您不需要分段時,就把 Never 當作 SectionName 型別參數傳入。2
相較於舊有的手動做法,它的回報在於遵循了 Observable。ResultsObserver 是 Observable 的,這讓 SwiftUI 視圖能在結果變動時自動更新,方式與 @Observable 模型物件驅動視圖失效如出一轍(詳見 @Observable 內部原理)。2 持有 ResultsObserver 的同步協調器無需寫下一行 NotificationCenter 程式碼就能取得變更通知,而任何讀取該觀察器結果的視圖也能免費重新繪製。
在 session 274 中,Apple 為 @Query 無法服務的情況引入了 ResultsObserver,透過 Swift Observation 在應用程式的任何位置提取並觀察資料儲存,包括 state object,或一款從未碰觸 SwiftUI 的遊戲。5
ResultsObserver 將 query 風格的 observation 帶到 SwiftUI 視圖之外的程式碼。
import SwiftData
import Observation
@Observable
final class ShoppingListModel {
let observer: ResultsObserver<ShoppingItem, Never>
init(context: ModelContext) {
let descriptor = FetchDescriptor<ShoppingItem>(
sortBy: [SortDescriptor(\.sortOrder)]
)
// No sectioning, so SectionName is Never.
observer = ResultsObserver(context: context, fetchDescriptor: descriptor)
}
}
Apple 已發佈的參考文件證實了類別宣告與設定介面(一個 FetchDescriptor,或篩選述詞與排序描述器;不分段時用 Never 作為 section name),但在撰寫本文時略過了確切的初始化器簽名,因此請將這些範例中的呼叫形式視為示意性質,並對照 SDK 確認參數標籤。
思維上的轉變在於:提取變成了一個您所擁有並四處傳遞的物件,而非困在視圖 body 中的屬性包裝器。@Query 回答的是「這個視圖顯示什麼?」ResultsObserver 回答的則是「無論我把這次提取持有在何處,它目前的狀態是什麼?」第二個問題,才是非視圖呼叫端真正會問的問題。
對歷史變更做出反應:HistoryObserver
@Query 與 ResultsObserver 兩者都留下未解的缺口,是那些源自您行程內提取之外的變更。每次儲存資料儲存時,SwiftData 都會記錄一筆歷史交易,描述變更了什麼、變更從何而來,以及一個用以識別它的 token。讓資料儲存與外部伺服器保持同步,或對來自應用程式擴充功能的寫入做出反應,就意味著得自行走訪那份持久化歷史。iOS 27 為這項工作提供了一個具名觀察器。3
HistoryObserver 就是結構化的解答:3
final class HistoryObserver
這個觀察器會監看 SwiftData 的持久化歷史,並讓您的程式碼能在新交易被加入時做出反應。3 如果您只需要某些種類的變更,就依模型型別與交易作者進行篩選,如此您只需對在意的寫入做出反應,而非每一次資料儲存的變動。3
整個介面就是一個可觀察的屬性:eventCounter。當新交易進入持久化歷史時,這個計數器就會遞增;您觀察它,並在每次遞增時呼叫 ModelContext.fetchHistory API,只讀取最新的變更。3 歷史 token 就存在於交易本身,因此 fetchHistory 只會回傳新增的部分,而不會重新掃描整個資料儲存。這讓同步處理常式在歷史日益增長時仍保持快速。
import SwiftData
import Observation
// Illustrative call shapes; confirm parameter labels against the SDK.
let historyObserver = HistoryObserver(container: modelContainer, authors: "App")
// Observe eventCounter; on each increment, fetch and process the new history.
let token = withContinuousObservation(of: historyObserver.eventCounter) {
Task { await processChanges() }
}
依交易作者篩選,正是讓伺服器同步正確無誤的關鍵細節。在 Apple 的範例中,觀察器把 "App" 當作作者傳入,使它只對應用程式所做的變更做出反應,而不會把來自伺服器的變更又重播回伺服器。3 在每次遞增時,您的 processChanges 步驟會呼叫 ModelContext.fetchHistory 來讀取新交易並上傳。本系列各應用程式以手工串接歷史所解決的多行程與外部同步模式(參見 SwiftData 結構規範),如今有了一個可供依附的框架接縫。
與歷史搭配使用的輕量讀取
一旦 HistoryObserver 喚醒您的同步處理常式,下一個問題就是該做多少工作。一個天真的處理常式會在每次遞增時重新提取受影響的模型,這會具現出您可能根本不需要的完整模型物件。SwiftData 提供了兩種讀取方式,能在不具現任何東西的情況下回答更狹隘的問題,而且兩者都能自然地與歷史觀察搭配。
ModelContext.fetchCount(_:) 會以一個單純的 Int 回傳符合某個提取描述器的模型數量,而不載入符合的物件:6
func fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModel
ModelContext.fetchIdentifiers(_:) 會以 [PersistentIdentifier] 回傳符合項,同樣不載入其背後的模型。一個 batchSize 多載則會針對大型結果集分批串流這些識別符:6
func fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModel
SwiftData Group Lab 提出了一種把這些直接綁到歷史觀察上的模式。當歷史變更抵達時,先提取受影響的識別符,並在決定是否重新載入之前,將它們與視圖實際顯示的內容相比較,如此就能避免具現您不需要的物件。6 對使用者沒在看的那一列做變更並不會移動任何像素,而一次識別符比較就能以一次鍵值查找的成本告訴您這件事,而非一次完整提取。fetchCount 回答的是更便宜的問題:究竟有沒有任何項目符合,這就足以判斷某個徽章或空狀態是否需要切換。
乾淨地持久化一個值型別:@Attribute(.codable)
第三項新增小巧而實用。SwiftData 原生儲存 Swift 的基本型別與 @Model 關聯,但若某個屬性的型別是自訂的 Codable 值(一個持有少數欄位的 struct、一個帶有關聯值的 enum),則需要一個 ValueTransformer 與 .transformable(by:) 屬性,而這是從巨集介面背後滲漏出來的 Core Data 繁文縟節。
iOS 27 加入了 codable 儲存選項:4
static var codable: Schema.Attribute.Option { get }
這個選項會使用屬性的 codable 表示形式來儲存該屬性,因此一個 Codable 值型別會透過它自身的 Encodable/Decodable 遵循來持久化,無需註冊任何 transformer。4 您套用它的方式與任何其他屬性選項相同:
import SwiftData
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}
@Model
final class Place {
var name: String
// Persisted via Coordinate's own Codable conformance.
@Attribute(.codable) var location: Coordinate
}
實用的準則是:當某個屬性是一個自成一體、且不值得擁有自己 @Model 資料表的 Codable 值時,就動用 .codable。一對座標、一個小型設定 struct、一個帶有酬載的 enum:這些是資料,而非實體,.codable 會透過它們早已定義好的表示形式內嵌儲存它們,而不會強迫您使用 transformer 或一個人為的關聯。
Apple 對其取捨直言不諱。一個 codable 屬性的內容對 SwiftData 而言是不透明的,因此您無法在述詞中用它們來篩選結果,也無法用於排序描述器;而 codable 型別形狀的變更(新增或移除屬性)並不會觸發遷移,所以它的 Codable 實作必須維持向前與向後相容。5 Apple 將 .codable 定位為針對您不擁有之型別的逃生口;對於您自行定義的型別,把它們建模為 SwiftData 模型或受支援的值型別,能讓排序、篩選與索引留在資料表上。5
何時該動用哪一個
這三項新增各自回答三個不同的問題,而問題本身會告訴您該用哪一個。
- 當非視圖呼叫端需要一次即時提取時,動用
ResultsObserver。 一個視圖模型、一個協調器、一項匯出工作,任何擁有會變動的資料、卻不是 SwiftUIbody的東西。在視圖內部,@Query仍是較輕的工具;一旦消費端不是視圖,這個觀察器便當之無愧。2 - 當您要讓資料儲存與您應用程式之外的某物同步時,動用
HistoryObserver。 一個外部伺服器,或一個寫入同一資料儲存的應用程式擴充功能。觀察eventCounter,依模型型別與交易作者篩選,並在每次遞增時呼叫ModelContext.fetchHistory只讀取新交易。3 - 當某個屬性是一個
Codable值、而非一個實體時,動用@Attribute(.codable)。 隨其擁有者一同移動的小型 struct 與 enum。若該型別需要自己的識別性、關聯或查詢,那它要的是@Model;若它只是內嵌資料,.codable便省去了 transformer。4
這兩個觀察器可以組合運用。ResultsObserver 讓您行程內的提取保持即時;HistoryObserver 則在一次遠端推送值得對變更採取行動時告知您。一個真正進行多裝置同步的應用程式會兩者並用,並在過程中用 .codable 讓它的值型別欄位保持誠實。
FAQ
ResultsObserver 與 @Query 有何不同?
@Query 是一個存在於視圖內部、並為該視圖供給一個陣列的 SwiftUI 屬性包裝器。ResultsObserver 則是一個您能在任何地方建立並持有的獨立類別,包括視圖階層之外,它會觀察並追蹤模型情境中某一組持久化模型集合的變更。2 由於該觀察器是 Observable 的,讀取它的 SwiftUI 視圖仍會自動更新,因此它同時涵蓋了視圖內的情況,以及 @Query 無法觸及的視圖模型或協調器情況。2
HistoryObserver 實際上觀察什麼?
它觀察 SwiftData 的持久化歷史,也就是 SwiftData 每次儲存資料儲存時所寫入的交易記錄。3 它公開一個可觀察的屬性 eventCounter,在有新交易可用時遞增;您可以依模型型別與交易作者篩選,使得只有您在意的變更會推動這個計數器。3 在每次遞增時,您的程式碼會呼叫 ModelContext.fetchHistory API 來讀取新交易,這使它成為與外部伺服器同步、或對應用程式擴充功能寫入做出反應的結構化處理常式。3
我能同時使用 ResultsObserver 與 HistoryObserver 嗎?
可以,而且一個同步應用程式通常就該如此。ResultsObserver 會隨本地情境變動讓行程內的提取保持最新;HistoryObserver 則浮現記錄於持久化歷史中、並依模型型別與交易作者篩選過的變更。23 兩者都是您能從 SwiftUI 視圖或另一個觀察器做出反應的可觀察物件,因此它們能嵌入同一條反應式流程,無需各自獨立的通知處理。23
何時該使用 @Attribute(.codable) 而非關聯?
當屬性是一個沒有獨立識別性、自成一體的 Codable 值型別時,使用 .codable,因為這個選項會透過它自身的 codable 表示形式來儲存該屬性。4 當值是一個擁有自己生命週期、識別性或查詢的真正實體時,使用 @Model 關聯。分界線在於:這個東西究竟是隸屬於其擁有者的資料,還是一個會被其他列參照的實體。
完整的 Apple Ecosystem 系列:SwiftData 結構規範 談 observation 所建立其上的遷移成本;SwiftData 遷移指南 談 VersionedSchema 與 MigrationPlan 機制;@Observable 內部原理 談這些類別所接入的 observation 模型;SwiftUI 內部原理 談其底層的框架基底。系列中樞在 Apple Ecosystem 系列。若想了解更廣的「iOS 結合 AI 代理」脈絡,請參閱 iOS Agent 開發指南。
參考資料
-
Apple Developer Documentation: SwiftData. The framework reference covering
@Model,ModelContext,ModelContainer, queries, and the iOS 27 observation additions. ↩ -
Apple Developer Documentation:
ResultsObserver(iOS 27.0 beta). “Observes and tracks changes to a collection of persistent models in a model context.” Declared asfinal class ResultsObserver<Element, SectionName> where Element : PersistentModel, SectionName : Hashable; configurable with aFetchDescriptoror with filter predicates and sort descriptors;Observable, so SwiftUI views update automatically; passNeverasSectionNamewhen no sectioning is needed. ↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, WWDC26 session 274, What’s new in SwiftData, and Apple Developer Documentation:
HistoryObserver(iOS 27.0 beta). Declared asfinal class HistoryObserver. Per session 274, it observes SwiftData’s persistent history and “has a single observable property” (eventCounter); “when new transactions are available in the persistent history, the eventCounter increments,” and “your code can observe the eventCounter and when it increments, use ModelContext.fetchHistory API to fetch the latest changes.” It “lets you filter by model type and transaction author”; the session’s example passes"App"as the author so app-originated changes are not replayed back to an external server. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation:
codable(iOS 27.0 beta). “Uses the property’s codable representation to store the property.” Declared asstatic var codable: Schema.Attribute.Option { get }. ↩↩↩↩↩↩ -
Apple, WWDC26 session 274, What’s new in SwiftData. Apple introduces
ResultsObserver, which “fetches data from your SwiftData store and then observes your store for changes” but “works anywhere in your app (independent of SwiftUI views) using Swift Observation,” and names a state object or a game written in SceneKit as cases@Querycannot reach. ↩↩↩ -
Apple Developer Documentation:
fetchCount(_:)andfetchIdentifiers(_:)onModelContext.fetchCountis declaredfunc fetchCount<T>(_ descriptor: FetchDescriptor<T>) throws -> Int where T : PersistentModeland “returns the number of models that match the criteria of the specified fetch descriptor.”fetchIdentifiersis declaredfunc fetchIdentifiers<T>(_ descriptor: FetchDescriptor<T>) throws -> [PersistentIdentifier] where T : PersistentModeland returns “an array of persistent identifiers, where each identifier represents a single model that satisfies the criteria”; afetchIdentifiers(_:batchSize:)overload returns the identifiers in batches as aFetchResultsCollection<PersistentIdentifier>. The fetch-affected-identifiers-then-compare-against-the-view pattern source: Paraphrased from a locally transcribed recording of the WWDC 2026 SwiftData Group Lab; Apple publishes no official captions for the labs. ↩↩↩