SwiftUI:如何更新由一组静态数据驱动的列表,并从另一组动态数据中提取信息位? [英] SwiftUI: How do I update a List that is driven by a static set of data and pulls bits of information from another dynamic set of data?

查看:47
本文介绍了SwiftUI:如何更新由一组静态数据驱动的列表,并从另一组动态数据中提取信息位?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我什至不确定标题问题是否有意义.请继续阅读:)

交叉链接到

预算结构保持不变-除非用户更改预算,但这又是一天的问题-Core Data拥有一个每月实例".每个类别中的 BudgetCategoryEntry .因此,驱动列表的数据位于具有 sections budget 属性中,并且每个部分都具有 categories .列表的代码如下:

  var body:some View {VStack {//其他一些视图列表 {ForEach(budget.sections){如果!section.hidden {部分(标题:BudgetSectionCell(部分:section,amountText:"){ForEach(section.categories){如果!category.hidden {BudgetCategoryCell(category:category,amountText:formatAmount(findBudgetCategoryEntry(withId:category.id)?. amount ?? 0)??-")}}}}}}//更多视图}} 

您当中最聪明的人会注意到 findBudgetCategoryEntry(withId:UUID)->BudgetCategoryEntry 函数.我的想法是,我要求Core Data使用固定预算结构中的ID给我与预算,月份和年份相对应的预算类别条目,然后仅在单元格中显示金额.

现在,因为我需要能够指定 month year ,所以在我的视图中将它们作为 @State 属性:

  @State私有var月:Int = 0@State私人var年:Int = 0 

然后,我需要一个 FetchRequest ,但是,因为我需要设置 NSPredicates 来引用实例属性( month year)我不能使用 @FetchRequest 包装器.所以,这就是我所拥有的:

  private var budgetEntriesRequest:FetchRequest< BudgetCategoryEntry>私人var budgetEntries:FetchedResults< BudgetCategoryEntry>{budgetEntriesRequest.wrappedValue}init(budget:BudgetInfo,currentBudgetId:Binding< UUID?>){_budget = .init(initialValue:预算)_currentBudgetId = currentBudgetIdvar cal = Calendar(identifier:.gregorian)cal.locale =语言环境(标识符:self._budget.wrappedValue.localeIdentifier)现在让我们= Date()_month = .init(initialValue:cal.component(.month,from:now))_monthName = .init(initialValue:cal.monthSymbols [self._month.wrappedValue])_year = .init(initialValue:cal.component(.year,from:now))budgetEntriesRequest = FetchRequest< BudgetCategoryEntry>(实体:BudgetCategoryEntry.entity(),sortDescriptors:[],谓词:NSCompoundPredicate(andPredicateWithSubpredicates:[NSPredicate(格式:"budgetId ==%@",budget.id为CVarArg),NSPredicate(格式:"month ==%i",_ month.wrappedValue),NSPredicate(格式:"year ==%i",_ year.wrappedValue)]))}私人功能findBudgetCategoryEntry(withId:UUID)->BudgetCategoryEntry?{返回budgetEntries.first(其中:{$ 0.budgetCategoryId == withId})} 

这似乎起作用.一次.出现在视图上.然后祝您好运,更改 month year 并相应地更新 budgetEntries ,这使我感到困惑,因为我认为对进行任何更新@State 变量使整个视图重新呈现,但是同样,SwiftUI可能比这还要聪明,并且在知道某个变量更改时知道要更新哪些部分方面有些不可思议.其中存在一个问题:这些变量仅间接影响 List ,因为它们应强制执行新的获取请求,然后又会更新 budgetEntries .除此之外,这也不起作用,因为 budgetEntries 不会直接影响 List ,因为它是由 findBudgetCategoryEntry(withId:UUID)功能.

然后,我尝试了多种设置组合,包括使用适当的初始化程序将 budgetEntriesRequest 设置为 @State 变量,但是这使得访问 budgetEntries爆炸,我不确定为什么,但是我可以看到这不是正确的方法.

我唯一想到的另一件事是将 findBudgetCategoryEntry 设置为 Dictionary 形状的属性,在该属性中,我使用类别ID作为键来访问条目.我还没有尝试过,但是即使我现在脑子里有道理,它仍然取决于是否更改 NSPredicates 中的三个变量是否确实使获取请求再次运行.

总结:我愿意接受建议.

编辑:我尝试了在此处概述的解决方案,这似乎是一种明智的方法,并且实际上,某种意义上说它可以提供或多或少相同的功能,并且条目似乎有所变化.但是,有两个问题:

  1. 似乎有一个滞后.例如,无论何时更改月份,我都会​​获得先前选择的月份的预算条目.我用 onChange(of:month)进行了验证,该打印出了 FetchedResults ,但这可能只是因为 FetchedResults 已更新 onChange 被调用之后.我不知道.
  2. 列表仍然没有更新.我虽然用示例中的 DynamicFetchView 包围了它,但仍会以某种方式说服SwiftUI将 FetchedResults 视为内容视图数据集的一部分,但不会.

作为对2的回应,我尝试了一些愚蠢但有时愚蠢的方法,那就是使用布尔值 @State ,将其切换为 onChange(of:month)(和年份),然后在列表中显示类似 .background(needsRefresh?Color.clear:Color.clear)的内容,但当然也不起作用.

我确认,关于上一次编辑中的第1点, FetchedResults 确实可以正确更新,即使只是过一会儿.所以在这一点上,我只是无法让List用正确的值重绘其单元格.

稍后(在BudgetCategoryCell行上)的一个断点,我可以确认budgetEntries( FetchedResults )已更新,并且当我进行简单更改时就重新绘制了List一个 @State 属性( month )-是的,我确保删除了所有hack和 onChange 内容.

以下 class ViewModel:ObservableObject {var budgetId:UUID {didSet {buildRequest()objectWillChange.send()}}var month:Int {didSet {buildRequest()objectWillChange.send()}}var年:Int {didSet {buildRequest()objectWillChange.send()}}var budgetEntriesRequest:FetchRequest< BudgetCategoryEntry>公共变量budgetEntries:FetchedResults< BudgetCategoryEntry>{budgetEntriesRequest.wrappedValue}init(budgetId:UUID,月份:整数,年份:整数){self.budgetId = budgetIdself.month =月self.year =年budgetEntriesRequest = FetchRequest< BudgetCategoryEntry>(实体:BudgetCategoryEntry.entity(),sortDescriptors:[],谓词:NSCompoundPredicate(andPredicateWithSubpredicates:[NSPredicate(格式:"budgetId ==%@",self.budgetId.uuidString),NSPredicate(格式:"month ==%ld",self.month),NSPredicate(格式:"year ==%ld",self.year)]))}func buildRequest(){budgetEntriesRequest = FetchRequest< BudgetCategoryEntry>(实体:BudgetCategoryEntry.entity(),sortDescriptors:[],谓词:NSCompoundPredicate(andPredicateWithSubpredicates:[NSPredicate(格式:"budgetId ==%@",budgetId.uuidString),NSPredicate(格式:"month ==%ld",月份),NSPredicate(格式:"year ==%ld",年份)]))}私人功能findBudgetCategoryEntry(withId:UUID)->BudgetCategoryEntry?{返回budgetEntries.first(其中:{$ 0.budgetCategoryId == withId})}}

但是当通过 budgetEntries 计算属性访问 budgetEntriesRequest.wrappedValue 时,我仍然得到 EXC_BAD_INSTRUCTION .

我使用 DynamicFetchView 技术制作了一个最小的可重现示例,并设法获取了 List 进行更新.所以至少我知道这种技术是可行且可行的.现在,我必须弄清楚我的主应用程序在做什么.

我尝试用其内容替换 BudgetCategoryCell (从 HStack 向下),现在我可以获取列表的单元格进行更新.看来这可能是绑定问题.现在,考虑到我正在向其中传递局部变量,我试图找出如何使 BudgetCategoryCell 与绑定成为一个视图.

事实证明 List 正确刷新了,但是其中的 BudgetCategoryCell 却没有刷新.我将 BudgetCategoryCell 的主体直接拉到 ForEach 中,突然间它按预期开始工作.现在,我需要弄清楚如何更新该视图,但这是另一个问题.

I'm not even sure the title question makes sense. Please carry on reading regardless :)

EDIT: Cross-link to Apple's Developer Forum.

EDIT: Here's the source code of the project.

I have a SwiftUI view that is using a peculiar combination of a "fixed" data structure and data from Core Data, and I'm struggling to understand how to make the two cooperate. I am pretty sure this is not something covered in your average "advanced core data and swiftui tutorials" because I just spent three days down a rabbit hole of documentation, guides, and tutorials, and all I could come up with is not working. Let me explain.

My view is mainly a List that shows a "fixed" set of rows. Let's call them "budget categories" -- yes, I'm making the nth budgeting app, bear with me. It looks more or less like this.

The structure of the budget stays the same -- unless the user changes it, but that's a problem for another day -- and Core Data holds a "monthly instance" of each category, let's call it BudgetCategoryEntry. Because of this, the data that drives the list is in a budget property that has sections, and each section has categories. The list's code goes like this:

var body: some View {
    VStack {
        // some other views
        
        List {
            ForEach(budget.sections) { section in
                if !section.hidden {
                    Section(header: BudgetSectionCell(section: section,
                                                      amountText: ""
                    ) {
                        ForEach(section.categories) { category in
                            if !category.hidden {
                                BudgetCategoryCell(category: category, amountText: formatAmount(findBudgetCategoryEntry(withId: category.id)?.amount ?? 0) ?? "--")
                            }
                        }
                    }
                }
            }
        }
        
        // more views
    }
}

The smarter among you would have noticed the findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry function. The idea is that I ask Core Data to give me the budget category entries corresponding to my budget, month, and year, using the ID from the fixed budget structure, and then I only display the amount in the cell.

Now, because I need to be able to specify month and year, I have them as @State properties in my view:

@State private var month: Int = 0
@State private var year: Int = 0

Then I need a FetchRequest but, because I need to set up NSPredicates that refer to instance properties (month and year) I can't use the @FetchRequest wrapper. So, this is what I have:

private var budgetEntriesRequest: FetchRequest<BudgetCategoryEntry>
private var budgetEntries: FetchedResults<BudgetCategoryEntry> { budgetEntriesRequest.wrappedValue }

init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
    _budget = .init(initialValue: budget)
    _currentBudgetId = currentBudgetId
    var cal = Calendar(identifier: .gregorian)
    cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
    let now = Date()
    _month = .init(initialValue: cal.component(.month, from: now))
    _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
    _year = .init(initialValue: cal.component(.year, from: now))

    budgetEntriesRequest = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
                                                             sortDescriptors: [],
                                                             predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
                                                                NSPredicate(format: "budgetId == %@", budget.id as CVarArg),
                                                                NSPredicate(format: "month == %i", _month.wrappedValue),
                                                                NSPredicate(format: "year == %i", _year.wrappedValue)
                                                             ])
    )
}

