Grouped UITableViews with MVVMCross

I recently had a requirement to implement a grouped iOS table view in a project that was using MVVMCross for its MVVM architecture. MVVMCross provides a very clever way of binding data (as a collection of ViewModels) to a table view source, but neither the official documentation nor Google searches gave any straightforward examples of implementing grouped sections within a UITableView, so here’s how I managed to achieve this:

Notes and assumptions

  • I’ve assumed if you’re reading this then you’re probably keen on using custom table cells, so the examples include the most basic of custom cells with a corresponding .xib and a ViewModel to bind to. You shouldn’t have any problems binding to properties in a basic cell type.

  • At the time of writing, the examples I’m using are buit for and using:

  • MVVMCross 5.6.3
  • Xamarin.iOS 11.6

  • The full example source code can be found here

The code

For this example I’ve added a UITableView to my main view controller .xib, and named it MyTableView:

Main view controller:

using System;
using Foundation;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Binding.ExtensionMethods;
using MvvmCross.Binding.iOS.Views;
using MvvmCross.Core.ViewModels;
using MvvmCross.iOS.Views;
using MvvmCross.iOS.Views.Presenters.Attributes;
using MvvmCross.Platform.Core;
using MvvmCrossGroupedTableView.iOS.Views.Cells;
using MvvmCrossGroupedTableView.ViewModels;
using UIKit;

namespace MvvmCrossGroupedTableView.iOS.Views
{
    [MvxRootPresentation(WrapInNavigationController = true)]
    public partial class MainView : MvxViewController<MainViewModel>
    {
        public MainView() : base("MainView", null)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            Title = "Grouped Table View";

            // Apply a custom MvxTableViewSource to the table view
            var myTableViewSource = new CustomTableViewSource(MyTableView);
            MyTableView.Source = myTableViewSource;

            // Bind the ItemsSource property in the table source to the view model
            var set = this.CreateBindingSet<MainView, MainViewModel>();
            set.Bind(myTableViewSource).To(vm => vm.MyData);
            set.Apply();

            MyTableView.ReloadData();
        }
    }
}

View model for the main view:

using MvvmCross.Core.ViewModels;

namespace MvvmCrossGroupedTableView.ViewModels
{
    public class MainViewModel : MvxViewModel
    {

        public MainViewModel()
        {
        }

        // A simple collection to provide data for the table view, yours will be more dynamic obviously 🙂
        public MvxObservableCollection<MvxObservableCollection<CustomCellViewModel>> MyData
        {
            get 
            {
                var firstGroup = new MvxObservableCollection<CustomCellViewModel>();
                var secondGroup = new MvxObservableCollection<CustomCellViewModel>();

                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 0"));
                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 1"));
                firstGroup.Add(new CustomCellViewModel("Section: 0, Row: 2"));

                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 0"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 1"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 2"));
                secondGroup.Add(new CustomCellViewModel("Section: 1, Row: 3"));

                return new MvxObservableCollection<MvxObservableCollection<CustomCellViewModel>>()
                {
                    firstGroup, secondGroup
                };
            }
        }

    }
}

View model for my custom cell:

using MvvmCross.Core.ViewModels;
using MvvmCross.Platform.UI;

namespace MvvmCrossGroupedTableView.ViewModels
{
    public class CustomCellViewModel : MvxViewModel
    {
        string _text;

        public CustomCellViewModel(string text)
        {
            _text = text;
        }

        public string Text
        {
            get => _text;
            set => SetProperty(ref _text, value);
        }
    }
}

My custom table view cell:

using System;
using Foundation;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Binding.iOS.Views;
using MvvmCrossGroupedTableView.ViewModels;
using MvvmCross.Plugins.Color;
using UIKit;

namespace MvvmCrossGroupedTableView.iOS.Views.Cells
{
    public partial class CustomCell : MvxTableViewCell
    {
        public static readonly NSString Key = new NSString("CustomCell");
        public static readonly UINib Nib = Nib = UINib.FromName("CustomCell", NSBundle.MainBundle);

        public static CustomCell Create()
        {
            return Nib.Instantiate(null, null)[0] as CustomCell;
        }

        protected CustomCell(IntPtr handle) : base(handle)
        {
            this.DelayBind(() => {
                var set = this.CreateBindingSet<CustomCell, CustomCellViewModel>();
                set.Bind(TextLabel).To(vm => vm.Text);
                set.Apply();
            });
        }
    }
}

Finally, we need to create our custom table view source, which is where the magic happens. You need to create a custom source which inherits from MvxTableViewSource as follows:

