由一个 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-)

相关推荐
洛神灬殇1 小时前
【LLM大模型技术专题】「入门到精通系列教程」基于ai-openai-spring-boot-starter集成开发实战指南
网络·数据库·微服务·云原生·架构
小鸡脚来咯1 小时前
redis分片集群架构
数据库·redis·架构
christine-rr2 小时前
征文投稿:如何写一份实用的技术文档?——以软件配置为例
运维·前端·网络·数据库·软件构建
海尔辛2 小时前
SQL 基础入门
数据库·sql
betazhou3 小时前
有没有 MariaDB 5.5.56 对应 MySQL CONNECTION_CONTROL 插件
linux·数据库·mysql·oracle·mariadb
Elohim8153 小时前
数据库SQLite基础
数据库·sqlite
TDengine (老段)4 小时前
TDengine 支持的平台汇总
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
大熊猫侯佩4 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
想用offer打牌5 小时前
面试官问:Redis和MySQL数据一致,为什么还需要MySQL?🤠
数据库·redis·mysql