private func findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry? {
    return budgetEntries.first(where: { $0.budgetCategoryId == withId })
}

This appears to work. Once. On view appear. Then good luck changing month or year and having the budgetEntries update accordingly, which baffles me a bit because I thought any update to a @State variable makes the entire view re-render, but then again maybe SwiftUI is a bit smarter than that and has some magicking to know what parts to update when a certain variable changes. And therein lies the problem: those variables only indirectly affect the List, in that they should force a new fetch request which then in turn would update budgetEntries. Except that that is not going to work either, because budgetEntries does not affect directly the List as that is mediated by the findBudgetCategoryEntry(withId: UUID) function.

I then tried a number of combinations of setups, including making budgetEntriesRequest a @State variable, with an appropriate change of initializer, but that made accessing budgetEntries go bang and I'm not entirely sure why, but I can see this is not the right approach.

The only other thing I can think of is to make findBudgetCategoryEntry a property in the shape fo a Dictionary where I use the category ID as a key to access the entry. I haven't tried this but, even if it does make sense in my head right now, it still depends on whether changing the three variables in the NSPredicates actually do make the fetch request run again.

In summary: I'm open to suggestions.

EDIT: I tried the solution outlined here which looks like a sensible approach, and in fact it sort-of works in the sense that it provides more or less the same functionality AND the entries seem to change. Two problems, however:

  1. There seems to be a lag. Whenever I change month, for example, I get the budget entries for the month that was previously selected. I verified this with an onChange(of: month) which prints out the FetchedResults, but maybe it's just because the FetchedResults are updated after onChange is called. I don't know.
  2. The list still does not update. I though surrounding it with the DynamicFetchView as in the example would somehow convince SwiftUI to consider the FetchedResults as part of the dataset of the content view, but nope.

