概述
从 WWDC 23 开始,苹果推出了崭新的数据库框架 SwiftData。默认在 SwiftData 中所有对数据的操作都会在主线程中进行,稍有不慎就会让 App 变得“鹅行鸭步”
那么,对于耗时的数据操作我们该如何优雅的面对?又如何让界面与其“一心一力”的同步呢?
在本篇博文中,您将学到如下内容:
- 概述
- 3. SwiftData 如何在后台改变数据?
- 4. 如何将后台的更改同步到界面中?
- 总结
这是本系列第二篇博文。闲言少叙,让我们马上开始 SwiftData 精彩的探究之旅吧!
Let‘s dive in!!!😉
3. SwiftData 如何在后台改变数据?
现在虽然我们已经圆满解决了之前那个崩溃问题,但是 SwiftData 中数据操作的“水还很深”,值得大家进一步“磨砥刻厉”的研究一番。
首先,我们从简单且实用的话题的聊起:SwiftData 如何在后台修改数据?
SwiftData 对于数据的操作是通过模型上下文来完成的,而通过之前的介绍可知:主模型上下文(Main Model Context,以下简称为主上下文)只能在主线程或 MainActor 上修改数据,而私有模型上下文则适合在其它线程或 Actor 中操作数据。
假设这样一种常见的场景:我们的 App 要在启动时生成大量数据,如果将这一操作用主上下文在主线程上执行就会阻塞界面,这在 App 开发中是绝对不能容忍的!
所以,一种方法就是将它们放在私有上下文在后台线程中执行。
将之前 ContentView 视图的代码略作修改,我们现在暂时抛弃 Model 类型,下面所有的代码都只涉及 Item 托管类型:
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query var items: [Item]
var body: some View {
VStack {
if let item = items.first {
Text(item.name)
}
}
.padding()
.task {
Task.detached {
let modelContext = ModelContext(.preview)
let item = Item(name: "\(Int.random(in: 0...10000))")
modelContext.insert(item)
try! modelContext.save()
}
}
}
}
从上面的代码可以看到,我们在 ContentView 显示时创建了一个包含随机值的 Item,并视图通过 @Query 将其“抓取”到主界面上显示。
值得注意的是,我们还做了下面几件事:
- 通过 Task.detached 创建了一个“分离”任务以确保“脏活累活”都在后台线程中运行;
- 使用 ModelContext 构造器创建了一个私有上下文,该上下文一旦创建就会和它处在的线程或 Actor 所绑定;
到目前为止一切都很简单惬意,不是吗?
不过当我们编译运行后,视图中心却空空如也!创建的 Item 跑哪去了呢?
4. 如何将后台的更改同步到界面中?
其实,后台线程新创建的 Item 托管对象就在那里,只是它还没有被同步到主上下文中而已。
对于目前的情况来说,SwiftUI 中的 @Query 只能自动同步主上下文中数据的改变,私有上下文中的改变却不在此列。这意味着:我们上面在后台线程中新增的 Item 对象并不能及时刷新到界面中。
这该如何是好呢?
一种简单却略显“粗暴”的方式是,在后台线程插入新 Item 对象后立即强制刷新 UI:
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Query var items: [Item]
@State var refreshID = false
var body: some View {
NavigationStack {
VStack {
List(items) { item in
Text(item.name).font(.headline.weight(.heavy))
}
.id(refreshID)
}
.toolbar {
ToolbarItem(placement:.topBarTrailing) {
Button("New", systemImage: "plus.app") {
Task.detached {
let modelContext = ModelContext(.preview)
let item = Item(name: "\(Int.random(in: 0...10000))")
modelContext.insert(item)
try! modelContext.save()
await MainActor.run {
refreshID.toggle()
}
}
}
.foregroundStyle(.white)
.tint(.green)
}
}
}
}
}
从上面的代码不难看出,我们每次在后台新插入 Item 对象后立即刷新了 List 视图,这样做会导致 SwiftUI 重新计算 @Query 宏中 items 的内容。
如此这般,我们即可在界面中及时反映出后台线程里私有上下文所导致的 SwiftData 数据变化了。
更多与 SwiftUI 界面刷新相关内容的介绍,请小伙伴们移步如下链接观赏:
- Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决
- SwiftUI 如何动态条件显示和隐藏 Toolbar 按钮且不做无谓刷新
- SwiftUI 如何快速识别视图(View)界面的刷新是由哪个状态的改变导致的?
- SwiftUI 后台刷新多个 Section 导致 global index in collection view 与实际不匹配问题的解决
- iOS 16 中 CoreData 托管对象发生变化但其衍生 (Derived) 属性在 SwiftUI 中不刷新的解决
- SwiftUI上下文菜单(Context Menu)内容不随对象状态刷新的解决
虽说手动刷新整个视图可以勉强“得偿所愿”,但它毕竟会对渲染性能造成或多或少的潜在影响。有没有更好的方法呢?
答案是肯定的!
总结
在本篇博文中,我们讨论了如何在后台线程处理 SwiftData 的数据操作,又如何将这些更改同步到界面中去。
在下一篇博文里,我们将会介绍 SwiftData 2.0 中新引入的 History Trace 机制,并用它来更优雅的解决问题。
感谢观赏,再会 😎