Post

Explicit Core Data Fetch Requests in SwiftUI Previews

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

直接檢查兩個地方:

  1. Preview 假資料是否使用 ManagedObject(context:) 自動反查 entity。
  2. 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。

This post is licensed under CC BY 4.0 by the author.