Core 数据支持开箱即用的撤消/重做。但它的行为出乎意料。
为了使我的用户界面与我的模型保持同步,我发送了通知。我的用户界面接收通知消息并更新受影响的视图。
@objc(Entity)
class Entity : NSManagedObject
{
var title : String? {
get {
self.willAccessValueForKey("title")
let text = self.primitiveValueForKey("title") as? String
self.didAccessValueForKey("title")
return text
}
set {
self.willChangeValueForKey("title")
self.setPrimitiveValue(newValue, forKey: "title")
self.didChangeValueForKey("title")
self.sendNotification(self, key:"title")
print("title did change: (title)")
}
}
}
现在我想向应用程序添加撤消/重做支持。 核心数据有一个 NSUndoManager,所以我认为不需要额外的工作。或者至少不多。为了测试这个假设,我制作了一个包含两个NSTextFields和一个核心数据实体(恰当地命名为Entity(的测试应用程序。
NSViewController 子类可以访问实体实例(恰当地命名为 testObject(。我观察每一次击键,通过controlTextDidChange更新testObject:。
override func controlTextDidChange(obj: NSNotification)
{
guard let value = self.textField?.stringValue else { return }
self.testObject?.setValue(value, forKey: "title")
}
func valueDidChange(sender: Entity, key: String)
{
self.textField?.stringValue = sender.valueForKey("title") as? String ?? ""
}
托管对象内容和两个文本字段具有相同的 NSUndoManager(调试控制台中的相同指针(。
当我编辑 NSTextField 并执行撤消/重做操作时,NSTextField 和底层 NSManagedObject 属性都保持同步。不出所料。
但是,当我将焦点(第一个响应者(更改为第二个 NSTextField(没有任何编辑(并撤消/重做操作时,第一个 NSTextField 已(正确(更新,但基础 NSManagedObject 属性未更新。永远不会调用标题属性。
因此,第一个 NSTextField 和实体实例在撤消/重做操作后具有不同的值。
更新底层核心数据实例而不是用户界面对我来说更有意义。这里出了什么问题?
旁注:因为我正在观察 NSManagedObject 的任何更改,并且因为 controlTextDidChange: 正在发送通知(因为它更新了 NSManagedObject(,所以我收到了对 valueDidChange 的不必要的调用。有没有办法避免这种情况,或者我怎样才能改进我的架构?
我已经做了类似的事情,我发现最有效的方法是将UI控制器代码(MVC中的C(分成两个单独的"路径"。
一种通过侦听来自核心数据模型的通知来观察核心数据模型中的更改,NSManagedObjectContextObjectsDidChangeNotification
筛选出更改是否影响控制器 UI 并相应地调整显示。这个"路径"是盲目地跟随核心数据的变化,不需要与用户交互,也不需要撤消知识。
其他路径记录更改用户请求并相应地修改核心数据模型。例如,如果我们有一个步进器控件和一个旁边带有数字的标签。用户单击步进器。然后,控制器通过添加或减去一个属性来更新核心数据对象上的相关属性。这将自动生成核心数据模型的撤消操作。如果用户更改影响核心数据中的多个属性,则所有更改都将包装在撤消分组中。然后,对核心数据对象的此更改将触发另一个控制器路径以更新所有 UI 内容(示例中的标签(。
现在撤消会自动对面工作。通过在 MOC 撤消管理器上调用撤消,coreData 将还原对对象的更改,这将再次触发第一个路径,并且 UI 会自动跟随。
如果用户正在编辑文本字段,我通常不会费心逐个键击跟踪更改,而是仅在文本字段通知编辑已结束时捕获结果。使用这种方法,编辑后的撤消会删除上一个编辑会话中的所有更改,这通常是想要的。如果还需要在文本字段中撤消(例如键入aa和cmd-z以撤消第二个a(,则可以通过在文本字段编辑时向窗口提供另一个撤消管理器来实现 - 从而避免与核心数据操作相同的撤消堆栈中的所有击键撤消。
要记住的一件事是,coreData 有时会等待执行一些使事情看起来不同步的操作。在结束撤消分组之前调用 MOC 上的-processPendingChanges
将解决此问题。
要考虑的另一件事是您要撤消的内容。您是否希望能够撤消用户密钥条目或撤消数据模型中的更改。我发现有时两者兼而有之,但不是同时发现,因此我发现多个撤消管理器很有用,如前所述。保留文档撤消管理器,仅对数据模型进行更改,这是用户可能长期关心的事情。然后创建一个新的撤消管理器,并在用户处于编辑模式时使用它来跟踪单个按键。一旦用户通过离开文本字段或在对话框中按确定等方式确认他对整个编辑感到满意,请扔掉该撤消管理器并获得编辑的最终结果,并使用文档撤消管理器将其填充到核心数据中。对我来说,这两种类型的撤消是根本不同的,不应该在撤消堆栈中交织在一起。
下面是一些代码,首先是更改的侦听器示例(在收到NSManagedObjectContextObjectsDidChangeNotification
后调用(:
-(void)coreDataObjectsUpdated:(NSNotification *)notif {
// Filter for relevant change dicts
NSPredicate *isSectorObject = [NSPredicate predicateWithFormat: @"className == %@", @"Sector"];
NSSet *set;
BOOL changes = NO;
set = [[notif.userInfo objectForKey:NSDeletedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
else {
set = [[notif.userInfo objectForKey:NSInsertedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
else {
set = [[notif.userInfo objectForKey:NSUpdatedObjectsKey] filteredSetUsingPredicate:isSectorObject];
if (set.count > 0) {
changes = YES;
}
}
}
if (changes) {
[self.sectorTable reloadData];
}
}
这是创建复合撤消操作的示例,编辑在单独的工作表中完成,此代码段将所有更改作为具有名称的单个可撤消操作移动到核心数据对象中。
-(IBAction) editCDObject:(id)sender{
NSManagedObject *stk = [self.objects objectAtIndex:self.objectTableView.clickedRow];
[self.editSheetController EditObject:stk attachToWindow:self.window completionHandler: ^(NSModalResponse returnCode){
if (returnCode == NSModalResponseOK) { // Write back the changes else do nothing
NSUndoManager *um = self.moc.undoManager;
[um beginUndoGrouping];
[um setActionName:[NSString stringWithFormat:@"Edit object"]];
stk.overrideName = self.editSheetController.overrideName;
stk.sector = self.editSheetController.sector;
[um endUndoGrouping];
}
}];
}
希望这能给出一些想法。