假设我有两个实体,员工和部门。部门与员工有多对多的关系,许多员工可以在每个部门,但每个员工只属于一个部门。我希望在tableview中显示所有员工,这些员工使用NSFetchedResultsController按照属于他们所属部门的属性的数据进行排序。问题是,当部门对象收到更改时,我希望我的表更新,就像员工的常规属性更改一样,但FetchedResultsController似乎不跟踪相关对象。我通过以下方式部分通过了这个问题:
for (Employee* employee in department.employees) {
[employee willChangeValueForKey:@"dept"];
}
/* Make Changes to department object */
for (Employee* employee in department.employees) {
[employee didChangeValueForKey:@"dept"];
}
这显然不是理想的,但它会导致基于员工的FRC委托方法didChangeObject被调用。我现在留下的真正问题是在跟踪员工对象的FRC中进行排序:
NSEntityDescription *employee = [NSEntityDescription entityForName:@"Employee" inManagedObjectContext:self.managedObjectContext];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"department.someProperty" ascending:NO];
这很有效,并且在第一次调用时对员工进行了正确的排序,问题是当我对某个部门进行更改时,someProperty应该更改我的员工表的排序没有任何反应。有没有什么好方法让我的员工FRC跟踪关系中的变化?特别是我只需要一些方法让它在排序基于相关属性时更新排序。我查看了一些类似的问题,但未能找到满意的解决方案。
答案 0 :(得分:18)
NSFetchedResultsController
实际上只是为了一次只能观看一个实体。您的设置虽然有意义,但它有点超出了NSFetchedResultsController
目前能够独立观看的内容。
我的建议是建立自己的观察者。你可以将它基于我在GitHub上设置的ZSContextWatcher,或者你可以使它更简单。
基本上,您希望查看NSManagedObjectContextDidSaveNotification
个帖子,然后在发生包含您的部门实体的内容时重新加载您的表格。
我还建议向Apple提交rdar并要求对NSFetchedResultsController
进行改进。
答案 1 :(得分:1)
这是NSFetchedResultsController
的已知限制:它仅监视您实体属性的更改,而不监视其关系属性的更改。但是您的用例是完全有效的,这是如何克服它。
工作原理
浏览了许多可能的解决方案之后,现在我只创建两个NSFetchedResultsController
:第一个Employee
(在您的情况下为Department
),另一个用于监视上述关系中的实体({ {1}})。然后,当以更新Department
FRC的方式更新Employee
实例时,我只是使用Employee
协议伪造了附属NSFetchedResultsControllerDelegate
实例的更改。请注意,受监视的Department
属性必须是其NSSortDescriptors
的{{1}}的一部分,才能正常工作。
示例代码
在您的示例中,如果可以这样工作:
在您的视图控制器中:
NSFetchedResultsController
还要确保在类声明中声明符合var employeesFetchedResultsController:NSFetchedResultsController!
var departmentsFetchedResultsController:NSFetchedResultsController!
。
在NSFetchedResultsControllerDelegate
中:
viewDidLoad()
在override func viewDidLoad() {
super.viewDidLoad()
// [...]
employeesFetchedResultsController = newEmployeesFetchedResultsController()
departmentsFetchedResultsController = newDepartmentsFetchedResultsController()
// [...]
}
创建中:
departmentsFetchedResultsController
在func newDepartmentsFetchedResultsController() {
// [specify predicate, fetchRequest, etc. as usual ]
let monitoredPropertySortDescriptor:NSSortDescriptor = NSSortDescriptor(key: "monitored_property", ascending: true)
request.sortDescriptors = [monitoredPropertySortDescriptor]
// continue with performFetch, etc
}
方法中:
这就是魔术的作用:
NSFetchedResultsControllerDelegate
每位受影响员工的部门虚假更新都会触发 func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if controller == departmentsFetchedResultsController {
switch(type){
case .insert, .delete, .update:
managedObjectContext.performAndWait {
let department = anObject as! Department
for employee in (department.employees ?? []) {
// we fake modifying each Employee so the FRC will refresh itself.
let employee = employee as! Employee // pure type casting
employee.department = department
}
}
break
default:
break
}
}
}
的正确更新。
答案 2 :(得分:0)
快速
由于NSFetchedResultsController一次是为一个实体设计的,因此您必须侦听NSManagedObjectContextObjectsDidChangeNotification才能获得所有实体关系更改的通知。
这里是一个例子:
//UITableViewController
//...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChangeHandler(notification:)), name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .NSManagedObjectContextObjectsDidChange, object: mainManagedContext)
}
@objc fileprivate func managedObjectsDidChangeHandler(notification: NSNotification) {
tableView.reloadData()
}
//...
答案 3 :(得分:0)
SwiftUI
我还没有在 SwiftUI 中看到直接解决这个问题的帖子。在尝试了许多帖子中概述的解决方案并试图避免编写自定义控制器后,使其在 SwiftUI 中工作的单一因素(这是 harrouet 上一篇文章的一部分(谢谢!))是:
利用 FetchRequest
on Employee
。
如果您关心每个部门的员工数量,那么虚假的关系更新在 SwiftUI 中并没有什么不同。也没有任何 willChangeValue
或 didChangeValue
语句。实际上,willChangeValue
导致了我的崩溃。
这是一个有效的设置:
import CoreData
struct SomeView: View {
@FetchRequest var departments: FetchedResults<Department>
// The following is only used to capture department relationship changes
@FetchRequest var employees: FetchedResults<Employee>
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: employees.count)
}
}
//.id(employees.count) does not trigger view updates
}
}
struct DepartmentView: View {
var department: Department
// Not used, but necessary for the department view to be refreshed upon employee updates
var totalEmployeeCount: Int
var body: some View {
// The department's employee count will be refreshed when, say,
// a new employee is created and added to the department
Text("\(department) has \(department.employees.count) employee(s)")
}
}
我不知道这是否解决了 CoreData
关系无法传播到视图的所有潜在问题,如果员工人数非常多,它可能会出现效率问题,但它对我有用。
另一种方法也可以在不获取所有员工的情况下建立正确的员工数量(这可能会解决上述代码段的效率问题)是创建对 NSFetchRequestResultType.countResultType
类型的 {{1} }:
FetchRequest
主视图变为:
// Somewhere in a DataManager:
import CoreData
final class DataManager {
static let shared = DataManager()
let persistenceController: PersistenceController
let context: NSManagedObjectContext!
init(persistenceController: PersistenceController = .shared) {
self.persistenceController = persistenceController
self.context = persistenceController.container.viewContext
}
func employeeCount() -> Int {
var count: Int = 0
context.performAndWait {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Employee")
fetchRequest.predicate = nil
fetchRequest.resultType = NSFetchRequestResultType.countResultType
do {
count = try context.count(for: fetchRequest)
} catch {
fatalError("error \(error)")
}
}
return count
}
}
同样,这可能无法解决所有可能的关系依赖,但它希望通过考虑 SwiftUI 视图中的各种类型的 import CoreData
struct SomeView: View {
@FetchRequest var departments: FetchedResults<Department>
// No @FetchRequest for all employees
var dataManager = DataManager.shared
var body: some View {
List {
ForEach(departments) { department in
DepartmentView(department: department,
// Required: pass some dependency on employees to trigger view updates
totalEmployeeCount: dataManager.employeeCount())
}
}
//.id(dataManager.employeeCount()) does not trigger view updates
}
}
// DepartmentView stays the same.
依赖来提示视图更新。
注意,FetchRequest
需要 NOT 成为在视图中被观察到的 DataManager
才能使其工作。