在 SwiftData 中,ModelContainer 是用来管理数据模型(@Model 标注的类)的容器。它类似于 Core Data 的 NSPersistentContainer,负责存储、查询、修改和删除模型对象。而 上下文绑定 是指确保视图中的模型实例由正确的 ModelContext 管理,且与特定的 ModelContainer 关联。
在SwiftData视图的预览视图中,如果绑定了一个ModelContainer,但没有从ModelContainer的上下文绑定中查询模型对象,就可能导致无法匹配的错误。这是因为,SwiftData 期望所有的 @Model 实例都由有效的 ModelContext 管理。
预览报错示例
问题描述
例如,在SwiftData文件中,创建了两个静态变量,分别用于管理预览实例和预览的ModelContainer。
import SwiftUI
import SwiftData
import MapKit
@Model
class Place {
...
@MainActor
static var preview: ModelContainer {
let container = try! ModelContainer(for: Place.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
for place in previewPlaces {
container.mainContext.insert(place)
}
return container
}
static var previewPlaces: [Place] {
[
Place(name: "Bellagio", latitude: 36.1129, longitude: -115.1765, interested: true),
Place(name: "Paris", latitude: 36.1125, longitude: -115.1707, interested: true)
...
]
}
}
从上述代码中,可以看到previewPlaces是一个静态属性的数组,存放了多个Place实例。Preview是一个ModelContainer,配置为仅在在内存中使用。然后遍历previewPlaces数组中的实例,插入到该ModelContainer的上下文中,最后返回一个ModelContainer。
问题报错
在View视图的预览视图中,如果想要通过Place.previewPlace数组传递一个测试实例,就会造成报错:
#Preview {
MapView(place: Place.previewPlaces[0])
.modelContainer(Place.preview)
}
报错代码:
PREVIEW UPDATE ERROR:
CrashReportError: Fatal Error in ModelContainer.swift
hackingwithswift crashed due to fatalError in ModelContainer.swift at line 144.
failed to find a currently active container for Place
Process: hackingwithswift[71542]
Date/Time: 2024-12-27 04:54:52 +0000
Log File: <none>
Application Specific Information:
dyld [
...
在这个报错当中,最重要的一点就是没有提供具体的报错原因。
前面提到在SwiftData中创建了两个静态变量用于预览视图,这里的MapView视图需要传递进去一个Place实例,然后根据Place实例的相关属性确定Map位置,在视图中显示对应的地图视图。
struct MapView: View {
var place: Place? = nil
@State var position: MapCameraPosition? = nil
init(place: Place? = nil, position: MapCameraPosition? = nil) {
self.place = place
self._position = State(initialValue: .camera(MapCamera(
centerCoordinate: place?.location ?? CLLocationCoordinate2D(latitude: 10, longitude: 10),
distance: 1000,
heading: 250,
pitch: 80
)))
}
var body: some View {
Map(initialPosition: position ?? .camera(MapCamera(
centerCoordinate: CLLocationCoordinate2D(latitude: 30, longitude: 100),
distance: 1000,
heading: 250,
pitch: 80
)))
}
}
在MapView视图中,当给place变量传递一个静态数组previewPlace实例时,Xcode就会报错,即使绑定ModelContainer也无济于事。
问题分析
在预览中的这个问题,实际也是很多SwiftData预览报错的原因。关键的问题在于,ModelContainer为视图提供了上下文对象。因此当向预览视图中传递Place实例时:
MapView(place: Place.previewPlaces[0])
这个Place.previewPlaces[0]实例不是从ModelContainer上下文中获取的,导致这个对象并没有有ModelContainer绑定,因此不能直接使用Place.previewPlaces的静态数据。
解决方案
因此,需要在#Preview视图中从ModelContainers上下文中获取数据,然后将获取的数据传递到视图当中。
#Preview {
let container = Place.preview
let context = container.mainContext
let places = try! context.fetch(FetchDescriptor<Place>()) // 从上下文中获取数据
return MapView(place: places[0])
.modelContainer(container)
}
实现效果
最终,通过从Place.preview容器上下文中获取数据,并在预览视图中传递该数据,实现预览地图的效果。
ModelContainer的上下文绑定
什么是 ModelContainer 的上下文绑定?
在 SwiftData 中,ModelContainer 是用来管理数据模型(@Model 标注的类)的容器。它类似于 Core Data 的 NSPersistentContainer,负责存储、查询、修改和删除模型对象。而 上下文绑定 是指确保视图中的模型实例由正确的 ModelContext 管理,且与特定的 ModelContainer 关联。
为什么上下文绑定很重要?
在 SwiftData 中直接创建一个模型实例(如 Place(name: “Example”, …)),这个实例是孤立的,没有与任何 ModelContext 或 ModelContainer 绑定。SwiftData 无法管理它,也无法将其持久化或查询到。
上下文绑定通过将 ModelContainer 提供给视图环境,解决以下问题:
模型管理:确保所有模型实例都由 ModelContext 管理。
数据一致性:保证视图使用的模型是从同一个容器上下文中获得的。
性能优化:提供统一的存储管理,而不是孤立的数据实例。
上下文绑定的实现方式
上下文绑定通常通过 .modelContainer(_:) 修饰符将 ModelContainer 绑定到视图环境中。这种绑定会自动为视图及其子视图提供上下文。
代码示例:
#Preview {
let container = Place.preview // 创建 ModelContainer(例如,内存存储)
let context = container.mainContext // 获取上下文
// 使用上下文管理的数据
let places = try! context.fetch(FetchDescriptor<Place>())
return MapView(place: places[0]) // 确保传递的是上下文管理的对象
.modelContainer(container) // 将 ModelContainer 绑定到视图环境
}
其中context.fetch(FetchDescriptor<Place>())是一种获取Place实体的用法。
上下文绑定的问题排查
1、报错:failed to find a currently active container
模型实例没有从 ModelContext 提供。
使用了直接创建的孤立模型实例(例如,Place(name: …))。
2、解决方法:
确保所有 @Model 实例通过 context.insert() 或 context.fetch() 管理。
确保视图绑定了 ModelContainer,例如:
.modelContainer(Place.preview)
为什么会触发上下文绑定报错?
1、@Model 的特性
当一个类被标记为 @Model,它的实例被设计为由 ModelContext 管理。SwiftData 默认假设所有 @Model 实例都与某个 ModelContainer 和 ModelContext 相关联。
即使直接传递一个 Place.previewPlaces[0],因为它是一个 @Model 实例,SwiftData 仍会尝试追踪其管理上下文。
2、Place.previewPlaces[0] 的问题
Place.previewPlaces[0] 是一个孤立的 Place 实例,它没有被插入到任何 ModelContext 中。
SwiftData 会认为这种孤立的实例是非法的,尤其是在视图绑定了 .modelContainer(_:) 时,SwiftData 会尝试验证实例的上下文关系。
3、.modelContainer(_:) 的作用
为预览绑定了 .modelContainer(Place.preview),SwiftData 会要求所有的 @Model 实例必须在该容器的上下文中。
但是,Place.previewPlaces[0] 并未被插入到 Place.preview 的 ModelContext 中,因此触发了报错。
为什么需要上下文即使视图没有直接使用 SwiftData?
1、SwiftData 的一致性检查:
一旦视图绑定了 .modelContainer(_:),SwiftData 会确保所有涉及的 @Model 实例都受其上下文管理。
即使代码中没有直接调用 SwiftData 的查询或操作,只要视图内存在 @Model 实例,SwiftData 就会尝试验证它的合法性。
2、@Model 的行为:
@Model 不仅是一个数据类型标记,它还隐含了对 SwiftData 管理的依赖。
在传递 @Model 实例时,SwiftData 会自动尝试为它找到一个有效的上下文。
3、视图预览机制:
预览运行时会加载整个 SwiftUI 环境,包括绑定的 ModelContainer。
如果 @Model 实例与预览绑定的上下文不匹配,SwiftData 的验证机制就会报错。
总结
报错发生是因为 SwiftData 的上下文验证机制,Place.previewPlaces[0] 是孤立的 @Model 实例,而视图绑定了一个特定的 ModelContainer,触发了不匹配的错误。
如果只是为了预览,可以:
确保实例被插入到绑定的上下文中。
或者使用独立的内存存储 ModelContainer 来管理预览数据。
或在不需要 SwiftData 时,移除 @Model 特性。
上下文绑定确保 SwiftData 中的数据流动统一且受控。如果没有绑定或绑定错误,@Model 实例会变得孤立,导致崩溃或数据更新失效。通过使用 ModelContainer 和 ModelContext,可以将数据管理集成到 SwiftUI 环境中,实现一致性和可预测性的数据操作。
相关文章
1、iOS 18, SwiftUI 6, & Swift 6: 从零开始构建iOS应用程序, 涵盖visionOS, macOS, watchOS:https://www.bilibili.com/video/BV1b6421f7Px?spm_id_from=333.788.videopod.episodes&vd_source=f21219cb93118beac6a36b0ef961df6a&p=9
2、SwiftData预览报错问题:https://fangjunyu.com/2024/11/04/swiftdata-%e9%a2%84%e8%a7%88%e6%8a%a5%e9%94%99%e9%97%ae%e9%a2%98/
3、SwiftData核心组件ModelContainer:https://fangjunyu.com/2024/11/05/swiftdata%e6%a0%b8%e5%bf%83%e7%bb%84%e4%bb%b6modelcontainer/
4、SwiftData框架属性包装器:@ModelContext:https://fangjunyu.com/2024/11/05/swiftdata%e6%a1%86%e6%9e%b6%e5%b1%9e%e6%80%a7%e5%8c%85%e8%a3%85%e5%99%a8modelcontext/
5、SwiftData使用静态数据预览视图:https://fangjunyu.com/2024/12/26/swiftdata%e4%bd%bf%e7%94%a8%e9%9d%99%e6%80%81%e6%95%b0%e6%8d%ae%e9%a2%84%e8%a7%88%e8%a7%86%e5%9b%be/
完整代码
MapView视图
import SwiftUI
import MapKit
import SwiftData
struct MapView: View {
var place: Place? = nil
@State var position: MapCameraPosition? = nil
init(place: Place? = nil, position: MapCameraPosition? = nil) {
self.place = place
self._position = State(initialValue: .camera(MapCamera(
centerCoordinate: place?.location ?? CLLocationCoordinate2D(latitude: 10, longitude: 10),
distance: 1000,
heading: 250,
pitch: 80
)))
}
var body: some View {
Map(initialPosition: position ?? .camera(MapCamera(
centerCoordinate: CLLocationCoordinate2D(latitude: 30, longitude: 100),
distance: 1000,
heading: 250,
pitch: 80
)))
}
}
#Preview {
let container = Place.preview
let context = container.mainContext
let places = try! context.fetch(FetchDescriptor<Place>()) // 从上下文中获取数据
return MapView(place: places[0])
.modelContainer(container)
}
Place文件
import SwiftUI
import SwiftData
import MapKit
@Model
class Place {
@Attribute(.unique) var name: String
var latitude: Double
var longitude: Double
var interested: Bool
var location: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
var image: Image {
Image(name.lowercased().replacingOccurrences(of: " ", with: ""))
}
init(name: String, latitude: Double, longitude: Double, interested: Bool) {
self.name = name
self.latitude = latitude
self.longitude = longitude
self.interested = interested
}
@MainActor
static var preview: ModelContainer {
let container = try! ModelContainer(for: Place.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
for place in previewPlaces {
container.mainContext.insert(place)
}
return container
}
static var previewPlaces: [Place] {
[
Place(name: "Bellagio", latitude: 36.1129, longitude: -115.1765, interested: true),
Place(name: "Paris", latitude: 36.1125, longitude: -115.1707, interested: true),
Place(name: "Treasure Island", latitude: 36.1247, longitude: -115.1721, interested: false),
Place(name: "Stratosphere", latitude: 36.1475, longitude: -115.1566, interested: true),
Place(name: "Luxor", latitude: 36.0955, longitude: -115.1761, interested: false),
Place(name: "Excalibur", latitude: 36.0988, longitude: -115.1754, interested: false)
]
}
}