Swipe to delete became almost an industry standard after its initial introduction on iOS platform. Nowadays similar implementations can be found on other platforms such as Android and even UWP (i.e. Mail client on Windows 10). Recently, I read a how-to post by @jamesmontemagno and decided elaborate on the idea of creating a re-usable UITableSourceController for Xamarin.iOS to be used with MVVM Cross.
We want to create a table view source that supports binding to delete action on swipe as well as other item level commands.
So lets start with creating our MvxTableViewSource. Implementing the MvxStandardTableViewSource is merely deriving our class and creating the base class constructors.
Unlike the sample how-to post, we will not be passing a view model instance, we will rather expose an ICommand property for the delete action. This way, while we are setting up the table view and source bindings, we can bind a command from the view model to the table source.
public class EditableTableViewSource : MvxStandardTableViewSource { public EditableTableViewSource(UITableView tableView, string bindingText) : base(tableView, bindingText) { } // ... "couple" of other constructor overloads #region Overrides of UITableViewSource // ... removed for brevity #endregion public ICommand DeleteRowCommand { get; set; } }
So now let us go over the overridden methods of the UITableViewSource. First of, let us override the CanEditRow method to return true, no matter what. This opens the gates to row level editing functionality. Another important method to override is the EditingStyleForRow. The latter method is called for each row separately when the table enters the editing mode or when the user tries to swipe the row.
public override bool CanEditRow(UITableView tableView, NSIndexPath indexPath) { return true; } public override UITableViewCellEditingStyle EditingStyleForRow(UITableView tableView, NSIndexPath indexPath) { if (DeleteRowCommand.CanExecute(indexPath.Row)) { return UITableViewCellEditingStyle.Delete; } return UITableViewCellEditingStyle.None; }
The important part of this implementation is the way we used the CanExecute method of the ICommand interface to determine if a row is going to be actionable or not.
So now let us create the command in our SectionPageViewModel from the previous post and bind it to the table view source DeleteRowCommand.
private MvxCommand<int> m_DeleteItemCommand; public ICommand DeleteItemCommand { get { return m_DeleteItemCommand ?? (m_DeleteItemCommand = new MvxCommand<int>(ExecDeleteItem, CanExecDeleteItem)); } } public void ExecDeleteItem(int index) { Mvx.Trace($"Deleting item at {index}"); } public bool CanExecDeleteItem(int index) { Mvx.Trace($"Checking removal at {index}"); return index > 0; }
Looking good. If the command is executed, it will give us a trace and the command is only executable if the row index is bigger than 0 (meaning the first row in the table is not removable).
So now let us change the table source to the EditableTableViewSource and bind our remove command:
var tableSource = new EditableTableViewSource(TableView, UITableViewCellStyle.Subtitle, new NSString("ItemCell"), "TitleText Title;DetailText Subtitle; ImageUrl ImagePath,Converter=AssetsPath"); TableView.Source = tableSource; var set = this.CreateBindingSet<SectionPageView, SectionPageViewModel>(); set.Bind(tableSource).To(vm => vm.Items); set.Bind(tableSource) .For(s => s.SelectionChangedCommand) .To(vm => vm.NavigateToItemCommand); set.Bind(tableSource) .For(s => s.DeleteRowCommand) .To(vm => vm.DeleteItemCommand);
Now the delete action should be available both in the editing mode (when the Editing flag is set to true on the TableView) and also as a swipe action.
Right now, the delete action does nothing since we didn’t yet used the DeleteRowCommand anywhere. The execution can be implemented in two ways. The first way (especially if you are mainly targeting the Edit view of the table rather than the swipe actions) is to use and override the method CommitEditingStyle. This method is executed when the Editing status of the table view is changed and the changes are about to be committed (also used as the default action for the swipe to delete)
public override void CommitEditingStyle(UITableView tableView, UITableViewCellEditingStyle editingStyle, NSIndexPath indexPath) { switch (editingStyle) { case UITableViewCellEditingStyle.Delete: DeleteRowCommand.Execute(indexPath.Row); break; case UITableViewCellEditingStyle.None: break; } }
And/or we can use and override the EditActionsForRow method which is called when the user swipes the row to reveal additional actions. This method basically returns the set of UI actions that will be used if the user selects one of the swipe actions.
public override UITableViewRowAction[] EditActionsForRow(UITableView tableView, NSIndexPath indexPath) { var rowActions = new List<UITableViewRowAction>(); if (DeleteRowCommand.CanExecute(indexPath.Row)) { rowActions.Add(UITableViewRowAction.Create(UITableViewRowActionStyle.Destructive, "Delete", (action, path) => { DeleteRowCommand.Execute(indexPath.Row); })); } return rowActions.ToArray(); }
Notice that we are actually checking if the delete command is executable for the row in question and if so adding it with the UITableViewRowActionStyle.Destructive (which in fact is the default style).
So far so good, but what about other actions for our row.
As we have seen in the delete row action, the command that is to be exposed as a row action should offer three important attributes: Title, Execution delegate, CanExecute delegate. So (cheating from UWP interface used for popup commands) we can create an IUICommand interface.
NOTE:The following implementation can be an overkill for most since the additional actions can easily be pushed into a dictionary as title, command pair. |
public interface IUICommand : ICommand { string Label { get; } }
And the implementation of the interface, we can derive from MvxCommand with the additional label constructor parameter.
public class MvxUICommand<T> : MvxCommand<T>, IUICommand { public MvxUICommand(Action<T> execute, Func<T, bool> canExecute = null, string label = "") : base(execute, canExecute) { Label = label; } #region Implementation of IUICommand public string Label { get; } #endregion }
Now we can add our additional action(s) to our view model before expanding the editable table view source.
private List<IUICommand> m_ItemCommands = null; public List<IUICommand> ItemActions { get { if (m_ItemCommands == null) { m_ItemCommands = new List<IUICommand>(); m_ItemCommands.Add(new MvxUICommand<int>(ExecuteFavouriteAction, CanExecuteFavouriteAction, "Favourite")); } return m_ItemCommands; } }
Here we are adding an imaginary add to favorites (i.e. ExecuteFavouriteAction) action (implementation should be similar to the delete action). The biggest difference here is that we are passing in a label value as well. Also let us mix-it-up a little bit with the CanExecute implementation so that we can see the differences between the rows that displays the action and the ones don’t.
public bool CanExecuteFavouriteAction(int rowIndex) { return rowIndex % 2 == 0; }
So, it will be executed only on even rows. Let us now, extend our table source with additional actions and bind this puppy to the table source 🙂
Declaring the public property and modifying the EditActionsForRow method would look similar to:
public ICommand DeleteRowCommand { get; set; } public List<IUICommand> AdditionalActions { get; set; } public override UITableViewRowAction[] EditActionsForRow(UITableView tableView, NSIndexPath indexPath) { var rowActions = new List<UITableViewRowAction>(); rowActions.AddRange( AdditionalActions .Where(command=>command.CanExecute(indexPath.Row)) .Select( command => UITableViewRowAction.Create( UITableViewRowActionStyle.Normal, command.Label, (action, path) => command.Execute(indexPath.Row)))); if (DeleteRowCommand.CanExecute(indexPath.Row)) { rowActions.Add(UITableViewRowAction.Create(UITableViewRowActionStyle.Destructive, "Delete", (action, path) => { DeleteRowCommand.Execute(indexPath.Row); })); } return rowActions.ToArray(); }
Notice the where clause selects the executable actions and we create UITableRowAction from the IUICommand using the label and the two delegates.
set.Bind(tableSource) .For(s => s.AdditionalActions) .To(vm => vm.ItemActions);
That was it, now let’s fire up our (awesome!) remoted iOS simulator and see the results.
As you can see Item 2 is at index 1 in our table so the Favorite button is not added to this row, while the Item 3 is at index 2 and the Favorite action is in fact added to this row.
NOTE:It is important to mention here that the row 0 is not displaying any actions even though it is supposed to be an “even” index. (i.e. 0%0 = 0) The reason for this is that when we were implementing the EditingStyleForRow method, we returned UITableViewCellEditingStyle.Delete only if the DeleteRowCommand.CanExecute returned true for the current row. So we have to extend this method to check the additional actions and return Delete style if any of them are executable.
It is in fact almost ridiculous that the Delete editing style is allowing other actions on the row to be executable as well but it is what it is :). This also creates another problem when the table is put into the Editing mode. The row at 0 index would return Delete style (i.e. would display the delete icon on the header of the row) but the delete action is actually not executable on this row so it would only display the Favorite button. |
So that’s it for today, hope you enjoyed it. In the next post we will be delegating the swipe actions to the items so that we can move the responsibility to the cells and the item view model instances so that we can include the commands as part of the item binding.
Happy coding everyone,
Pingback: Developing Universal/Cross-Platform Apps with MVVM – VII | Can Bilgin