核心数据支持开箱即用的撤消/重做。但它表现得出乎意料。
为了让我的用户界面与我的模型保持同步,我会发送通知。我的用户界面收到通知消息并更新受影响的视图。
@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子类可以访问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的不必要的调用。有没有诀窍可以避免这种情况,或者我如何改进我的架构呢?
答案 0 :(得分:2)
我做了类似的事情,我发现最好的方法是将UI控制器代码(MVC中的C)分成两个独立的“路径”。
通过侦听来自核心数据模型NSManagedObjectContextObjectsDidChangeNotification
的通知来观察核心数据模型中的更改,如果更改影响控制器UI并相应地调整显示,则会过滤掉。这个“路径”盲目地跟随coreData的变化,不需要与用户交互,也不需要撤消知识。
其他路径记录更改用户请求并相应地修改核心数据模型。例如,如果我们有一个步进控件和一个旁边有数字的标签。用户单击步进器。然后,控制器通过添加或减去一个来更新核心数据对象上的相关属性。这会使用核心数据模型自动生成撤消操作。如果用户更改影响核心数据中的多个属性,则所有更改都将包含在撤消分组中。然后,对核心数据对象的此更改将触发另一个控制器路径以更新所有UI事物(示例中的标签)。
现在撤消工作自动对面。通过在MOC撤消管理器上调用undo,coreData将还原对象的更改,这将再次触发第一个路径,并自动跟随UI。
如果用户正在编辑文本字段,我通常不会通过按键跟踪更改按键,而只会在文本字段通知编辑结束时捕获结果。使用此方法,编辑后撤消会删除上一个编辑会话中通常所需的所有更改。如果还要在文本字段内撤消(例如,键入aa和cmd-z以撤消第二个a),则可以通过在文本字段编辑时向窗口提供另一个撤消管理器来实现 - 从而避免在同一个撤消中的所有按键撤消堆栈作为核心数据操作。
要记住的一件事是,coreData有时会等待执行一些使事情看起来不同步的操作。在结束撤消分组之前在MOC上调用-processPendingChanges
将解决此问题。
要考虑的另一件事是你要撤消的内容。您是否希望能够撤消用户密钥条目或撤消数据模型中的更改。我有时发现两者但不是同时发现因此我发现有多个撤销管理器,如前所述。保持doc撤消管理器只对数据模型进行更改,这是用户可能长期关注的事情。然后创建一个新的撤消管理器,并在用户处于编辑模式时使用它来跟踪单个按键。一旦用户通过离开文本字段或在对话框中按OK确认他对编辑作为一个整体感到满意,然后扔掉该撤消管理器并获得编辑的最终结果并使用doc将其填充到核心数据中撤消经理。对我来说,这两种类型的undos基本上是不同的,不应该在撤销堆栈中交织在一起。
下面是一些代码,首先是更改侦听器的示例(在收到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];
}
}];
}
希望这能带来一些想法。