Explicit Core Data Fetch Requests in SwiftUI Previews
原本作法
SwiftUI 的 @FetchRequest 可以只提供排序條件,讓 generic result type 自動推導 Core Data entity:
1
2
3
4
5
6
7
8
9
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(
keyPath: \CareEvent.occurredAt,
ascending: true
)
]
)
private var events: FetchedResults<CareEvent>
一般執行 app 時這種簡寫很方便。不過 Xcode Preview 使用 JIT 與動態替換映像時,執行中的 CareEvent class 可能無法正確對應到載入模型裡的 entity。
同樣的問題也會出現在:
1
let event = CareEvent(context: context)
Core Data 若無法透過 class 反查 entity,建立物件時會直接 crash:
1
2
An NSManagedObject of class 'CareEvent'
must have a valid NSEntityDescription.
第一個修改與衍生問題
Preview seeder 先改成從 preview context 明確取得 entity:
1
2
3
4
5
6
7
8
9
10
11
12
guard let entity = NSEntityDescription.entity(
forEntityName: "CareEvent",
in: context
) else {
assertionFailure("Missing CareEvent entity")
return
}
let event = CareEvent(
entity: entity,
insertInto: context
)
這解決了假資料建立時的 crash,因為物件與 preview store 確定使用同一份 NSManagedObjectModel。
但畫面開始更新後,SwiftUI 的 @FetchRequest 仍使用原本的自動推導。於是 crash 往後移到 fetch 階段:
1
2
executeFetchRequest:error:
A fetch request must have an entity.
也就是說,只修正 insert 不夠。Preview 內所有依賴 class 自動推導 entity 的入口都可能遇到相同問題。
Apple 官方建議
Apple 的 NSFetchRequest.entity 文件明確說明:fetch request 必須設定 entity,否則執行時會失敗。
需要完整控制 request 時,SwiftUI 官方提供 @FetchRequest(fetchRequest:),搭配 NSFetchRequest(entityName:) 明確指定 entity:
1
2
3
4
5
6
7
8
9
10
11
12
13
@FetchRequest(fetchRequest: {
let request = NSFetchRequest<CareEvent>(
entityName: "CareEvent"
)
request.sortDescriptors = [
NSSortDescriptor(
keyPath: \CareEvent.occurredAt,
ascending: true
)
]
return request
}())
private var events: FetchedResults<CareEvent>
這個 request 不再依賴 SwiftUI 從 FetchedResults<CareEvent> 自動推導 entity。Core Data 會在執行時用 context 所屬的 model 尋找 CareEvent。
日後看到就怎麼做
只要 Xcode Preview 出現以下任一訊息:
1
must have a valid NSEntityDescription
1
A fetch request must have an entity
直接檢查兩個地方:
- Preview 假資料是否使用
ManagedObject(context:)自動反查 entity。 - View 是否使用只傳 sort descriptors 的
@FetchRequest簡寫。
Preview seeder 改用 NSEntityDescription.entity(forEntityName:in:) 建立物件;SwiftUI fetch 改用 NSFetchRequest(entityName:) 加上 @FetchRequest(fetchRequest:)。
重點不是清除 DerivedData,也不是重做 Clean Build。這兩種 exception 都表示 request 或 managed object 沒有取得明確、有效而且屬於同一份 model 的 entity。