Replace ModelContainerPreview with PreviewModifier
用 PreviewModifier 取代 ModelContainerPreview
上一篇(搞懂 SwiftData Preview 為什麼需要 ModelContainerPreview)解釋了 Apple 範例裡 ModelContainerPreview 在做什麼。
這篇講的是:如果你的專案支援 iOS 18+,可以用 Apple 官方的 PreviewModifier protocol 取代它,刪掉一個檔案。
來源
Apple 在 WWDC24 的 What’s new in SwiftData(8:15 開始)直接示範了這個做法:
To use preview traits, you can create a new struct that conforms to
PreviewModifierwhich has two functions: one for setting up a shared context for the preview, and another to apply the shared context to a view. For SwiftData previews, you can vend aModelContaineras the shared context, creating aModelConfigurationthat stores data in memory only.
官方 API 文件:PreviewModifier – Apple Developer Documentation
另外 Donny Wals 也有寫專文介紹。
PreviewModifier 是什麼?
它是 SwiftUI framework 內建的 protocol(iOS 18+ / macOS 15+),讓你定義「Preview 的共享環境」。你只要寫一個 struct,實作兩個 method:
makeSharedContext()— 準備什麼東西(建 container、塞假資料)body(content:context:)— 怎麼把那個東西注入到 View 裡
然後在 #Preview 用 traits: 參數一行搞定。
它取代了什麼?
PreviewModifier 讓你可以刪掉 ModelContainerPreview.swift 這個手刻的 wrapper View。
ModelContainerPreview 手動處理的三件事,PreviewModifier 全部幫你做了:
ModelContainerPreview 手動處理的 | PreviewModifier 怎麼處理 |
|---|---|
MainActor.assumeIsolated 確保在 main thread | makeSharedContext() 是 async,framework 自動排程 |
try / fatalError 錯誤處理 | makeSharedContext() 是 throws,framework 自動 catch |
.modelContainer(container) 注入 | body(content:context:) 裡寫一次就好 |
ModelContainerPreview.swift 做的事 framework 都幫你做了,所以可以刪掉。
完整對照:舊 → 新
舊的做法(兩個檔案)
1
2
3
PreviewHelper/
├── ModelContainerPreview.swift ← 刪掉
└── Preview+ModelContainer.swift ← 要改寫
ModelContainerPreview.swift(整個檔案要刪掉):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 手刻一個 wrapper View,處理 MainActor、try/catch、注入 container
struct ModelContainerPreview<Content: View>: View {
var content: () -> Content
@State var container: ModelContainer
init(_ modelContainer: @escaping () throws -> ModelContainer,
@ViewBuilder content: @escaping () -> Content) {
self.content = content
do {
self.container = try MainActor.assumeIsolated(modelContainer)
} catch {
fatalError("Failed to create the model container: \(error.localizedDescription)")
}
}
var body: some View {
content()
.modelContainer(container)
}
}
Preview+ModelContainer.swift(舊的):
1
2
3
4
5
6
7
8
9
10
11
// 提供一個 closure 給 ModelContainerPreview 呼叫
extension ModelContainer {
@MainActor static let sample: () throws -> ModelContainer = {
let schema = Schema([FoodLog.self, CareEvent.self])
let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: [configuration])
FoodLog.insertSampleData(modelContext: container.mainContext)
CareEventSeeder.seedForPreview(into: container.mainContext)
return container
}
}
舊的 Preview 寫法:
1
2
3
4
5
#Preview {
ModelContainerPreview(ModelContainer.sample) {
DailyTimelineView()
}
}
新的做法(一個檔案)
1
2
3
PreviewHelper/
├── ModelContainerPreview.swift ← 刪掉了
└── Preview+ModelContainer.swift ← 改寫成 PreviewModifier
Preview+ModelContainer.swift(新的):
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
29
30
31
import SwiftData
import SwiftUI
/// Preview 用的 SwiftData 環境:in-memory container + 假資料。
///
/// 遵循 PreviewModifier protocol(iOS 18+),取代舊的 ModelContainerPreview wrapper。
/// - `makeSharedContext()` 負責建 container 和塞假資料
/// - `body(content:context:)` 負責把 container 注入到 View 的 environment
///
/// 來源:[What's new in SwiftData – WWDC24](https://developer.apple.com/videos/play/wwdc2024/10137/?time=495)
struct SampleDataPreviewModifier: PreviewModifier {
static func makeSharedContext() async throws -> ModelContainer {
let schema = Schema([FoodLog.self, CareEvent.self])
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: [config])
#if DEBUG
FoodLog.insertSampleData(modelContext: container.mainContext)
CareEventSeeder.seedForPreview(into: container.mainContext)
#endif
return container
}
func body(content: Content, context: ModelContainer) -> some View {
content.modelContainer(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
/// 在 #Preview 裡用 `traits: .sampleData` 即可注入 SwiftData 環境。
@MainActor static var sampleData: Self = .modifier(SampleDataPreviewModifier())
}
新的 Preview 寫法:
1
2
3
#Preview(traits: .sampleData) {
DailyTimelineView()
}
遷移步驟
如果你的專案 deployment target 是 iOS 18+(MeowCare 是 18.1),遷移很簡單:
- 改寫
Preview+ModelContainer.swift— 把ModelContainer.sampleextension 換成上面的SampleDataPreviewModifier+PreviewTraitextension 全專案搜尋
ModelContainerPreview(— 把每個#Preview從舊語法改成新語法:1 2 3 4 5 6 7 8 9 10 11
// 舊 #Preview { ModelContainerPreview(ModelContainer.sample) { SomeView() } } // 新 #Preview(traits: .sampleData) { SomeView() }
- 搜尋
ModelContainer.sample)— 有些 Preview 可能直接用.modelContainer(try! ModelContainer.sample()),也一起改掉 - 刪掉
ModelContainerPreview.swift— 確認沒有任何地方引用後刪除
注意事項
makeSharedContext 是共享的
PreviewModifier 的 makeSharedContext() 回傳的 context 會被 Xcode 快取,多個 Preview 可能共用同一個 container。
這跟舊的 ModelContainerPreview 不同——舊的每次都呼叫 closure 建新 container。
實際上大部分情況這不是問題,因為 Preview 通常只讀資料。但如果你的 Preview 會測試刪除或修改,要注意 Preview 之間可能互相影響。
只適用 iOS 18+ / macOS 15+
PreviewModifier 是 iOS 18 新增的 API。如果你的專案要支援 iOS 17,就不能用,繼續用 ModelContainerPreview。
不影響不依賴 SwiftData 的 View
如果 View 不使用 @Query 也不使用 @Environment(\.modelContext),就不需要 traits: .sampleData,直接寫 #Preview { ... } 即可。