我有一个视图,显示团队中的消息,这些消息是使用带有固定谓词“开发人员”的 @Fetchrequest 进行过滤的。
struct ChatView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)],
predicate: NSPredicate(format: "team.name == %@", "Developers"),
animation: .default) var messages: FetchedResults<Message>
@Environment(\.managedObjectContext)
var viewContext
var body: some View {
VStack {
List {
ForEach(messages, id: \.self) { message in
VStack(alignment: .leading, spacing: 0) {
Text(message.text ?? "Message text Error")
Text("Team \(message.team?.name ?? "Team Name Error")").font(.footnote)
}
}...
我想使这个谓词动态化,以便当用户切换团队时显示该团队的消息。下面的代码给了我以下错误
无法在属性初始值设定项中使用实例成员“teamName”;属性初始值设定项在“self”可用之前运行
struct ChatView: View {
@Binding var teamName: String
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)],
predicate: NSPredicate(format: "team.name == %@", teamName),
animation: .default) var messages: FetchedResults<Message>
@Environment(\.managedObjectContext)
var viewContext
...
我可以使用一些帮助来解决这个问题,到目前为止我无法自己解决这个问题。
有同样的问题,Brad Dillon 的评论显示了解决方案:
var predicate:String
var wordsRequest : FetchRequest<Word>
var words : FetchedResults<Word>{wordsRequest.wrappedValue}
init(predicate:String){
self.predicate = predicate
self.wordsRequest = FetchRequest(entity: Word.entity(), sortDescriptors: [], predicate:
NSPredicate(format: "%K == %@", #keyPath(Word.character),predicate))
}
在此示例中,您可以修改初始化程序中的谓词。
编辑 8/10/2023:我有一个新答案,但保留了下面的旧答案。看来我们一直在向后思考这个问题,因为在 SwiftUI 中,正确获取事实来源非常重要,因此我们实际上需要将关系搜索置于层次结构中的较高位置(而不是较低位置),默认使用一个虚拟谓词来查找没有结果,然后我们将计算绑定到获取谓词传递到导航中,当选择团队时,它会在获取请求上设置 nsPredicate,例如
@FetchRequest(
sortDescriptors: [SortDescriptor(\.createdAt)],
predicate: NSPredicate(value: false),
animation: .default)
private var messages: FetchedResults<Message>
var selectionBinding: Binding<[Message]> {
Binding {
if let p = messages.nsPredicate as? NSComparisonPredicate {
if let team = p.rightExpression.constantValue as? Team {
return [team]
}
}
return []
} set: { value in
if let v = value.first {
messages.nsPredicate = NSPredicate(format: "team = %@", v)
}
else{
messages.nsPredicate = NSPredicate(value: false)
}
}
}
var body: some View {
NavigationStack(path: selectionBinding) {
请注意,当没有选择时,谓词是一个假谓词,Core Data 会检测到它,甚至不会查询数据库,如果您打开 SQL 调试,您可以看到该数据库。
旧答案:
对于 SwiftUI,重要的是 View 结构不会出现更改,否则
body
将被不必要地调用,在 @FetchRequest
的情况下也会访问数据库。 SwiftUI 仅使用相等性来检查 View 结构中的更改,如果不相等(即任何属性已更改),则调用 body
。在 iOS 14 上,即使使用相同的参数重新创建 @FetchRequest
,也会产生不同的 View 结构,从而导致 SwiftUI 的相等性检查失败,并导致主体在正常情况下不会被重新计算。 @AppStorage
和@SceneStorage
也有这个问题,所以我觉得很奇怪,大多数人可能首先学习的@State
却没有!无论如何,我们可以使用属性不变的包装视图来解决这个问题,这可以阻止 SwiftUI 的差异算法:
struct ContentView: View {
@State var teamName "Team" // source of truth, could also be @AppStorage if would like it to persist between app launches.
@State var counter = 0
var body: some View {
VStack {
ChatView(teamName:teamName) // its body will only run if teamName is different, so not if counter being changed was the reason for this body to be called.
Text("Count \(counter)")
}
}
}
struct ChatView: View {
let teamName: String
var body: some View {
// ChatList body will be called every time but this ChatView body is only run when there is a new teamName so that's ok.
ChatList(messages: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)], predicate: NSPredicate(format: "team.name = %@", teamName)))
}
}
struct ChatList : View {
@FetchRequest var messages: FetchedResults<Message>
var body: some View {
ForEach(messages) { message in
Text("Message at \(message.createdAt!, formatter: messageFormatter)")
}
}
}
编辑:使用
EquatableView
而不是包装器 View
来实现同样的事情是可能的,以允许 SwiftUI 仅对 teamName
而不是 FetchRequest
变量进行比较。更多信息请点击这里:
https://swiftwithmajid.com/2020/01/22/optimizing-views-in-swiftui-using-equatableview/
可能是动态过滤@FetchRequest的更通用的解决方案。
1、创建自定义DynamicFetchView
import CoreData
import SwiftUI
struct DynamicFetchView<T: NSManagedObject, Content: View>: View {
let fetchRequest: FetchRequest<T>
let content: (FetchedResults<T>) -> Content
var body: some View {
self.content(fetchRequest.wrappedValue)
}
init(predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor], @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate)
self.content = content
}
init(fetchRequest: NSFetchRequest<T>, @ViewBuilder content: @escaping (FetchedResults<T>) -> Content) {
self.fetchRequest = FetchRequest<T>(fetchRequest: fetchRequest)
self.content = content
}
}
2、如何使用
//our managed object
public class Event: NSManagedObject{
@NSManaged public var status: String?
@NSManaged public var createTime: Date?
... ...
}
// some view
struct DynamicFetchViewExample: View {
@State var status: String = "undo"
var body: some View {
VStack {
Button(action: {
self.status = self.status == "done" ? "undo" : "done"
}) {
Text("change status")
.padding()
}
// use like this
DynamicFetchView(predicate: NSPredicate(format: "status==%@", self.status as String), sortDescriptors: [NSSortDescriptor(key: "createTime", ascending: true)]) { (events: FetchedResults<Event>) in
// use you wanted result
// ...
HStack {
Text(String(events.count))
ForEach(events, id: \.self) { event in
Text(event.name ?? "")
}
}
}
// or this
DynamicFetchView(fetchRequest: createRequest(status: self.status)) { (events: FetchedResults<Event>) in
// use you wanted result
// ...
HStack {
Text(String(events.count))
ForEach(events, id: \.self) { event in
Text(event.name ?? "")
}
}
}
}
}
func createRequest(status: String) -> NSFetchRequest<Event> {
let request = Event.fetchRequest() as! NSFetchRequest<Event>
request.predicate = NSPredicate(format: "status==%@", status as String)
// warning: FetchRequest must have a sort descriptor
request.sortDescriptors = [NSSortDescriptor(key: "createTime", ascending: true)]
return request
}
}
通过这种方式,您可以动态更改您的 NSPredicate 或 NSSortDescriptor。
修改了 @FKDev 的答案,因为它抛出了一个错误,我喜欢这个答案,因为它的简洁性以及与 SwiftUI 其余部分的一致性。只需要从获取请求中删除括号即可。虽然@Antoine Weber 的答案是一样的。
但是我对这两个答案都遇到了问题,包括我的答案。这会导致一个奇怪的副作用,即一些与获取请求无关的行会在屏幕右侧动画显示,然后仅在获取请求数据第一次更改时从左侧返回屏幕。当以默认的 SwiftUI 方式实现获取请求时,不会发生这种情况。
更新: 通过简单地删除获取请求动画参数,修复了屏幕外随机行动画的问题。虽然如果你需要这个论点,我不确定有什么解决方案。这很奇怪,因为您期望动画参数仅影响与该获取请求相关的数据。
@Binding var teamName: String
@FetchRequest var messages: FetchedResults<Message>
init() {
var predicate: NSPredicate?
// Can do some control flow to change the predicate here
predicate = NSPredicate(format: "team.name == %@", teamName)
self._messages = FetchRequest(
entity: Message.entity(),
sortDescriptors: [],
predicate: predicate,
// animation: .default)
}
另一种可能是:
struct ChatView: View {
@Binding var teamName: String
@FetchRequest() var messages: FetchedResults<Message>
init() {
let fetchRequest: NSFetchRequest<Message> = Message.fetchRequest()
fetchRequest.sortDescriptors = NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)
fetchRequest = NSPredicate(format: "team.name == %@", teamName),
self._messages = FetchRequest(fetchRequest:fetchRequest, animation: .default)
}
...
我刚刚遇到了类似的问题,并且发现
FetchRequest.Configuration
很有帮助。从原始代码中可以看出这一点
@Binding var teamName: String
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Message.createdAt, ascending: true)],
predicate: NSPredicate(format: "team.name == %@", "Developers"),
animation: .default) var messages: FetchedResults<Message>
,例如,绑定到
teamName
的 TextField,您可以添加一个 onChange
处理程序来更改 messages
的谓词:
TextField("Team Name", text: $teamName)
.onChange(of: teamName) { newValue in
messages.nsPredicate = newValue.isEmpty
? nil
: NSPredicate(format: "team.name == %@", newValue)
}
有关更多信息,请参阅 SwiftUI 的 FetchRequest.Configuration 的文档,特别是标记为 直接设置配置的部分。