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 (Cocoa-dev@lists.apple.com)

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:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to