SwiftUI Preview and Archive Must Compile Together
SwiftUI Preview 與 Archive 使用不同的建置情境,但兩者仍會編譯同一批來源檔。 如果 Preview 專用 helper 只存在於 Debug,而 #Preview 在 Release 仍引用它, 就會發生「Preview 修好了,Archive 卻失敗」;反過來,若直接把整個 Preview 排除,又可能掩蓋 Preview 原本的 Core Data 問題。
正確目標不是讓其中一邊暫時通過,而是讓 Preview 的宣告與相依程式碼在所有 組態都能通過編譯,只把真正不應進入正式版本的假資料 seed 隔離。
問題寫法:Preview helper 只存在於 Debug
假設 Preview trait 在所有組態都會被編譯:
1
2
3
4
5
6
7
8
struct SampleDataPreviewModifier: PreviewModifier {
static func makeSharedContext() async throws -> Context {
let container = await MainActor.run {
PersistenceController.makePreviewContainer()
}
return Context(container: container)
}
}
但是 container helper 被整個包在 #if DEBUG:
1
2
3
4
5
#if DEBUG
static func makePreviewContainer() -> NSPersistentContainer {
// 建立 Preview container
}
#endif
Debug Preview 可以找到方法,Archive 使用 Release 時卻看不到它,最後出現:
1
Type 'PersistenceController' has no member 'makePreviewContainer'
錯誤在 Xcode Issue Navigator 有時只短暫出現,點擊後又消失,但 Archive 程序已經失敗。這不是簽章或 Organizer 問題,而是 Debug 與 Release 的 符號可見範圍不一致。
不完整的補法:把所有 Preview 包進 #if DEBUG
最直接的 workaround 是:
1
2
3
4
5
#if DEBUG
#Preview(traits: .sampleData) {
ChartSummaryView()
}
#endif
這通常可以解除 Archive 的編譯錯誤,但它不是 Apple #Preview 與 PreviewModifier 範例採用的結構。更重要的是,它只讓 Release 看不到問題, 沒有處理 Preview 本身的資料環境是否正確。
如果 Preview 原本還有 Core Data fetch request 缺少 entity,這個補法不會 修正它,只會把兩條建置路徑切得更開。
正確寫法:基礎設施皆可編譯,只有 seed 限定 Debug
Apple 建議使用 PreviewModifier 建立共享 context,再透過 trait 注入 Preview。 Preview container helper 應在 Debug 與 Release 都保持可編譯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static func makePreviewContainer() -> NSPersistentContainer {
let modelURL = Bundle(for: FoodItem.self)
.url(forResource: "MeowCare", withExtension: "momd")!
let model = NSManagedObjectModel(contentsOf: modelURL)!
let container = NSPersistentContainer(
name: "MeowCare",
managedObjectModel: model
)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
description.configuration = "Private"
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
precondition(error == nil, "Preview store 載入失敗:\(error!)")
}
return container
}
PreviewModifier 本身也保持所有組態可編譯,但假資料只在 Debug 建立:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct SampleDataPreviewModifier: PreviewModifier {
struct Context: @unchecked Sendable {
let container: NSPersistentContainer
}
static func makeSharedContext() async throws -> Context {
let container = await MainActor.run {
PersistenceController.makePreviewContainer()
}
#if DEBUG
await MainActor.run {
CareEventSeeder.seedForPreview(
into: container.viewContext
)
}
#endif
return Context(container: container)
}
func body(content: Content, context: Context) -> some View {
content.environment(
\.managedObjectContext,
context.container.viewContext
)
}
}
Preview 則維持 Apple 範例的正常形式:
1
2
3
#Preview(traits: .sampleData) {
ChartSummaryView()
}
這樣 Release 能解析 Preview trait 與 helper,Debug Preview 則額外取得假資料。 兩條路徑共用同一套宣告,不會因為 #if DEBUG 把必要方法切掉。
Core Data Preview 必須明確指定 entity
即使 Preview container 正確,Xcode Preview 的 JIT/動態替換仍可能無法從 FetchedResults<CareEvent> 推導 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 執行環境通常正常,但 Preview 可能崩潰:
1
2
executeFetchRequest:error:
A fetch request must have an entity.
改用 Apple 支援的完整 request initializer,明確指定 entity name:
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>
這不是只針對某個 Preview 的特例。只要同一個 Core Data model 的 View 仍使用 簡寫,就可能成為下一個崩潰點。因此遇到 entity 相關 Preview crash 時,應搜尋 整個專案的 @FetchRequest,一次統一處理,而不是只補目前出錯的畫面。
驗證順序
修改 Preview/Core Data 基礎設施後,至少依序確認:
- Debug 模擬器建置成功。
- 所有受影響的 Preview Canvas 能建立並顯示。
- Release 使用真正的
archiveaction 成功。 - 既有單元測試通過。
git diff --check沒有格式問題。
一般 Release Build 成功不等於 Archive 已驗證;同樣地,Preview 編譯成功也 不代表 Canvas 執行時不會因 fetch request 或 managed object entity 問題崩潰。
完成條件必須同時包含 Preview runtime 與 Archive。
看到錯誤時怎麼判斷
如果 Archive 出現:
1
Type 'PersistenceController' has no member 'makePreviewContainer'
先檢查 Preview 呼叫端與 helper 是否位於不同的條件編譯範圍。
如果 Preview 出現:
1
must have a valid NSEntityDescription
或:
1
A fetch request must have an entity
則檢查:
- Preview container 是否載入正確的
.momd。 - seeder 是否從同一個 context 的 model 取得 entity。
- 所有
@FetchRequest是否明確使用NSFetchRequest(entityName:)。
不要先把問題歸因於 DerivedData、Clean Build 或使用者操作。這些錯誤訊息已經 指出編譯條件或 Core Data entity 配置不完整,應直接檢查程式碼。