Editing Tables with Xamarin.iOS
Table editing features are enabled by overriding methods in a UITableViewSource
subclass. The simplest editing behavior is the
swipe-to-delete gesture that can be implemented with a single method override.
More complex editing (including moving rows) can be done with the table in edit
mode.
Swipe to Delete
The swipe to delete feature is a natural gesture in iOS that users expect.
There are three method overrides that affect the swipe gesture to show a Delete button in a cell:
- CommitEditingStyle – The table source detects if this method is overridden and automatically enables the swipe-to-delete gesture. The method’s implementation should call
DeleteRows
on theUITableView
to cause the cells to disappear, and also remove the underlying data from your model (for example, an array, dictionary or database). - CanEditRow – If CommitEditingStyle is overridden, all rows are assumed to be editable. If this method is implemented and returns false (for some specific rows, or for all rows) then the swipe-to-delete gesture will not be available in that cell.
- TitleForDeleteConfirmation – Optionally specifies the text for the Delete button. If this method is not implemented the button text will be “Delete”.
These methods are implemented in the TableSource
class follows:
public override void CommitEditingStyle (UITableView tableView, UITableViewCellEditingStyle editingStyle, Foundation.NSIndexPath indexPath)
{
switch (editingStyle) {
case UITableViewCellEditingStyle.Delete:
// remove the item from the underlying data source
tableItems.RemoveAt(indexPath.Row);
// delete the row from the table
tableView.DeleteRows (new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Fade);
break;
case UITableViewCellEditingStyle.None:
Console.WriteLine ("CommitEditingStyle:None called");
break;
}
}
public override bool CanEditRow (UITableView tableView, NSIndexPath indexPath)
{
return true; // return false if you wish to disable editing for a specific indexPath or for all rows
}
public override string TitleForDeleteConfirmation (UITableView tableView, NSIndexPath indexPath)
{ // Optional - default text is 'Delete'
return "Trash (" + tableItems[indexPath.Row].SubHeading + ")";
}
For this example the UITableViewSource
has been updated to use a List<TableItem>
(instead of a string array) as the data source because it supports adding and deleting items from the collection.
Edit Mode
When a table is in edit mode the user sees a red ‘stop’ widget on each row, which reveals a Delete button when touched. The table also displays a ‘handle’ icon to indicate that the row can be dragged to change the order. The TableEditMode sample implements these features as shown.
There are a number of different methods on UITableViewSource
that affect a table’s edit mode behavior:
- CanEditRow – whether each row can be edited. Return false to prevent both swipe-to-delete and deletion while in edit mode.
- CanMoveRow – return true to enable the move ‘handle’ or false to prevent moving.
- EditingStyleForRow – when the table is in edit mode, the return value from this method determines whether the cell displays the red deletion icon or the green add icon. Return
UITableViewCellEditingStyle.None
if the row should not be editable. - MoveRow – called when a row is moved so that the underlying data structure can be modified to match the data as it is displayed in the table.
The implementation for the first three methods is relatively straight forward – unless you
wish to use the indexPath
to change the behavior of specific rows,
just hardcode the return values for the entire table.
public override bool CanEditRow (UITableView tableView, NSIndexPath indexPath)
{
return true; // return false if you wish to disable editing for a specific indexPath or for all rows
}
public override bool CanMoveRow (UITableView tableView, NSIndexPath indexPath)
{
return true; // return false if you don't allow re-ordering
}
public override UITableViewCellEditingStyle EditingStyleForRow (UITableView tableView, NSIndexPath indexPath)
{
return UITableViewCellEditingStyle.Delete; // this example doesn't support Insert
}
The MoveRow
implementation is a little more complicated because
it needs to alter the underlying data structure to match the new order. Because
the data is implemented as a List
the code below deletes the data item at its
old location and inserts it at the new location. If the data was stored in a
SQLite database table with an ‘order’ column (for example), this method
would instead need to perform some SQL operations to reorder the numbers in that
column.
public override void MoveRow (UITableView tableView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
{
var item = tableItems[sourceIndexPath.Row];
var deleteAt = sourceIndexPath.Row;
var insertAt = destinationIndexPath.Row;
// are we inserting
if (destinationIndexPath.Row < sourceIndexPath.Row) {
// add one to where we delete, because we're increasing the index by inserting
deleteAt += 1;
} else {
// add one to where we insert, because we haven't deleted the original yet
insertAt += 1;
}
tableItems.Insert (insertAt, item);
tableItems.RemoveAt (deleteAt);
}
Finally, to get the table into edit mode, the Edit
button needs to call SetEditing
like this
table.SetEditing (true, true);
and when the user is finished editing, the Done button should turn editing mode off:
table.SetEditing (false, true);
Row Insertion Editing Style
Row insertion from within the table is an uncommon user interface – the main example in the standard iOS apps is the Edit Contact screen. This screenshot shows how the row insertion functionality works – in edit mode there is an additional row that (when clicked) inserts additional rows into the data. When editing is complete, the temporary (add new) row is removed.
There are a number of different methods on UITableViewSource
that affect a table’s edit mode behavior. These methods have been implemented
as follows in the example code:
- EditingStyleForRow – returns
UITableViewCellEditingStyle.Delete
for the rows containing data, and returnsUITableViewCellEditingStyle.Insert
for the last row (which will be added specifically to behave as an insert button). - CustomizeMoveTarget – While the user is moving a cell the return value from this optional method can override their choice of location. This means you can prevent them from ‘dropping’ the cell in certain positions – such as this example that prevents any row from being moved after the (add new) row.
- CanMoveRow – return true to enable the move ‘handle’ or false to prevent moving. In the example, the last row has the move ‘handle’ hidden because it is intended to server as an insert button only.
We also add two custom methods to add the ‘insert’ row and then remove it again when no longer required. They are called from the Edit and Done buttons:
- WillBeginTableEditing – When the Edit button is touched it calls
SetEditing
to put the table in edit mode. This triggers the WillBeginTableEditing method where we display the (add new) row at the end of the table to act as an ‘insert button’. - DidFinishTableEditing – When the Done button is touched
SetEditing
is called again to turn off edit mode. The example code removes the (add new) row from the table when editing is no longer required.
These method overrides are implemented in the sample file TableEditModeAdd/Code/TableSource.cs:
public override UITableViewCellEditingStyle EditingStyleForRow (UITableView tableView, NSIndexPath indexPath)
{
if (tableView.Editing) {
if (indexPath.Row == tableView.NumberOfRowsInSection (0) - 1)
return UITableViewCellEditingStyle.Insert;
else
return UITableViewCellEditingStyle.Delete;
} else // not in editing mode, enable swipe-to-delete for all rows
return UITableViewCellEditingStyle.Delete;
}
public override NSIndexPath CustomizeMoveTarget (UITableView tableView, NSIndexPath sourceIndexPath, NSIndexPath proposedIndexPath)
{
var numRows = tableView.NumberOfRowsInSection (0) - 1; // less the (add new) one
if (proposedIndexPath.Row >= numRows)
return NSIndexPath.FromRowSection(numRows - 1, 0);
else
return proposedIndexPath;
}
public override bool CanMoveRow (UITableView tableView, NSIndexPath indexPath)
{
return indexPath.Row < tableView.NumberOfRowsInSection (0) - 1;
}
These two custom methods are used to add and remove the (add new) row when the table’s editing mode is enabled or disabled:
public void WillBeginTableEditing (UITableView tableView)
{
tableView.BeginUpdates ();
// insert the 'ADD NEW' row at the end of table display
tableView.InsertRows (new NSIndexPath[] {
NSIndexPath.FromRowSection (tableView.NumberOfRowsInSection (0), 0)
}, UITableViewRowAnimation.Fade);
// create a new item and add it to our underlying data (it is not intended to be permanent)
tableItems.Add (new TableItem ("(add new)"));
tableView.EndUpdates (); // applies the changes
}
public void DidFinishTableEditing (UITableView tableView)
{
tableView.BeginUpdates ();
// remove our 'ADD NEW' row from the underlying data
tableItems.RemoveAt ((int)tableView.NumberOfRowsInSection (0) - 1); // zero based :)
// remove the row from the table display
tableView.DeleteRows (new NSIndexPath[] { NSIndexPath.FromRowSection (tableView.NumberOfRowsInSection (0) - 1, 0) }, UITableViewRowAnimation.Fade);
tableView.EndUpdates (); // applies the changes
}
Finally, this code instantiates the Edit and Done buttons, with lambdas that enable or disable edit mode when they’re touched:
done = new UIBarButtonItem(UIBarButtonSystemItem.Done, (s,e)=>{
table.SetEditing (false, true);
NavigationItem.RightBarButtonItem = edit;
tableSource.DidFinishTableEditing(table);
});
edit = new UIBarButtonItem(UIBarButtonSystemItem.Edit, (s,e)=>{
if (table.Editing)
table.SetEditing (false, true); // if we've half-swiped a row
tableSource.WillBeginTableEditing(table);
table.SetEditing (true, true);
NavigationItem.LeftBarButtonItem = null;
NavigationItem.RightBarButtonItem = done;
});
This row insertion UI pattern is not used very often, however you can also
use the UITableView.BeginUpdates
and EndUpdates
methods to animate the insertion
or removal of cells in any table. The rule for using those methods is that the
difference in value returned by RowsInSection
between the BeginUpdates
and
EndUpdates
calls must match the net number of cells added/deleted with the
InsertRows
and DeleteRows
methods. If the underlying datasource isn’t changed
to match the insertions/deletions on the table view an error will occur.