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

概述

在 WWDC 24 中,苹果推出了数据库框架 SwiftData 2.0,并为其加入了全新的 History Trace、"墓碑"等诸多激动人心的新功能。那么它们到底如何实际应用到我们的 App 中去呢?

想搞清楚历史记录追踪(History Trace)如何在 SwiftData 2.0 中大放异彩吗?看这篇就对了!

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

  1. 拯救者:History Trace!
  2. ModelContext.autosaveEnabled 开启并不保证改变会自动 save

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

Let's dive in!!!;)


7. 拯救者:History Trace!

为了通过历史记录追踪机制(History Trace)来更雅致地将后台线程中数据的修改与界面之间进行同步,我们需要分几步来完成。

第一步,为了区分主上下文和私有上下文对持久数据所做的更新,我们需要为模型上下文对象打上"标签(author)":

swift 复制代码
Task.detached {
    let modelContext = ModelContext(.preview)
    // 为上下文对象添加"标签"
    modelContext.author = "BG"
    
    let item = Item(name: "\(Int.random(in: 0...10000))")
    modelContext.insert(item)
    
    try! modelContext.save()
    
    await MainActor.run {
        ...
    }
}

这样一来我们就可以只关心私有上下文产生的更改,而完全忽略主上下文所做变更了。


当然,为了更具效率的使用 History Trace 机制,我们还可以根据时间令牌(DefaultHistoryToken)来进一步筛选所需的历史记录。

限于篇幅该特性将不在本系列文章中介绍,该特性的"缺席"不会影响本系列文章内容的理解,我们会在后续其它博文中再进行讨论。


接下来,我们只需在底层数据发生变化时利用 History Trace 机制读取这些 change 并妥善处理即可:

swift 复制代码
.onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: DispatchQueue.main)) { _ in
            
    let mainContext = modelContext
    var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
    historyDesc.predicate = #Predicate { trans in
        trans.author == "BG"
    }
    
    let transactions = try! mainContext.fetchHistory(historyDesc)
    for trans in transactions {
        
        for change in trans.changes {
            switch change {
            case .insert(let historyInsert):
                // 在这里处理记录的新插入操作
            case .update(let historyUpdate):
                break
            case .delete(let historyDelete):
                break
            @unknown default:
                fatalError()
            }
        }
    }
}

以上这些代码的主要功能是:

  • 创建 HistoryDescriptor<DefaultHistoryTransaction> 查询描述符。因为我们是在 SwiftData 默认的数据容器中查询历史记录,所以查询结果类型是 DefaultHistoryTransaction;
  • 利用 #Predicate 宏过滤掉查询结果里主上下文自身造成的改变;
  • 调用上下文的 fetchHistory 实例方法查询改变历史记录;
  • 查询结果为若干 DefaultHistoryTransaction 中的若干 HistoryChange 对象;

如上图所示,目前我们可以监听 3 种类型的数据变化,它们分别为:

  1. 数据的删除
  2. 数据的新增
  3. 数据的更新

因为前面例子中涉及到数据的新增操作,我们就先来看看它吧。

为了促使主上下文中对应数据记录的刷新,我们需要找到一种办法来触发它。一种方法是在托管类型中创建可刷新属性,在适当的时候将其实例中对应的值"Refresh"。

比如,在我们的 Item 类型里有一个时间戳(timestamp)属性:

swift 复制代码
@Model
class Item {
    var name: String
    var timestamp: Date
    
    init(name: String) {
        self.name = name
        timestamp = .now
    }
}

我们可以通过刷新它来触发主上下文背后支持的 @Query 查询结果的"Refresh"。

最后一步就是利用上面所有这些讨论过的内容和技巧,将一个全新的 ContentView 实现逻辑"跃然纸上":

swift 复制代码
struct ContentView: View {
    
    @Environment(\.modelContext) var modelContext
    @Query var items: [Item]
        
    var body: some View {
        NavigationStack {
            VStack {
                List(items) { item in
                    Text(item.name).font(.headline.weight(.heavy))
                }
            }
            .toolbar {
                ToolbarItem(placement:.topBarTrailing) {
                    Button("New", systemImage: "plus.app") {
                        Task.detached {
                            let modelContext = ModelContext(.preview)
                            modelContext.author = "BG"
                            
                            let item = Item(name: "\(Int.random(in: 0...10000))")
                            modelContext.insert(item)
                            
                            try! modelContext.save()
                        }
                    }
                    .foregroundStyle(.white)
                    .tint(.green)
                }
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: DispatchQueue.main)) { _ in
            
            let mainContext = modelContext
            var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
            historyDesc.predicate = #Predicate { trans in
                trans.author == "BG"
            }
            
            let transactions = try! mainContext.fetchHistory(historyDesc)
            for trans in transactions {
                for change in trans.changes {
                    
                    guard let changedItem = mainContext.model(for: change.changedPersistentIdentifier) as? Item else { continue }
                    
                    switch change {
                    case .insert(let historyInsert):
                    	// 刷新新插入 Item 对象中对应的属性,从而引起 @Query 结果的重新求值
                        changedItem.timestamp = .now
                    case .update(let historyUpdate):
                        break
                    case .delete(let historyDelete):
                        break
                    @unknown default:
                        fatalError()
                    }
                }
            }
        }
    }
}

