Single Open SwipeView in Collection - Xamarin

In a project I’m working on, there was a specific request for having a collection of SwipeViews on a page, where it is only possible to have a single SwipeView swiped open. Since I wasn’t able to find a proper solution for that on the web (only a request, for example here), I decided to write a short post about my solution. The example repo you can find here.

drawing

In the last few months, Xamarin HQ has been working on stabilizing the SwipeView. As David Ortinau announced in his latest blogpost, Xamarin Forms 5.0 promises a lot of bug resolves and hopefully so a stable version of this control. In the current stable version of Xamarin Forms, this control is still in preview and developers need to set the Experimental Flags for the SwipeView in the native projects, as described in the documentation for the SwipeView.

For this project, I used a simple MVVM setup, so we need at least a Model, a View and a ViewModel to work with. I will only use SwipeViews that open up on the right side, but you can implement this solution in every way you like for the SwipeView. I would say the possibilities are countless…

Model

First thing I set up was the model, in this case a Person class with two properties. The IsOpen property is necessary to bind to the custom SwipeView I will explain later. I implemented the INotifyPropertyChanged interface to be able to notify the view about the IsOpen property updates.

public class Person : INotifyPropertyChanged
{
    public string Name { get; set; }

    private bool _isOpen;
    public bool IsOpen
    {
        get => _isOpen;
        set
        {
            if (_isOpen != value)
            {
                _isOpen = value;
                OnPropertyChanged();
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

ViewModel

Simply create a list of Persons as a source for the CollectionView. The DeleteCommand handles the SwipeView Delete button that shows up when swiping the item to the left. The OpenItemChangedCommand sets the IsOpen property of any other Person item, that is not the last swiped Person item, to false. OnPropertyChanged() is needed to update the page when an item is deleted from the list.

public class SwipeViewCollectionViewModel : INotifyPropertyChanged
  {
      public SwipeViewCollectionViewModel()
      {
          Persons = new List<Person>
          {
              new Person { Name =  "Albert"},
              new Person { Name =  "Burak"},
              new Person { Name =  "Conny"},
              new Person { Name =  "Dolly"},
              new Person { Name =  "Erik"},
          };
      }

      public event PropertyChangedEventHandler PropertyChanged;

      private List<Person> _persons;
      public List<Person> Persons
      {
          get => _persons;
          set
          {
              if (_persons != value)
              {
                  _persons = value;
                  OnPropertyChanged();
              }
          }
      }

      private Command<Person> _deletePersonCommand;
      public Command<Person> DeletePersonCommand => _deletePersonCommand ??= new Command<Person>((person) => Persons.Remove(person));

      private Command<Person> _openItemChangedCommand;
      public Command<Person> OpenItemChangedCommand => _openItemChangedCommand ??= new Command<Person>(ChangeOpenPersonItem);
      private void ChangeOpenPersonItem(Person swipedPersonItem)
      {
          if (swipedPersonItem != null && swipedPersonItem.IsOpen)
          {
              if (Persons.Count < 2 || Persons.Select(x => x.IsOpen).Count() < 2)
                  return;

              foreach (var person in Persons.Where(x => x != swipedPersonItem && x.IsOpen))
              {
                  person.IsOpen = false;
              }
          }
      }

      private void OnPropertyChanged([CallerMemberName] string propertyName = null)
      {
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      }
  }

Custom SwipeView

In order to get the solution working, we need to extend the existing SwipeView with some properties of our own. In the official documentation for SwipeView, I have already seen a bindable property IsOpen, but in the current stable Xamarin version, you’re not yet able to use it. In this case I created a bindable IsOpen property that will be bound to the IsOpen property in the Person model. When that property changes, the Command will be executed. Also make sure to set the BindingMode to TwoWay, since we need to be able to update both the View and the ViewModel in both ways.

The SwipeView gives us the ability to subscribe to the SwipeEnded event. With the SwipeEndedEventArgs we’re able to check if the SwipeView is open when the swipe gesture has ended. This is the key of success to this implementation: if the SwipeView is open, the IsOpen property will be updated. This will update the IsOpen property for the swiped Person item AND will execute the command to make sure all other Person items in the list will have the IsOpen property set to false.

public class CustomSwipeView : SwipeView, IDisposable
{
    public CustomSwipeView()
    {
        SwipeEnded += CustomSwipeView_SwipeEnded;
    }

    public static readonly BindableProperty IsOpenProperty = BindableProperty.Create(
        nameof(IsOpen),
        typeof(bool),
        typeof(CustomSwipeView),
        false,
        BindingMode.TwoWay,
        propertyChanged: IsOpenPropertyChanged);

    public bool IsOpen
    {
        get => (bool)GetValue(IsOpenProperty);
        set => SetValue(IsOpenProperty, value);
    }

    public static readonly BindableProperty CommandProperty = BindableProperty.Create(
        nameof(Command), typeof(ICommand), typeof(CustomSwipeView), null);
    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
        nameof(CommandParameter), typeof(object), typeof(CustomSwipeView), null);
    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    public void Dispose()
    {
        SwipeEnded -= CustomSwipeView_SwipeEnded;
    }

    private static void IsOpenPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (!(bindable is CustomSwipeView control) || !(oldValue is bool wasOpen) || !(newValue is bool isOpen))
            return;

        if (!wasOpen)
            return;

        if (!isOpen)
            control.Close();
    }

    private void CustomSwipeView_SwipeEnded(object sender, SwipeEndedEventArgs e)
    {
        if (e.IsOpen && Command.CanExecute(CommandParameter))
        {
            IsOpen = true;
            Command.Execute(CommandParameter);
        }
    }
}

CollectionView

Last but not least, we need to create the page with the CollectionView. I used relative binding to be able to bind to the ViewModel from a DataTemplate. The model itself is bound as a CommandParameter to be able to update or delete the model from the list. Make sure to bind the Person.IsOpen property to the CustomSwipeView.IsOpen property.

<CollectionView
    ItemsSource="{Binding Persons}"
    SelectionMode="None">
    <CollectionView.ItemsLayout>
        <LinearItemsLayout
            Orientation="Vertical"
            ItemSpacing="10"/>
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <ContentView>
                <views:CustomSwipeView
                    Margin="8"
                    Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:SwipeViewCollectionViewModel}}, Path=OpenItemChangedCommand}"
                    CommandParameter="{Binding .}"
                    IsOpen="{Binding IsOpen}">
                    <views:CustomSwipeView.RightItems>
                        <SwipeItems>
                            <SwipeItemView
                                CommandParameter="{Binding .}"
                                Command="{Binding Source={RelativeSource AncestorType={x:Type viewModels:SwipeViewCollectionViewModel}}, Path=DeletePersonCommand}">
                                <Frame
                                    HasShadow="False"
                                    BackgroundColor="Red">
                                    <Label Text="Delete" TextColor="White"/>
                                </Frame>
                            </SwipeItemView>
                        </SwipeItems>
                    </views:CustomSwipeView.RightItems>
                    <Frame
                        HasShadow="False"
                        BackgroundColor="LightGray">
                        <Label Text="{Binding Name}" HorizontalTextAlignment="Center"/>
                    </Frame>
                </views:CustomSwipeView>
            </ContentView>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

This is my solution, I bet there are many other solutions. If you have one, let me know! I created an example repo for everyone to refer to. You can also find me on Twitter (@rickavdijk) and LinkedIn or even by mail to rick@baseflow.com. Please leave your feedback if you have some!

Rick van Dijk

rick@baseflow.com