In response to 2, I tried something stupid but sometimes stupid works, which is to have a boolean @State, toggle it onChange(of: month) (and year), and then having something like .background(needsRefresh ? Color.clear : Color.clear) on the list but of course that didn't work either.

EDIT 2: I verified that, regarding point 1 in the previous EDIT, the FetchedResults do update correctly, if only after a while. So at this point I just can't get the List to redraw its cells with the correct values.

EDIT 3: A breakpoint later (on the line of BudgetCategoryCell) I can confirm the budgetEntries (FetchedResults) are updated, and the List is redrawn when I simply change a @State property (month) -- and yes, I made sure I removed all the hacks and onChange stuffs.

EDIT 4: Following nezzy's suggestion of building a ViewModel, I did the following:

class ViewModel: ObservableObject {
    var budgetId: UUID {
        didSet {
            buildRequest()
            objectWillChange.send()
        }
    }

    var month: Int {
        didSet {
            buildRequest()
            objectWillChange.send()
        }
    }

    var year: Int {
        didSet {
            buildRequest()
            objectWillChange.send()
        }
    }

    var budgetEntriesRequest: FetchRequest<BudgetCategoryEntry>
    public var budgetEntries: FetchedResults<BudgetCategoryEntry> { budgetEntriesRequest.wrappedValue }

