概览
在现代化 App 的开发中,所见即所得(WYSIWYG)界面逻辑的调试无疑能够为头发茂密的小码农们"雪中送炭"。所幸的是 Xcode 本身的预览(Preview)机制就对其提供了卓越的支持。
然而某些 SwiftUI 视图在预览中渲染的并不正常,比如 CoreData 对应的 @FetchRequest 查询结果可能无法及时与界面同步,这该如何解决呢?
在本篇博文中,您将学到如下内容:
- 概览
- [1. "奇葩"的 SwiftUI 预览](#1. “奇葩”的 SwiftUI 预览)
- [2. 手动获取 @FetchRequest 结果](#2. 手动获取 @FetchRequest 结果)
- [3. "知趣"刷新 SwiftUI 视图](#3. “知趣”刷新 SwiftUI 视图)
- 总结
相信今后如果小伙伴们遇到与此类似的问题都将胸有成竹、手到擒来!
那还等什么呢?Let's go!!!😉
对应的视频课在此,欢迎恣意观赏 😃
Xcode 预览调试中 CoreData 数据不刷新的解决
1. "奇葩"的 SwiftUI 预览
从某种意义上来说,Xcode 中预览(Preview)机制是一把双刃剑:一方面它为我们界面调试带来了极大的便利,另一方面它某些"恢诡谲怪"的怪癖有时真会让秃头码农们欲哭无泪、头发落满地。
下面是一段非常简单的 SwiftUI 代码:
swift
struct V2_WorldView: View {
@Environment(V2_Model.self) var model
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \V2_Cave.name, ascending: false)], predicate: NSPredicate(format: "isUndeletable = false"), animation: .bouncy) var customCaves: FetchedResults<V2_Cave>
@State var isSheetNewChallengeView = false
private let builtInCaves = try? V2_Cave.allBuiltInCaves()
private let gridItems = [GridItem](repeating: .init(.flexible()), count: 3)
var body: some View {
Form {
Section("正在进行") {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(inProcessingCaves) { cave in
Button(action: {
tappingCave = cave
}, label: {
V2_CaveCell(cave: cave)
})
.buttonStyle(.borderless)
}
}
}
Section("内置挑战") {
if let builtInCaves {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(builtInCaves) { cave in
Button(action: {
tappingCave = cave
}, label: {
V2_CaveCell(cave: cave)
})
.buttonStyle(.borderless)
}
}
.padding(.vertical)
.navigationDestination(item: $tappingCave) { cave in
V2_CaveView(cave: cave)
}
}
}
Section("自定义挑战") {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(customCaves) { cave in
Button(action: {
tappingCave = cave
}, label: {
V2_CaveCell(cave: cave)
})
.buttonStyle(.borderless)
}
.padding(.vertical)
}
}
}
.sheet(isPresented: $isSheetNewChallengeView, content: {
V2_NewChallengeView()
})
.toolbar {
if model.mainTabSelecting == .world {
ToolbarItem(placement: .primaryAction) {
Button(action: {
isSheetNewChallengeView = true
}) {
Text("新挑战")
}
}
}
}
}
}
我们在 WorldView 中可以点击"新挑战"按钮,在新弹出的 sheet 视图中创建一个自定义挑战,等该视图被关闭后新创建的挑战应该出现在 WorldView 中自定义挑战的 Section 中。
从上图中可以看到,代码在模拟器或真机中运行一切都很 nice,可是如果在预览中执行呢?
出乎意料的是原本"安适如常"的代码却变得"无疾而终"了:新挑战并没有在应该的地方出现,不了解预览"本性"的小伙伴们可能以为是代码出了什么问题呢。
其实,我们的代码"无懈可击",罪魁祸首恰恰是 Xcode 中那个平淡无奇的 Preview。
2. 手动获取 @FetchRequest 结果
我们基本确定这是预览中的一个 Bug,至少是一个"怪癖(Quirk)"。
要想在预览中也能得到正确的运行结果,我们可以采取应变措施(Workaround)。
仔细观察上面的代码,我们是通过 @FetchRequest 动态属性来获取 CoreData 数据库中符合条件的托管对象的。
其实,我们还可以手动来取得与 @FetchRequest 相同的结果。这样做的好处是:我们可以最大化把控执行数据查询的时机。
新建一个 allCustomCaves 方法,它将返回与@FetchRequest 完全相同的结果(每一个挑战都对应一个 Cave,所以我们在界面上只要显示 Cave 就可以了):
swift
static func allCustomCaves(_ moc: NSManagedObjectContext) throws -> [V2_Cave] {
let req: NSFetchRequest<V2_Cave> = fetchRequest()
req.predicate = NSPredicate(format: "isUndeletable = false")
req.sortDescriptors = [.init(keyPath: \V2_Cave.name, ascending: false)]
return try moc.fetch(req)
}
在主视图中创建一个计算属性 customCaves,并删除之前 @FetchRequest 对应的同名属性。修改源代码中与此相关的内容:
swift
struct V2_WorldView: View {
var customCaves: [V2_Cave]? {
// 调用我们的自定义 Caves 获取方法
try? V2_Cave.allCustomCaves(Common.moc_auto)
}
var body: some View {
Form {
Section("正在进行") {...}
Section("内置挑战") {...}
Section("自定义挑战") {
if let customCaves {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(customCaves) { cave in
Button(action: {
tappingCave = cave
}, label: {
V2_CaveCell(cave: cave)
})
.buttonStyle(.borderless)
}
.id(refreshID)
.padding(.vertical)
}
}
}
}
}
}
现在我们已经完全接管了自定义 Cave 对象的创生过程,是时候在 Preview 中施展我们的"黑魔法"了。
3. "知趣"刷新 SwiftUI 视图
有了上面的 customCaves 计算属性,接下来我们只需在新挑战创建后适时的刷新界面从而反应出新的变化即可。
正如大家所见,在 WorldView 视图中我们是通过一个惰性 VGrid 来陈列所有自定义 Cave 托管对象的,那么此刻我们只需在"新建挑战"的 sheet 视图关闭后立即刷新 LazyVGrid 即可。
所幸的是,sheet 修改器方法包含一个视图被关闭时的回调闭包形参 onDismiss,借助它我们可以轻而易举的捕获到视图的关闭行为:
我们的意图是在新建挑战 sheet 视图被关闭时立即强制重置 LazyVGrid 容器视图,这是通过改变其 id 值来实现的:
swift
struct V2_WorldView: View {
@State private var refreshID = false
var customCaves: [V2_Cave]? {
try? V2_Cave.allCustomCaves(Common.moc_auto)
}
var body: some View {
Form {
Section("正在进行") {...}
Section("内置挑战") {...}
Section("自定义挑战") {
if let customCaves {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(customCaves) { cave in
Button(action: {
tappingCave = cave
}, label: {
V2_CaveCell(cave: cave)
})
.buttonStyle(.borderless)
}
.padding(.vertical)
}
.id(refreshID)
}
}
}
.sheet(isPresented: $isSheetNewChallengeView, onDismiss: {
// 在 sheet 视图被关闭时立即刷新 LazyVStack 容器
refreshID.toggle()
}, content: {
V2_NewChallengeView()
})
}
}
如上代码所示,当在数据库中插入"新挑战"托管对象之后,我们立即刷新了 SwiftUI 界面让预览的最新渲染"如期而至":
现在无论是在模拟器、真机或是预览里,我们的代码逻辑都能如实反映出对应的运行结果,从而让秃头码农们不再彷徨绝望,棒棒哒!💯
总结
在本篇博文中,我们介绍了 Xcode 15 预览 SwiftUI 视图中 @FetchRequest 的查询结果不能被正确刷新的问题,并通过应变措施让代码在模拟器、真机或是预览中都能毫无二致的反应出"理所当然"的运行结果。
感谢观赏,再会!😎