当 SwiftUI 中的相关实体发生更改时,如何更新 @FetchRequest?

Bjö*_* B. 36 core-data swiftui combine

在 SwiftUI 中,View我有一个List基于@FetchRequest显示Primary实体和通过关系连接Secondary实体的数据。当我添加一个带有新的相关次要实体的新实体时,View和它List的更新正确Primary

问题是,当我Secondary在详细视图中更新连接的项目时,数据库会更新,但更改未反映在Primary列表中。显然,@FetchRequest不会被另一个视图中的更改触发。

当我此后在主视图中添加新项目时,先前更改的项目最终会得到更新。

作为一种解决方法,我还更新Primary了详细信息视图中实体的属性,并将更改正确传播到Primary视图。

我的问题是:如何强制更新@FetchRequestsSwiftUI Core Data 中的所有相关内容?特别是,当我无法直接访问相关实体时/ @Fetchrequests

数据结构

import SwiftUI

extension Primary: Identifiable {}

// Primary View

struct PrimaryListView: View {
    @Environment(\.managedObjectContext) var context

    @FetchRequest(
        entity: Primary.entity(),
        sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
    )
    var fetchedResults: FetchedResults<Primary>

    var body: some View {
        List {
            ForEach(fetchedResults) { primary in
                NavigationLink(destination: SecondaryView(primary: primary)) {
                VStack(alignment: .leading) {
                    Text("\(primary.primaryName ?? "nil")")
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                }
                }
            }
        }
        .navigationBarTitle("Primary List")
        .navigationBarItems(trailing:
            Button(action: {self.addNewPrimary()} ) {
                Image(systemName: "plus")
            }
        )
    }

    private func addNewPrimary() {
        let newPrimary = Primary(context: context)
        newPrimary.primaryName = "Primary created at \(Date())"
        let newSecondary = Secondary(context: context)
        newSecondary.secondaryName = "Secondary built at \(Date())"
        newPrimary.secondary = newSecondary
        try? context.save()
    }
}

struct PrimaryListView_Previews: PreviewProvider {
    static var previews: some View {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        return NavigationView {
            PrimaryListView().environment(\.managedObjectContext, context)
        }
    }
}

// Detail View

struct SecondaryView: View {
    @Environment(\.presentationMode) var presentationMode

    var primary: Primary

    @State private var newSecondaryName = ""

    var body: some View {
        VStack {
            TextField("Secondary name:", text: $newSecondaryName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
            Button(action: {self.saveChanges()}) {
                Text("Save")
            }
            .padding()
        }
    }

    private func saveChanges() {
        primary.secondary?.secondaryName = newSecondaryName

        // TODO: ? workaround to trigger update on primary @FetchRequest
        primary.managedObjectContext.refresh(primary, mergeChanges: true)
        // primary.primaryName = primary.primaryName

        try? primary.managedObjectContext?.save()
        presentationMode.wrappedValue.dismiss()
    }
}
Run Code Online (Sandbox Code Playgroud)

G. *_*arc 64

我也为此苦苦挣扎,并找到了一个非常好的和干净的解决方案:

您必须将行包装在单独的视图中,并在实体的该行视图中使用 @ObservedObject。

这是我的代码:

酒单:

struct WineList: View {
    @FetchRequest(entity: Wine.entity(), sortDescriptors: [
        NSSortDescriptor(keyPath: \Wine.name, ascending: true)
        ]
    ) var wines: FetchedResults<Wine>

    var body: some View {
        List(wines, id: \.id) { wine in
            NavigationLink(destination: WineDetail(wine: wine)) {
                WineRow(wine: wine)
            }
        }
        .navigationBarTitle("Wines")
    }
}
Run Code Online (Sandbox Code Playgroud)

葡萄酒行:

struct WineRow: View {
    @ObservedObject var wine: Wine   // !! @ObserveObject is the key!!!

    var body: some View {
        HStack {
            Text(wine.name ?? "")
            Spacer()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这应该是公认的答案。它更简单并且更符合 SwiftUI 理念。 (6认同)
  • 关键是“@ObserveObject”,你震撼了!效果完美! (2认同)

Asp*_*eri 27

您需要一个发布者,它会在主视图中生成有关上下文更改和某些状态变量的事件,以强制在接收来自该发布者的事件时重建视图。
重要提示:视图构建器代码中必须使用状态变量,否则渲染引擎将不知道发生了什么变化。

这是对代码受影响部分的简单修改,提供了您需要的行为。

@State private var refreshing = false
private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)

var body: some View {
    List {
        ForEach(fetchedResults) { primary in
            NavigationLink(destination: SecondaryView(primary: primary)) {
                VStack(alignment: .leading) {
                    // below use of .refreshing is just as demo,
                    // it can be use for anything
                    Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
                    Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
                }
            }
            // here is the listener for published context event
            .onReceive(self.didSave) { _ in
                self.refreshing.toggle()
            }
        }
    }
    .navigationBarTitle("Primary List")
    .navigationBarItems(trailing:
        Button(action: {self.addNewPrimary()} ) {
            Image(systemName: "plus")
        }
    )
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢您的回答!但 @FetchRequest 应该对数据库中的更改做出反应。使用您的解决方案,视图将随着数据库中的每次保存而更新,无论涉及哪些项目。我的问题是如何让 @FetchRequest 对涉及数据库关系的更改做出反应。您的解决方案需要与 @FetchRequest 并行的第二个订阅者(NotificationCenter)。此外,还必须使用额外的假触发器 `+ (self.refreshing ? "" : "")`。也许 @Fetchrequest 本身并不是一个合适的解决方案? (3认同)
  • @Asperi 我接受你的回答。正如您所说,问题在于渲染引擎无法识别任何更改。使用对已更改对象的引用是不够的。更改后的变量必须在视图中使用。在身体的任何部位。即使用在列表的背景上也会起作用。我使用“RefreshView(toggle: Bool)”,其主体中有一个 EmptyView。使用“List {...}.background(RefreshView(toggle: self.refreshing))”将会起作用。 (3认同)
  • 我找到了更好的方法来强制列表刷新/重新获取,它在[SwiftUI:删除所有核心数据实体条目后列表不会自动更新](/sf/answers/4216161141/)中提供。万一。 (3认同)
  • @g-marc 答案是正确的[链接](/sf/answers/4446718531/) (2认同)

Joh*_*_Ye 13

另一种方法:使用 Publisher 和 List.id():

struct ContentView: View {
  /*
    @FetchRequest...
  */

  private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)  //the publisher
  @State private var refreshID = UUID()

  var body: some View {
      List {
        ...
      }
      .id(refreshID)
      .onReceive(self.didSave) { _ in   //the listener
          self.refreshID = UUID()
          print("generated a new UUID")
      }    
  }
}
Run Code Online (Sandbox Code Playgroud)

每次在上下文中调用 NSManagedObjects 的 save() 时,它都会为列表视图生成一个新的 UUID,并强制刷新列表视图。