由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)

概述

从 WWDC 23 开始,苹果推出了崭新的数据库框架 SwiftData。默认在 SwiftData 中所有对数据的操作都会在主线程中进行,稍有不慎就会让 App 变得"鹅行鸭步"

那么,对于耗时的数据操作我们该如何优雅的面对?又如何让界面与其"一心一力"的同步呢?

在本篇博文中,您将学到如下内容:

  1. SwiftData 如何在后台改变数据?
  2. 如何将后台的更改同步到界面中?

这是本系列第二篇博文。闲言少叙,让我们马上开始 SwiftData 精彩的探究之旅吧!

Let's dive in!!!;)


3. SwiftData 如何在后台改变数据?

现在虽然我们已经圆满解决了之前那个崩溃问题,但是 SwiftData 中数据操作的"水还很深",值得大家进一步"磨砥刻厉"的研究一番。

首先,我们从简单且实用的话题的聊起:SwiftData 如何在后台修改数据?

SwiftData 对于数据的操作是通过模型上下文来完成的,而通过之前的介绍可知:主模型上下文(Main Model Context,以下简称为主上下文)只能在主线程或 MainActor 上修改数据,而私有模型上下文则适合在其它线程或 Actor 中操作数据。

假设这样一种常见的场景:我们的 App 要在启动时生成大量数据,如果将这一操作用主上下文在主线程上执行就会阻塞界面,这在 App 开发中是绝对不能容忍的!

所以,一种方法就是将它们放在私有上下文在后台线程中执行。

将之前 ContentView 视图的代码略作修改,我们现在暂时抛弃 Model 类型,下面所有的代码都只涉及 Item 托管类型:

swift 复制代码
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:

swift 复制代码
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 数据变化了。

虽说手动刷新整个视图可以勉强"得偿所愿",但它毕竟会对渲染性能造成或多或少的潜在影响。有没有更好的方法呢?

答案是肯定的!

总结

在本篇博文中,我们讨论了如何在后台线程处理 SwiftData 的数据操作,又如何将这些更改同步到界面中去。

在下一篇博文里,我们将会介绍 SwiftData 2.0 中新引入的 History Trace 机制,并用它来更优雅的解决问题。

感谢观赏,再会 8-)

相关推荐
文牧之27 分钟前
PostgreSQL 的扩展pg_freespacemap
运维·数据库·postgresql
deriva44 分钟前
某水表量每15分钟一报,然后某天示数清0了,重新报示值了 ,如何写sql 计算每日水量
数据库·sql
Leo.yuan2 小时前
数据库同步是什么意思?数据库架构有哪些?
大数据·数据库·oracle·数据分析·数据库架构
Kookoos2 小时前
ABP VNext 与 Neo4j:构建基于图数据库的高效关系查询
数据库·c#·.net·neo4j·abp vnext
云之兕2 小时前
MyBatis 的动态 SQL
数据库·sql·mybatis
gaoliheng0063 小时前
Redis看门狗机制
java·数据库·redis
?ccc?3 小时前
MySQL主从复制与读写分离
数据库·mysql
会飞的Anthony3 小时前
数据库优化实战分享:高频场景下的性能调优技巧与案例解析
数据库
MaoJiu4 小时前
Flutter造轮子系列:flutter_permission_kit
flutter·swiftui