public class CustomTableViewSource : MvxTableViewSource
    {
        public CustomTableViewSource(UITableView tableView) : base(tableView)
        {
        }

        // # 1:
        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            var group = ItemsSource.ElementAt(indexPath.Section) as MvxObservableCollection<CustomCellViewModel>;
            var item = group.ElementAt(indexPath.Row) as CustomCellViewModel;

            var cell = GetOrCreateCellFor(tableView, indexPath, item);

            return cell;
        }

        // # 2:
        protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
        {
            var cell = tableView.DequeueReusableCell(CustomCell.Key) as CustomCell;

            if (cell == null)
            {
                cell = CustomCell.Create();
            }

        // # 3:
            var bindable = cell as IMvxDataConsumer;
            if (bindable != null)
                bindable.DataContext = item;

            return cell;
        }

        // # 4:
        public override nint NumberOfSections(UITableView tableView)
        {
            return ItemsSource.Count();
        }

        // # 5:
        public override nint RowsInSection(UITableView tableview, nint section)
        {
            var group = ItemsSource.ElementAt((int)section) as MvxObservableCollection<CustomCellViewModel>;
            return group.Count();
        }

        public override string TitleForHeader(UITableView tableView, nint section)
        {
            return string.Format($"Header for section {section}");
        }
  1. Override the GetCell() method as you would normally do in a standard UITableView source. First of all, parse your ItemsSource (the default data source which your view model data is bound to, which is IEnumerable) to get the cell’s custom view model. Next, use the GetOrCreateCellFor() override to generate the cell to return.

  2. In a MvxTableViewSource you must override the GetOrCreateCellFor() method, as this is where your cell is set up and data bindings are connected. Because we’ve overridden GetCell(), we can pass the specific cell view model to the cell we’re creating.

  3. This chunk of code is vital to set up the bindings between the custom cell and the cell’s view model that you’ve passed in.

  4. and 5. As with a normal UITableViewSource, you’ll want to override these methods to explicitly control the number of sections and rows for each section.

I hope this guide provides enough information for you to implement your own grouped tables, if there’s anything that’s missing or if you have any questions, comment below or get in touch via Github.

Shrinking your Xamarin iOS binary using the mtouch linker

Submitting an app to the App Store can be tricky business – setting up certificates, provisioning profiles, providing all the necessary assets – it can take hours, if not days to finally get your app submitted for review for the first time. Add to this Apple’s 100mb file size limit and you can end up in a world of hurt.

One way of squeezing the size of your app binary is to make use of the linker, which can be set here:

linkerlocation
(iOS Project) > Options > iOS Build

During build time, the mtouch linker will run through every part of your code, stripping out anything that it deems unused, so that your resulting binary is as small as possible. By default, your project’s default setting for  will likely be set to ‘Don’t Link’ for your iPhone Simulator configuration (which means a faster compile time, since the linker doesn’t attempt to strip away unused code), and ‘Link Framework SDKs Only’ for debugging on a device, which means that the linker will only strip unused code from the Xamarin.iOS SDK and leave your code and packages alone.

These settings may be sufficient for your app, but when it comes to publishing to the App Store you might find that your app’s file size is greater than Apple’s 100mb limit, especially if you’re using a lot of embedded media. To help solve this problem you can use the linker to cut down on unused code from all of the packages and assemblies you’ve added to your project, as well as your own code by setting the behaviour to ‘Link All‘. Your compiled binary will be made as lightweight as possible.

Magic, right?

Well, it might not be as easy as all that. Depending on your app, it’s likely that the linker will strip away code that your app actually needs, such as code that your app calls dynamically or indirectly. In these cases, you need to tell the linker to preserve the code that you’re still using.

It comes down to a bit of trial and error – running your app and seeing where it crashes or doesn’t behave predictably, then finding the code that’s no longer executing properly. When you do find the bugs, you can make use of Preserve attributes to make the linker ignore the feature and move on.

Preserving your code

In my latest iOS project, the code I found to be troublesome during linking were the data models I created for using Json.NET parsing. The linker was stripping out the empty constructors, which Json.NET relies on whilst serializing/deserializing Json data.

To overcome this, firstly I created a new PreserveAttribute class so that I could use Preserve attributes in my shared PCL (place this in any namespace you like – the linker looks this attribute by type name):

public sealed class PreserveAttribute : System.Attribute 
{
    public bool AllMembers;
    public bool Conditional;
}

Then I used thePreserve attribute to tag all of the models that were causing issues:

[Preserve(AllMembers = true)]
public class Route
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("description")]
    public string Description { get; set; }

    [JsonProperty("distance")]
    public double Distance { get; set; }

    //...
}

Do this wherever there’s code that you don’t wish the linker to remove. You have the option of using [Preserve (AllMembers = true)] for preserving the whole class, or [Preserve (Conditional=true)] to protect specific members within a class.

Preserving assemblies

If you are having problems with the linker taking useful code away from 3rd party packages that you’ve installed, you won’t have the option of setting attributes like above. To protect an assembly from the linker, you can use the mtouch command-line tool. In Xamarin Studio you can add arguments in the iOS Build settings of your project, for example:

Screen Shot 2017-06-10 at 10.06.33

Finding the troublesome code can be very tricky and time consuming, so in some cases using the linker might not be the best option for you. If you have large media files embedded in your project such as images or video, you could consider hosting them on a server and then downloading them at run-time instead of bundling them into your package file.

If you’re having problems using the linker, more details are available on the Xamarin website, where there is also details about using the Android linker.