UndoManager, NSManagedObjectContext and sheets
UndoManager, NSManagedObjectContext and sheets
- Subject: UndoManager, NSManagedObjectContext and sheets
- From: Arved von Brasch via Cocoa-dev <email@hidden>
- Date: Wed, 11 Nov 2020 08:41:21 +1100
Hello Cocoa list,
I’m wondering about the best approach to managing Undo/Redos when presenting a
sheet that allows multiple changes to the selected item in an
NSArrayController. My approach mostly works as I expect, but there are a few
edge cases that seem more difficult to deal with than it should be. When you
start fighting with the APIs, that’s usually a sign that you’re doing
something wrong.
I have a CoreData project (macOS 10.14 +), with an array of items managed by an
NSArrayController. When a single item is selected, a button is made available
that allows triggering this block of code. I create a custom NSWindowController
to display a sheet. That sheet presents an NSTableView where the user can
manage the to-many relationship. This could trigger changes to the file system
(I have blobs that are too large to store in the CoreData database and it’s
easiest to mange them in a folder under the control of the application). Any
changes to the underlying files I register the appropriate Undo/Redo
invocations, and undo and redo works as expected both inside the sheet, and as
a coalesced single undo/redo when the sheet ends.
I have two code smells, which may be related to my approach:
The first is that if no changes are made in the sheet, I still get an unnamed
Undo item. I got around this by disabling Undo Registration, undoing the
no-change and then reenabling Undos. This works fine, but doesn’t seem elegant.
I’ve found I need to open and close an Undo Grouping in order to properly
coalesce all the changes in the sheet into a single Undo operation, but opening
and closing an Undo Grouping seems to be enough to generate the Unnamed Undo
item. Is there a better approach here? The Sheet creates a child Managed Object
Context with it’s own, new UndoManager()
The second is about registering Undo invocations. When the user deletes an item
in the Sheet view, I move the managed file blob to the trash and register the
appropriate Undo invocations to recover it with both the Sheet’s UndoManager,
which belongs to a child Context that manages the sheet, and also register it
with the parent context’s managedObjectContext. If I don't do this, then the
path to the trashed item is lost when the child Context is released when the
sheet ends. This does mean I need to specifically handle the Redo if the User
selects Redo after Undoing all changes in the Sheet view. This isn’t an
insurmountable problem, but again, seems inelegant.
From my NSArrayController subclass:
@IBAction func details(_ sender: AnyObject) {
if managedObjectContext!.hasChanges {
try? managedObjectContext?.save()
}
managedObjectContext?.undoManager?.beginUndoGrouping()
details = DetailsSheetController(managedObject: selectedObjects![0] as!
NSManagedObject)
NSApp.mainWindow?.beginSheet(details!.window!, completionHandler: {
modalResponse in
if self.managedObjectContext!.hasChanges {
self.managedObjectContext?.undoManager?.setActionName(NSLocalizedString("Edit
Details", comment: "EDIT_DETAILS"))
try? self.managedObjectContext?.save()
self.managedObjectContext?.undoManager?.endUndoGrouping()
}
else {
self.managedObjectContext?.undoManager?.endUndoGrouping()
self.managedObjectContext?.undoManager?.disableUndoRegistration()
self.managedObjectContext?.undoManager?.undo()
self.managedObjectContext?.undoManager?.enableUndoRegistration()
}
self.details = nil
})
}
From my NSManagedObject subclass, hooked in when the User deletes one of the
blob objects:
private func retireFile(from: URL) {
do {
var trashedPath: NSURL?
try sharedFileAccess.trashFile(at: from, trashedPath: &trashedPath)
// Wrapper for FileManager - has extra debug
managedObjectContext?.undoManager?.registerUndo(withTarget: self) {
_ in
self.restoreFile(from: trashedPath! as URL, to: from)
}
if let _ = self.managedObjectContext?.parent {
let object = self.managedObjectContext?.parent?.object(with:
self.objectID) as! File
object.managedObjectContext?.undoManager?.registerUndo(withTarget: object) { _
in
object.restoreFile(from: trashedPath! as URL, to: from)
}
}
} catch {
NSApp.presentError(AppError.fileInaccessible(name:
from.lastPathComponent).error(), modalFor: NSApp.mainWindow!, delegate: nil,
didPresent: nil, contextInfo: nil)
}
}
This code works under the use cases we’ve been testing under, it just seems
inelegant. I suspect I’m missing something obvious from the documentation of
UndoManager. I’d appreciate any insights anyone has.
Kind regards,
Arved
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden