核心数据支持开箱即用的撤消/重做。但是它的表现出乎意料。
为了使我的用户界面与模型保持同步,我发出了通知。我的用户界面收到通知消息并更新受影响的视图。
@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,因此我认为不需要其他工作。或至少不多。为了测试这个假设,我制作了一个具有两个NSTextField和一个核心数据实体(适当命名为Entity)的测试应用。
NSViewController子类有权访问Entity实例(恰当地命名为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 ?? ""
}
managedObjectContent和两个textField具有相同的NSUndoManager(调试控制台中的相同指针)。
当我编辑NSTextField并执行撤消/重做操作时,NSTextField和基础NSManagedObject属性都保持同步。如预期的那样。
但是,当我将焦点(第一响应者)更改为第二个NSTextField(不进行任何编辑)和撤消/重做操作时,第一个NSTextField被(正确)更新了,但基础的NSManagedObject属性却没有更新。 title属性永远不会被调用。
因此,在撤消/重做操作之后,第一个NSTextField和Entity实例具有不同的值。
对我而言,更新基础核心数据实例而不是用户界面将更为有意义。这是怎么了?
旁注:因为我正在观察NSManagedObject的任何更改,并且因为controlTextDidChange:正在发出通知(因为其更新了NSManagedObject),所以我不必要地调用了valueDidChange。有什么技巧可以避免这种情况,或者如何改善我的体系结构?
最佳答案
我做了类似的事情,发现最有效的方法是将UI控制器代码(MVC中的C)分成两个单独的“路径”。
一种方法是通过侦听来自核心数据模型NSManagedObjectContextObjectsDidChangeNotification
的通知来观察核心数据模型的变化,如果更改影响了控制器UI并相应地调整了显示,则该通知将被滤除。这个“路径”盲目地跟随coreData的变化,不需要与用户交互,也不需要撤消知识。
另一个路径记录更改了用户请求,并相应地修改了核心数据模型。例如,如果我们有一个步进控件和一个旁边带有数字的标签。用户单击步进器。然后,控制器通过添加或减去一个来更新核心数据对象上的相关属性。这将使用核心数据模型自动生成撤消操作。如果用户更改影响核心数据中的多个属性,则所有更改都将打包在撤消分组中。然后,对核心数据对象的更改将触发另一个控制器路径来更新所有UI内容(示例中的标签)。
现在,撤消功能会自动相反。通过在MOC上调用undo,撤消管理器coreData将还原对对象的更改,该对象将再次触发第一条路径,并且UI将自动跟随。
如果用户正在编辑文本字段,我通常不会打扰跟踪击键的更改,而仅在文本字段通知编辑已结束时才捕获结果。使用此方法,编辑后撤消将删除通常需要的上一个编辑会话中的所有更改。如果还希望在文本字段内进行撤消(例如,键入aa和cmd-z来撤消第二个a),可以通过在编辑文本字段时向窗口提供另一个撤消管理器来实现-从而避免同一撤消操作中的所有按键撤消堆栈作为核心数据操作。
要记住的一件事是,coreData有时确实会等待执行一些使事情看起来不同步的动作。在结束撤消分组之前在MOC上调用-processPendingChanges
可以解决此问题。
要考虑的另一件事是您要撤消的操作。您是否希望能够撤消用户键条目或撤消数据模型中的更改。我发现有时两者都存在,但不是同时出现,因此我发现多个撤消管理器非常有用,如前所述。保留文档撤消管理器仅用于更改数据模型,这是用户可能长期关注的事情。然后制作一个新的撤消管理器,并在用户处于编辑模式时使用它来跟踪各个按键。一旦用户通过离开文本字段或在对话框中按OK等确认自己对整个编辑感到满意,就丢弃该撤消管理器并获得编辑的最终结果,并使用doc将其填充到核心数据中撤消经理。对我而言,这两种类型的撤消从根本上是不同的,不应在撤消堆栈中交织在一起。
以下是一些代码,第一个示例是更改的侦听器(在收到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];
}
}];
}
希望这能带来一些想法。
关于cocoa - 如何将NSUndoManager与核心数据结合使用,并使用户界面和模型保持同步?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/35322590/