SwiftData 预览报错问题
SwiftData 预览报错问题

SwiftData 预览报错问题

问题描述

在学习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/

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注