I looked at a simple implementation of Core Data's deleteObject() method recently, but for my Swift and Metal based reaction diffusion application, I want something a little more user friendly: if my user inadvertently deletes an item, I want them to be able to undo the deletion without resorting to unnecessary dialogs and stopping the proceedings with idiocy.
The first step is to replace the default 'cut, copy, paste' context menu that comes for free with the UICollectionView with my own action sheet. To that end, I'm not using any of the menu or action related collectionView() methods that are part of UICollectionViewDelegate: I'm adding my own long press gesture recogniser to the UICollectionView instance in my BrowseAndLoadController:
let longPress = UILongPressGestureRecognizer(target: self, action: "longPressHandler:")
collectionViewWidget.addGestureRecognizer(longPress)
When the user first touches one of the collection view's cells - before the cell is selected - I want to note a reference to it inside a tuple containing its instance and index path. In the collection view, this is a highlight, so I use the didHighlightCellAtIndexPath implementation of the delegate method, collectionView():
var longPressTarget: (cell: UICollectionViewCell, indexPath: NSIndexPath)?
[...]
func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath)
{
longPressTarget = (cell: self.collectionView(collectionViewWidget, cellForItemAtIndexPath: indexPath), indexPath: indexPath)
}
When the long hold gesture begins (which after the half second press, not at the first touch), my longPressHandler() is invoked. Here, I dynamically create an action sheet that either displays a delete option or, if the item has already been marked for a delete, an undelete option. I use the frame of the highlighted cell to up the action sheet:
func longPressHandler(recognizer: UILongPressGestureRecognizer)
{
if recognizer.state==UIGestureRecognizerState.Began
{
iflet _longPressTarget = longPressTarget
{
let entity = dataprovider[_longPressTarget.indexPath.item]
let contextMenuController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet)
let deleteAction = UIAlertAction(title: entity.pendingDelete ? "Undelete" : "Delete", style: UIAlertActionStyle.Default, handler: togglePendingDelete)
contextMenuController.addAction(deleteAction)
iflet popoverPresentationController = contextMenuController.popoverPresentationController
{
popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirection.Down
popoverPresentationController.sourceRect = _longPressTarget.cell.frame.rectByOffsetting(dx: collectionViewWidget.frame.origin.x, dy: collectionViewWidget.frame.origin.y-collectionViewWidget.contentOffset.y)
popoverPresentationController.sourceView = view
presentViewController(contextMenuController, animated: true, completion: nil)
}
}
}
}
When the user deletes or undeletes, the action invokes togglePendingDelete() and, since the tuple, longPressTarget, holds the item of interest, I simply toggle its pendingDelete Boolean property.
The BrowseAndLoadController has a showDeleted property which indicates whether its collection view should display the items with a true pendingDelete, so togglePendingDelete() either reloads the changed item or uses the deleteItemsAtIndexPath() to animate the removal of a deleted cell:
func togglePendingDelete(value: UIAlertAction!) -> Void
{
iflet _longPressTarget = longPressTarget
{
let targetEntity = dataprovider[_longPressTarget.indexPath.item]
targetEntity.pendingDelete = !targetEntity.pendingDelete
ifshowDeleted
{
// if we're displaying peniding deletes....
collectionViewWidget.reloadItemsAtIndexPaths([_longPressTarget.indexPath])
}
else
{
// if we're deleting
if targetEntity.pendingDelete
{
let targetEntityIndex = find(dataprovider, targetEntity)
dataprovider.removeAtIndex(targetEntityIndex!)
collectionViewWidget.deleteItemsAtIndexPaths([_longPressTarget.indexPath])
}
}
}
}
The toggle switch that shows or hides the items pending a delete toggles the showDeleted Boolean and, in the case of hiding, uses a simple filter closure to remove the unwanted items from view:
ifshowDeleted
{
dataprovider = fetchResults
}
else
{
dataprovider = fetchResults.filter({!$0.pendingDelete})
}
In this screen shot, you can see items pending delete being show slightly dimmed out and displaying 'undelete' on a long hold gesture:
The final piece of the puzzle is to actually delete these items. I do this in my AppDelegate class inside applicationWillTerminate(). Here, I loop over all the items inside the managed object context and invoke deleteObject() on each one with its pendingDelete set to true:
func applicationWillTerminate(application: UIApplication)
{
let fetchRequest = NSFetchRequest(entityName: "ReactionDiffusionEntity")
iflet _managedObjectContext = managedObjectContext
{
iflet fetchResults = _managedObjectContext.executeFetchRequest(fetchRequest, error: nil) as? [ReactionDiffusionEntity]
{
for entity in fetchResults
{
if entity.pendingDelete
{
_managedObjectContext.deleteObject(entity)
}
}
}
}
self.saveContext()
}
All the source code for this project is available at my GitHub repository here.
Addendum: looks like applicationWillTerminate doesn't always get called (see this StackOverflow post). So, I've moved the code to delete items that are pending delete into its own function that gets invoked on applicationDidEnterBackground() andapplicationWillTerminate(). I'm considering timestamping the user's delete action to only properly delete items that the user marked for deletion after a period of time (such as an hour).