使用 SwiftUI PreviewModifier 設定 preview 共用的資料和 modifier — iOS 18 新功能
開發 SwiftUI App 時,我們常常需要在很多畫面的 preview 設定共用的資料和 modifier,比方 Observable 的物件,SwiftData 的 Model Container,偏好的顏色模式等。
從 iOS 18 開始有簡化程式的方法,我們可以用 PreviewModifier 將 preview 共用的資料和 modifier 集中在一個地方,不用再重覆撰寫。
以下我們分別以 Observable 的物件和 SwiftData 的 Model Container 舉例說明。
設定 preview 共用的 Observable 物件
以電影資料為例,以下我們用 MovieStore & Movie 定義電影資料。
MovieStore.swift
import Foundation
@Observable
class MovieStore {
var movies = [
Movie(title: "哥吉拉-1.0", year: 2023, rating: 4.5),
]
}
Movie.swift
import Foundation
struct Movie: Identifiable {
let id = UUID()
let title: String
let year: Int
let rating: Double
}
當我們想在畫面存取 MovieStore & 顯示 dark mode 模式時,preview 裡要記得加上 .environment(MovieStore())
和 .preferredColorScheme(.dark)
。
import SwiftUI
struct MovieListView: View {
@Environment(MovieStore.self) private var movieStore
var body: some View {
List {
ForEach(movieStore.movies) { movie in
Text(movie.title)
}
}
}
}
#Preview {
MovieListView()
.environment(MovieStore())
.preferredColorScheme(.dark)
}
接下來讓我們用 iOS 18 的 PreviewModifier 簡化 preview 的程式。
import SwiftUI
struct SampleMovieStoreEnvironment: PreviewModifier {
static func makeSharedContext() async throws -> MovieStore {
MovieStore()
}
func body(content: Content, context: MovieStore) -> some View {
content
.environment(context)
.preferredColorScheme(.dark)
}
}
說明。
- 定義遵從 PreviewModifier 的型別 SampleMovieStoreEnvironment。
- function makeSharedContext 回傳 preview 需要的資料,在此我們回傳 MovieStore。
- 在 function body 設定 preview 需要的資料跟 modifier,body 的參數 content 是 preview 顯示的畫面,context 是 makeSharedContext 回傳的 MovieStore。
之後定義 preview 畫面時,只要在參數 traits 傳入 .modifier(SampleMovieStoreEnvironment())
,即可讓 preview 畫面存取 MovieStore 和套用 dark mode。
#Preview(traits: .modifier(SampleMovieStoreEnvironment())) {
MovieListView()
}
我們還可進一步簡化,透過定義 PreviewTrait 的 extension,宣告 type property sampleMovieStoreEnvironment 是 .modifier(SampleMovieStoreEnvironment())。
import SwiftUI
struct SampleMovieStoreEnvironment: PreviewModifier {
static func makeSharedContext() async throws -> MovieStore {
MovieStore()
}
func body(content: Content, context: MovieStore) -> some View {
content
.environment(context)
.preferredColorScheme(.dark)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var sampleMovieStoreEnvironment: Self = .modifier(SampleMovieStoreEnvironment())
}
之後定義 preview 畫面時,traits 參數將更簡短,輸入 .sampleMovieStoreEnvironment 即可。
#Preview(traits: .sampleMovieStoreEnvironment) {
MovieListView()
}
設定 preview 共用的 SwiftData Model Container
以歌曲資料為例,以下我們用 Song & SampleData 定義歌曲資料。
Song.swift
import Foundation
import SwiftData
@Model
class Song {
var title: String
var singer: String
init(title: String, singer: String) {
self.title = title
self.singer = singer
}
static let sampleData = [
Song(title: "多虧你啊", singer: "戴佩妮")
]
}
SampleData.swift
import Foundation
import SwiftData
@MainActor
class SampleData {
static let shared = SampleData()
let modelContainer: ModelContainer
var context: ModelContext {
modelContainer.mainContext
}
private init() {
let schema = Schema([
Song.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
do {
modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
insertSampleData()
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
func insertSampleData() {
for song in Song.sampleData {
context.insert(song)
}
}
var song: Song {
Song.sampleData[0]
}
}
當我們想在畫面存取 songs & 顯示 dark mode 模式時,preview 裡要記得加上 modelContainer(SampleData.shared.modelContainer)
和 .preferredColorScheme(.dark)
。
import SwiftUI
import SwiftData
struct SongListView: View {
@Query private var songs: [Song]
var body: some View {
List {
ForEach(songs) { song in
Text(song.title)
}
}
}
}
#Preview {
SongListView()
.modelContainer(SampleData.shared.modelContainer)
.preferredColorScheme(.dark)
}
同樣的,定義遵從 PreviewModifier 的型別可以簡化 preview 的程式,這次 makeSharedContext 改成產生需要的 Song 和回傳 SwiftData 需要的 ModelContainer。
import SwiftUI
import SwiftData
struct SampleSongEnvironment: PreviewModifier {
static func makeSharedContext() async throws -> ModelContainer {
let schema = Schema([
Song.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
let modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])
for song in Song.sampleData {
modelContainer.mainContext.insert(song)
}
return modelContainer
}
func body(content: Content, context: ModelContainer) -> some View {
content
.modelContainer(context)
.preferredColorScheme(.dark)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var sampleSongEnvironment: Self = .modifier(SampleSongEnvironment())
}
preview 程式成功地簡化,現在只需在參數 traits 傳入 .sampleSongEnvironment 即可存取 SwiftData 需要的 model container。
#Preview(traits: .sampleSongEnvironment) {
SongListView()
}
參考連結
剛剛例子的寫法主要參考 Apple 的官方範例,有興趣的朋友也可進一步研究以下程式。
- BOT-anist。
import SwiftUI
struct SampleAppStateEnvironment: PreviewModifier {
static func makeSharedContext() async throws -> AppState {
AppState()
}
func body(content: Content, context: AppState) -> some View {
content
.environment(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var sampleAppState: Self = .modifier(SampleAppStateEnvironment())
}
#Preview(traits: .sampleAppState) {
ContentView()
}
- Destination Video。
import Foundation
import SwiftData
import SwiftUI
/// A modifier that creates the a model container for the preview data.
struct PreviewData: PreviewModifier {
static func makeSharedContext() throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: Video.self, Genre.self, Person.self, UpNextItem.self,
configurations: config
)
try Importer.importVideoMetadata(into: container.mainContext, isPreview: true)
return container
}
func body(content: Content, context: ModelContainer) -> some View {
content.modelContainer(context)
.environment(PlayerModel(modelContainer: context))
#if os(visionOS)
.environment(ImmersiveEnvironment())
#endif
#if os(iOS) || os(macOS)
.preferredColorScheme(.dark)
#endif
}
}
extension PreviewTrait where T == Preview.ViewTraits {
@MainActor static var previewData: Self = .modifier(PreviewData())
}
#Preview(traits: .previewData) {
ContentView()
}
- Previewing your app’s interface in Xcode。