问题描述
在学习SwiftData中,预览存在无法显示的情况,但是模拟器是正常的。
预览代码为:
#Preview {
ProfileView()
}
报错的内容为:
CrashReportError: hackingwithswift crashed due to an uncaught exception
hackingwithswift crashed due to an uncaught exception `NSInvalidArgumentException`. Reason: Cannot insert 'Book' in this managed object context because it is not found in the associated managed object model..
经排查发现问题跟数据持久化和状态管理有关。
需要在SwiftUI的预览中,提供一个有效的ModelContainer,并插入示例数据,以便在预览中可以访问到数据。
#Preview {
// 创建一个 ModelContainer,并插入一些示例数据
let container = try! ModelContainer(for: Book.self)
let exampleBook = Book(title: "示例书籍", isbn: "1234567890", publicationYear: 2024, author: "作者名")
// 插入示例数据
let context = container.mainContext
context.insert(exampleBook)
try? context.save() // 注意错误处理,使用 try? 来避免崩溃
return ProfileView()
.modelContainer(container)
}
预览代码
在部分视图中,可以在预览代码中使用modelContainer(for:),以一种更简洁的方式绑定模型容器来进行预览。
// 模型容器绑定单个类型
ContentView()
.modelContainer(for: Friendface.self)
// 模型容器绑定多个类型
ContentView()
.modelContainer(for: [Friendface.self,Friend.self])
同类问题
modelContainer(for:),这个简洁的方法并不总是奏效。
这一次的问题为,一个展示内容视图预览报错:
import SwiftUI
import SwiftData
struct FriendInfo: View {
@Environment(\.modelContext) var modelContext
var friendInfo: Friendface
var body: some View {
Form {
Section("name") {
Text(friendInfo.name)
}
...
}
}
}
#Preview {
FriendInfo(friendInfo: Friendface(
id: "123",
isActive: true,
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Swift Street",
about: "A friendly person.",
registered: Date(),
tags: ["developer", "iOS"],
friends: []
))
.modelContainer(container)
}
内容展示视图FriendInfo在模拟器或者真机的状态下,都是正常访问且无报错的,但是Xcode报错。
在Xcode中,内容列表视图ContentView可以通过预览视图访问到这个内容展示视图FriendInfo。
但是FriendInfo视图下预览就报错,因此推测问题是在预览代码中:
#Preview {
FriendInfo(friendInfo: Friendface(
id: "123",
isActive: true,
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Swift Street",
about: "A friendly person.",
registered: Date(),
tags: ["developer", "iOS"],
friends: []
))
.modelContainer(for: Friendface.self)
}
但是预览代码也是正常的生成一个Friendface实例并加载了modelContainer(for:)。
在ContentView视图中,modelContainer(for:)是可以正常预览的。
也没有从Xcode报错中找到问题的原因,只知道可能跟Friendface.init 的初始化有关。但没有定位到具体的报错行。
最后,还是通过上次的解决方案解决的这一问题:
Preview {
// 创建一个 ModelContainer,并插入一些示例数据
let container = try! ModelContainer(for: Friendface.self)
let exampleBook = Friendface(
id: "123",
isActive: true,
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Swift Street",
about: "A friendly person.",
registered: Date(),
tags: ["developer", "iOS"],
friends: []
)
// 插入示例数据
let context = container.mainContext
context.insert(exampleBook)
try? context.save() // 注意错误处理,使用 try? 来避免崩溃
return FriendInfo(friendInfo: Friendface(
id: "123",
isActive: true,
name: "John Doe",
age: 30,
email: "john.doe@example.com",
address: "123 Swift Street",
about: "A friendly person.",
registered: Date(),
tags: ["developer", "iOS"],
friends: []
))
.modelContainer(container)
}
使用 ModelContainer 和 modelContainer(_:) 解决了预览报错,显式地为预览提供了 SwiftData 的上下文环境和模型容器。
解决步骤
1、ModelContainer 的显式初始化
通过 ModelContainer(for:) 创建了一个与 Friendface 关联的模型容器。这样,预览可以初始化一个完整的 SwiftData 环境,而不再缺少必需的上下文。
let container = try! ModelContainer(for: Friendface.self)
SwiftData 的 @Model 类型需要在一个 ModelContainer 中管理。如果没有提供容器,SwiftData 无法在预览时管理模型对象,这正是之前的错误原因。
2、向容器插入数据
显式地将数据插入到了容器的上下文中:
let context = container.mainContext
context.insert(exampleBook)
try? context.save()
这一步模拟了 SwiftData 数据存储的行为,使得容器中有实际的数据供视图使用,而不会因为缺少数据导致运行时错误。
3、绑定容器到视图的预览环境
最后,通过 modelContainer(_:) 将模型容器绑定到视图,使视图能够访问和使用这个容器的上下文:
.modelContainer(container)
这一绑定建立了视图与 SwiftData 的连接,从而解决了之前预览环境中缺少上下文的报错问题。
为什么之前的预览代码报错?
SwiftData 缺少上下文: @Model 类型依赖一个有效的 ModelContainer 和上下文。如果没有绑定容器,Friendface 的 @Model 特性在预览中无法正确解析和初始化。
没有显式插入数据: 预览中访问的数据是动态加载的。如果上下文中没有插入数据,视图会尝试从空的数据源中加载,可能会导致崩溃或无法渲染。
预览环境的限制问题
如果上述问题无法解决你的问题,可以看这一个案例:
在SwiftData使用的过程中,尝试删除和插入SwiftData对象:
Button("4面", action: {
try? modelContext.delete(model: Dice.self)
let diceNum = Dice(num:4)
modelContext.insert(diceNum)
print("4面插入成功")
})
每次点击Button按钮,预览都会报错:
CrashReportError: hackingwithswift crashed due to an uncaught exception
hackingwithswift crashed due to an uncaught exception `NSInternalInconsistencyException`. Reason: NSFetchRequest could not locate an NSEntityDescription for entity name 'Dice'.
但模拟器正常操作按钮,这时可以考虑在预览中使用内存存储:
#Preview {
ContentView()
.modelContainer(for: Dice.self, inMemory: true) // 使用内存存储
}
在预览中使用InMemory:true后,预览可以正常点击。
问题分析
1、预览环境的特点
SwiftUI 预览运行在一个沙盒化的环境,与模拟器或真实设备的运行环境不同。
预览中的 ModelContainer 默认会尝试使用持久化存储(例如 SQLite),但在预览环境下:
文件系统可能不可用或被限制。
数据库的初始化和读写可能失败,尤其是在动态创建存储文件时。
如果 ModelContainer 的初始化失败(比如无法找到 Dice 的实体描述或存储文件路径),就会导致崩溃。
2、inMemory: true 的作用
启用内存存储后,ModelContainer 将仅在内存中运行,不会尝试访问文件系统或磁盘。这解决了以下问题:
1)避免持久化存储的问题
文件路径限制:在预览环境中,持久化存储可能无法正确定位或创建文件路径。
SQLite 依赖:预览环境可能缺乏对 SQLite 等存储机制的支持。
使用内存存储后,数据只存在于内存中,避免了文件系统相关问题。
2)更轻量级的存储方式
内存存储不需要初始化或管理数据库文件,适合开发和预览时使用。
在预览中,每次更新都会重新初始化内存存储,提供一个干净的运行环境,避免了历史数据的干扰。
3)便于动态修改数据
在内存中运行时,@Query 和 modelContext 操作(如插入、删除)可以即时生效,无需担心磁盘存储的同步或延迟问题。
在预览中动态切换数据的面数(如切换骰子)不会触发崩溃。
为什么模拟器和真机不需要 inMemory
模拟器和真机有完整的文件系统支持,ModelContainer 可以正确初始化持久化存储。
持久化存储的文件可以在磁盘上创建并管理,因此不会出现实体找不到或存储路径问题。
什么时候需要持久化存储
尽管内存存储适合预览和测试环境,但以下情况下需要使用持久化存储:
1、在生产环境中需要保存用户数据。
2、需要对数据进行长期管理和查询(如在 SQLite 数据库中)。
3、需要多次运行应用时保持数据一致性。
在开发时可以使用 inMemory,而在模拟器或真机上测试时切换回持久化存储:
#if DEBUG
let container = try ModelContainer(for: Dice.self, inMemory: true)
#else
let container = try ModelContainer(for: Dice.self)
#endif
在预览中使用 inMemory: true 的优势:
1、避免文件系统和数据库的初始化问题。
2、提供轻量、临时的数据存储环境。
3、更好地支持动态数据操作(如插入、删除)。
总结
总体来说,可能很多预览的报错,都是因为视图需要传递实例,而@Model修饰的Class在传递时,会检查上下文绑定,如果没有绑定上下文,就会报错。
因此比较好的预览代码,就是创建一个ModelContainer容器,然后将测试数据插入到ModelContainer容器内,再将插入的数据用值传递的形式传递给视图。
针对不需要传递参数的视图,可以绑定一个ModelContainer并设置仅在内存中使用。
此外,我还将关于ModelContainer上下文导致预览的报错,单独写在《SwiftData中ModelContainer上下文绑定导致预览报错问题》一文中,有兴趣的可以进一步了解上下文导致的报错和解决方案。
相关文章
1、Swift编译调试代码#if DEBUG:https://fangjunyu.com/2024/11/25/swift%e7%bc%96%e8%af%91%e8%b0%83%e8%af%95%e4%bb%a3%e7%a0%81if-debug/
2、SwiftData中ModelContainer上下文绑定导致预览报错问题:https://fangjunyu.com/2024/12/27/swiftdata%e4%b8%admodelcontainer%e4%b8%8a%e4%b8%8b%e6%96%87%e7%bb%91%e5%ae%9a%e5%af%bc%e8%87%b4%e9%a2%84%e8%a7%88%e6%8a%a5%e9%94%99%e9%97%ae%e9%a2%98/