编辑:交叉链接到Apple的开发者论坛。
编辑:这是该项目的源代码。
我有一个SwiftUI视图,它使用“固定”数据结构和来自Core Data的数据的特殊组合,我正在努力理解如何使两者合作。我相信这不是你平均的“高级核心数据和swiftui教程”所涵盖的内容,因为我刚刚花了三天时间查看文档、指南和教程,但我找不到可行的方法。让我解释一下。
我的视图主要是一个
List
,显示了一组“固定”的行。我们称之为“预算类别”——是的,我正在制作第n个预算应用程序,请耐心等待。它看起来更或多或少像这样。
预算的结构保持不变--除非用户更改它,但这是另一天的问题--Core Data保存每个类别的“月度实例”,我们称之为。因此,驱动列表的数据在一个budget
属性中,该属性具有sections
,每个部分都有categories
。列表的代码如下: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
}
}
你们中聪明的人会注意到
findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry
函数。我的想法是使用来自固定预算结构的ID,要求Core Data给我提供与我的预算、月份和年份对应的预算类别条目,然后在单元格中仅显示金额。现在,因为我需要能够指定
month
和year
,所以我将它们作为我的视图中的@State
属性。@State private var month: Int = 0
@State private var year: Int = 0
然后我需要一个
FetchRequest
,但由于我需要设置引用实例属性 (month
和 year
) 的 NSPredicates
,所以我不能使用 @FetchRequest
包装器。因此,这就是我的代码: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 })
}
这似乎有效。一次。在视图出现时。然后祝你好运改变
month
或year
并相应地更新budgetEntries
,这让我感到困惑,因为我认为对@State
变量的任何更新都会重新渲染整个视图,但是也许SwiftUI比那更聪明,并且在某些变量更改时知道要更新哪些部分。问题就在于:这些变量只间接影响List
,因为它们应该强制执行新的获取请求,然后进而更新budgetEntries
。但这也行不通,因为budgetEntries
并没有直接影响List
,因为它由findBudgetCategoryEntry(withId: UUID)
函数中介。
然后我尝试了许多设置组合,包括使budgetEntriesRequest
成为@State
变量,并进行适当的初始化更改,但这会使访问budgetEntries
失败,我不完全确定原因,但我可以看出这不是正确的方法。
findBudgetCategoryEntry
作为一个Dictionary
属性,其中使用类别ID作为键来访问该条目。虽然我现在能够理解它的意义,但我还没有尝试过,因为这仍然取决于是否更改NSPredicates
中的三个变量实际上会再次运行获取请求。总之:我愿意听取建议。
编辑:我尝试了这里概述的解决方案,看起来是一个明智的做法,事实上它有点起作用,因为它提供了几乎相同的功能,并且条目似乎发生了变化。但是存在两个问题:
- 似乎有一些延迟。例如,当我更改月份时,我会得到先前选择的月份的预算条目。我通过
onChange(of: month)
验证了这一点,它打印出FetchedResults
,但也许只是因为FetchedResults
在调用onChange
之后更新。我不确定。 - 列表仍然没有更新。我尝试像示例中那样将其包装在
DynamicFetchView
中,以某种方式说服SwiftUI将FetchedResults
视为内容视图数据集的一部分,但没有成功。
针对第二个问题,我尝试了一些愚蠢的方法,但有时候愚蠢的方法也能奏效,即使用一个布尔值的@State
,在onChange(of: month)
(和年份)时切换它,然后在列表上添加类似于.background(needsRefresh ? Color.clear : Color.clear)
的内容,但当然这也没有起作用。
FetchedResults
确实会正确更新。所以此时,我只是无法让列表使用正确的值重新绘制其单元格。
编辑3:稍后在BudgetCategoryCell行上设置断点后,我可以确认budgetEntries (FetchedResults
)已更新,并且当我仅更改@State
属性(month
)时,List将被重新绘制--是的,我确保删除了所有的hack和onChange
内容。
编辑4:根据nezzy's suggestion建立ViewModel的建议,我执行了以下操作:
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 })
}
}
但是当通过budgetEntries
计算属性访问budgetEntriesRequest.wrappedValue
时,仍然会出现EXC_BAD_INSTRUCTION
。
编辑5:我使用了DynamicFetchView
技术创建了一个最小可重现示例,并成功使List
更新。因此,我知道这种技术是可行的并且有效的。现在我需要弄清楚在我的主应用程序中做错了什么。
编辑6:我尝试用HStack
下面的内容替换BudgetCategoryCell
,现在我可以更新列表的单元格了。看起来这可能是绑定的问题。现在我正在尝试如何使BudgetCategoryCell
成为具有绑定的视图,考虑到我正在将局部变量传递给它。