Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决

2024-06-04 6806阅读

Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决 第1张

概览

在现代化 App 的开发中,所见即所得(WYSIWYG)界面逻辑的调试无疑能够为头发茂密的小码农们“雪中送炭”。所幸的是 Xcode 本身的预览(Preview)机制就对其提供了卓越的支持。

Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决 第2张

然而某些 SwiftUI 视图在预览中渲染的并不正常,比如 CoreData 对应的 @FetchRequest 查询结果可能无法及时与界面同步,这该如何解决呢?

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

  • 概览
  • 1. “奇葩”的 SwiftUI 预览
  • 2. 手动获取 @FetchRequest 结果
  • 3. “知趣”刷新 SwiftUI 视图
  • 总结

    相信今后如果小伙伴们遇到与此类似的问题都将胸有成竹、手到擒来!

    那还等什么呢?Let‘s go!!!😉


    对应的视频课在此,欢迎恣意观赏 😃

    Xcode 预览调试中 CoreData 数据不刷新的解决


    1. “奇葩”的 SwiftUI 预览

    从某种意义上来说,Xcode 中预览(Preview)机制是一把双刃剑:一方面它为我们界面调试带来了极大的便利,另一方面它某些“恢诡谲怪”的怪癖有时真会让秃头码农们欲哭无泪、头发落满地。

    下面是一段非常简单的 SwiftUI 代码:

    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
            
        @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 中。

    Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决 第3张

    从上图中可以看到,代码在模拟器或真机中运行一切都很 nice,可是如果在预览中执行呢?

    Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决 第4张

    出乎意料的是原本“安适如常”的代码却变得“无疾而终”了:新挑战并没有在应该的地方出现,不了解预览“本性”的小伙伴们可能以为是代码出了什么问题呢。

    其实,我们的代码“无懈可击”,罪魁祸首恰恰是 Xcode 中那个平淡无奇的 Preview。

    2. 手动获取 @FetchRequest 结果

    我们基本确定这是预览中的一个 Bug,至少是一个“怪癖(Quirk)”。

    要想在预览中也能得到正确的运行结果,我们可以采取应变措施(Workaround)。

    仔细观察上面的代码,我们是通过 @FetchRequest 动态属性来获取 CoreData 数据库中符合条件的托管对象的。

    其实,我们还可以手动来取得与 @FetchRequest 相同的结果。这样做的好处是:我们可以最大化把控执行数据查询的时机。

    新建一个 allCustomCaves 方法,它将返回与@FetchRequest 完全相同的结果(每一个挑战都对应一个 Cave,所以我们在界面上只要显示 Cave 就可以了):

    static func allCustomCaves(_ moc: NSManagedObjectContext) throws -> [V2_Cave] {
        let req: NSFetchRequest = fetchRequest()
        req.predicate = NSPredicate(format: "isUndeletable = false")
        req.sortDescriptors = [.init(keyPath: \V2_Cave.name, ascending: false)]
        return try moc.fetch(req)
    }
    

    在主视图中创建一个计算属性 customCaves,并删除之前 @FetchRequest 对应的同名属性。修改源代码中与此相关的内容:

    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,借助它我们可以轻而易举的捕获到视图的关闭行为:

    Xcode 15 预览 SwiftUI 视图中 @FetchRequest 查询结果不能正确刷新的解决 第5张

    我们的意图是在新建挑战 sheet 视图被关闭时立即强制重置 LazyVGrid 容器视图,这是通过改变其 id 值来实现的:

    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 查询结果不能正确刷新的解决 第6张

    现在无论是在模拟器、真机或是预览里,我们的代码逻辑都能如实反映出对应的运行结果,从而让秃头码农们不再彷徨绝望,棒棒哒!💯

    总结

    在本篇博文中,我们介绍了 Xcode 15 预览 SwiftUI 视图中 @FetchRequest 的查询结果不能被正确刷新的问题,并通过应变措施让代码在模拟器、真机或是预览中都能毫无二致的反应出”理所当然“的运行结果。

    感谢观赏,再会!😎


    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]