【问题标题】:How to use NSUndoManager with coredata and keep user interace and model in sync?如何将 NSUndoManager 与核心数据一起使用并保持用户界面和模型同步?
【发布时间】:2016-05-21 05:46:57
【问题描述】:

核心数据支持开箱即用的撤消/重做。但它的行为出乎意料。

为了使我的用户界面与我的模型保持同步,我会发送通知。我的用户界面收到通知消息并更新受影响的视图。

@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 和一个核心数据实体(恰当地命名为实体)的测试应用程序。

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 ?? ""

}

managedObjectContent 和两个 textField 具有相同的 NSUndoManager(调试控制台中的相同指针)。

当我编辑 NSTextField 并执行撤消/重做操作时,NSTextField 和底层 NSManagedObject 属性保持同步。正如预期的那样。

但是当我将焦点(第一响应者)更改为第二个 NSTextField(没有任何编辑)和撤消/重做操作时,第一个 NSTextField (正确)更新但底层 NSManagedObject 属性没有。 title 属性永远不会被调用。

所以第一个 NSTextField 和 Entity 实例在撤消/重做操作后具有不同的值。

更新底层核心数据实例而不是用户界面对我来说更有意义。这里出了什么问题?

旁注:因为我正在观察 NSManagedObject 的任何更改,并且因为 controlTextDidChange: 正在发送通知(因为它正在更新 NSManagedObject),所以我收到了对 valueDidChange 的不必要调用时间>。有什么技巧可以避免这种情况,或者我该如何改进我的架构?

【问题讨论】:

  • 你试过回滚吗?
  • NSObjectController 和绑定可以为你做到这一点。您唯一要做的就是设置对象控制器的内容。
  • 与您的问题无关,但与其做的所有获取/设置的东西,不如直接做:didSet { self.sendNotification(...) }
  • 哦,coredata 仍然停留在 obj-c 中,并且对 swift enums 和 struct 的支持并不是那么好。所以我有支持这些类型的自定义设置器/获取器。这个 sn-p 可能只是复制/粘贴这些功能来澄清我的问题。具体我记不太清了。但是,是的,didSet 在正常情况下也可以正常工作。这取决于您架构的其余部分。

标签: cocoa user-interface core-data swift2 nsundomanager


【解决方案1】:

我做过类似的事情,我发现最好的方法是将 UI 控制器代码(MVC 中的 C)分成两个单独的“路径”。

通过侦听来自核心数据模型NSManagedObjectContextObjectsDidChangeNotification 的通知来观察核心数据模型的变化,过滤出变化是否影响控制器 UI 并相应地调整显示。这个“路径”是盲目跟随 coreData 变化的,不需要与用户交互,也不需要 undo 知识。

其他路径记录改变用户请求并相应修改核心数据模型。例如,如果我们有一个步进控件和一个旁边带有数字的标签。用户单击步进器。然后控制器通过加或减1来更新核心数据对象的相关属性。这会使用核心数据模型自动生成撤消操作。如果用户更改影响核心数据中的多个属性,则所有更改都包含在撤消分组中。然后对核心数据对象的这种更改将触发另一个控制器路径来更新所有 UI 事物(示例中的标签)。

现在撤消会自动相反。通过在 MOC 撤消管理器上调用 undo,coreData 将恢复对对象的更改,这将再次触发第一个路径,并且 UI 会自动跟随。

如果用户正在编辑文本字段,我通常不会费心跟踪按键的更改,而是仅在文本字段通知编辑结束时捕获结果。使用这种方法,编辑后撤消会删除先前编辑会话中的所有更改,这通常是想要的。如果还需要在文本字段中撤消(例如,键入 aa 和 cmd-z 以撤消第二个 a),可以通过在文本字段正在编辑时向窗口提供另一个撤消管理器来实现 - 从而避免在同一撤消中的所有击键撤消堆栈作为核心数据动作。

要记住的一点是,coreData 有时会等待执行一些使事情看起来不同步的操作。在结束撤消分组之前在 MOC 上调用 -processPendingChanges 将解决此问题。

要考虑的另一件事是您要撤消什么。您是否希望能够撤消用户密钥条目或撤消数据模型中的更改。我有时会发现两者但不是同时发现,因此我发现多个撤消管理器很有用,如前所述。保留文档撤消管理器仅用于对数据模型的更改,这是用户可能长期关心的事情。然后创建一个新的撤消管理器,并在用户处于编辑模式时使用它来跟踪单个按键。一旦用户通过离开文本字段或在对话框中按 OK 等确认他对整个编辑感到满意,就丢弃该撤消管理器并使用文档获得编辑的最终结果并将其填充到核心数据中撤消管理器。对我来说,这两种类型的撤消是根本不同的,不应该在撤消堆栈中交织在一起。

下面是一些代码,首先是更改监听器的示例(在收到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];
}

}

这是创建复合撤消操作的示例,编辑在单独的工作表中完成,此 sn-p 将所有更改作为具有名称的单个可撤消操作移动到核心数据对象中。

-(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];
    }
}];

}

希望这能提供一些想法。

【讨论】:

  • 谢谢。这种方法要好得多。关于如何避免两次更新文本字段的任何想法?现在它会因为击键(或结束编辑)和通知而更新一次。
  • 不客气。不,我真的看不到文本字段的第二次更新的方法,因为核心数据更改正在冒泡。我想您可以测试内容是否已经等于新值,但是为什么要麻烦呢?就我而言,我通常不允许直接在主 UI 中进行编辑。相反,双击会触发覆盖表的显示,其中包含所有属性的编辑控件,然后我将所有内容一次性保存到工作表末尾的核心数据。这样我真的需要通知,因为在此之前主 UI 不会改变。
猜你喜欢
  • 2015-06-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-08
  • 1970-01-01
  • 2013-05-28
  • 1970-01-01
  • 2019-10-28
相关资源
最近更新 更多