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