SwiftData中ModelContainer上下文绑定导致预览报错问题
SwiftData中ModelContainer上下文绑定导致预览报错问题

SwiftData中ModelContainer上下文绑定导致预览报错问题

在 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)
        ]
    }
}

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

发表回复

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