    init(budgetId: UUID, month: Int, year: Int) {
        self.budgetId = budgetId
        self.month = month
        self.year = year
        budgetEntriesRequest = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
                                                                 sortDescriptors: [],
                                                                 predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
                                                                    NSPredicate(format: "budgetId == %@", self.budgetId.uuidString),
                                                                    NSPredicate(format: "month == %ld", self.month),
                                                                    NSPredicate(format: "year == %ld", self.year)
                                                                 ])
                               )
    }

    func buildRequest() {
        budgetEntriesRequest = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
                                                                 sortDescriptors: [],
                                                                 predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
                                                                    NSPredicate(format: "budgetId == %@", budgetId.uuidString),
                                                                    NSPredicate(format: "month == %ld", month),
                                                                    NSPredicate(format: "year == %ld", year)
                                                                 ])
                               )
    }

    private func findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry? {
        return budgetEntries.first(where: { $0.budgetCategoryId == withId })
    }
}

but I still get an EXC_BAD_INSTRUCTION when accessing budgetEntriesRequest.wrappedValue through the budgetEntries computed property.

EDIT 5: I made a minimum reproducible example using the DynamicFetchView technique and I managed to get the List to update. So at least I know that this technique is viable and working. Now I have to figure out what I'm doing wrong in my main application.

EDIT 6: I tried replacing BudgetCategoryCell with its content (from the HStack down) and I can now get the list's cells to update. It looks like this might have been an issue with binding. I am now trying to figure out how to make BudgetCategoryCell a view with bindings considering that I'm passing local variables into it.

解决方案

It turns out that the List was correctly refreshing, but the BudgetCategoryCells within it were not. I pulled out the body of BudgetCategoryCell directly into the ForEach and all of a sudden it started working as expected. Now I need to figure out how to make that view update, but this is a matter for another question.

这篇关于SwiftUI:如何更新由一组静态数据驱动的列表,并从另一组动态数据中提取信息位?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