其中可以看到:

  • 我们完全"抛弃了"原先靠手动刷新视图促使界面刷新的方法;
  • 我们利用 MainContext#model 实例方法将新插入的 Item 对象加载到了内存中;
  • 我们更新它的 timestamp 属性迫使 @Query 重新计算从而刷新 UI 的显示;

运行代码可以发现:现在仅需借助 SwiftData 2.0 中 History Trace 的"东风",我们即可以 easy 而 nice 的实时同步后台线程中数据的修改到界面中去了,棒棒哒!💯

8. ModelContext.autosaveEnabled 开启并不保证改变会自动 save

自 SwiftData 诞生那天起,它就"绞尽脑汁"的想超越自己的老前辈 CoreData。SwiftData 提供了很多简洁的方式来简化原本 CoreData 中复杂繁琐的功能调用。

其中,在 SwiftData 的模型上下文里包含一个 autosaveEnabled 属性,它用来表示是否需要在某些特定事件发生时自动将待处理的更改(pending changes)刷新到持久存储中去:

从上面的描述中可以看到:除了主上下文对象以外,其它私有上下文对象的 autosaveEnabled 属性默认都是为 false。

很多小伙伴们可能以为:只要将该属性置为 true 就可以信誓旦旦的确保所有修改都可以被自动保存到底层数据库中,其实并不是这样。

还拿上面的例子来说,如果我们将私有上下文的 autosaveEnabled 属性设置为 true,是否可以不调用 save 方法而让新增的 Item 自动写入到持久存储中去呢?

让我们驰马试剑一番吧:

swift 复制代码
Task.detached {
    let modelContext = ModelContext(.preview)
    modelContext.author = "BG"
    // 开启私有上下文的自动保存功能
    modelContext.autosaveEnabled = true
    
    let item = Item(name: "\(Int.random(in: 0...10000))")
    modelContext.insert(item)
    
    // 不再显式手动保存
    //try! modelContext.save()
}

编译运行代码可以发现:虽然此时上下文的 autosaveEnabled 被设置为 true,但其中的新增行为再也不会触发 NSPersistentStoreRemoteChange 消息了,这意味这些改变并没有真正的被保存到数据库中去。

因为 autosaveEnabled 属性说明文档中所谓的那些促使自动保存的"某些特定事件"含义并不明确,所以在需要确保将改变刷入持久存储的场景中我们最好还是手动调用一下 save() 方法为妙。

总结

在本篇博文中,我们讨论了如何利用 SwiftData 2.0 中全新的历史记录追踪(History Trace)机制来同步后台线程与 UI 中的数据;我们还介绍了为什么开启模型上下文中的自动保存特性并不能绝对保证改变会写入到底层数据库中。

感谢观赏,再会!8-)

相关推荐
老华带你飞12 分钟前
酒店预约|基于springboot 酒店预约系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
会飞的土拨鼠呀29 分钟前
如何查询MySQL的CPU使用率突然变高
数据库·mysql
想用offer打牌37 分钟前
一站式了解数据库三大范式(库表设计基础)
数据库·后端·面试
甘露s39 分钟前
MySQL深入之索引、存储引擎和SQL优化
数据库·sql·mysql
偶遇急雨洗心尘1 小时前
记录一次服务器迁移时,数据库版本不一致导致sql函数报错和系统redirect重定向丢失域名问题
运维·服务器·数据库·sql
Arva .2 小时前
MySQL 的存储引擎
数据库·mysql
Logic1012 小时前
《Mysql数据库应用》 第2版 郭文明 实验5 存储过程与函数的构建与使用核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
小二·2 小时前
MyBatis基础入门《十六》企业级插件实战:基于 MyBatis Interceptor 实现 SQL 审计、慢查询监控与数据脱敏
数据库·sql·mybatis
bing.shao2 小时前
Golang WaitGroup 踩坑
开发语言·数据库·golang
专注VB编程开发20年2 小时前
C#内存加载dll和EXE是不是差不多,主要是EXE有入口点
数据库·windows·microsoft·c#