Xamarin iOS UITableView GetCell Method Called for Invisible Cells
In my Xamarin iOS application I’m leveraging the UITableView control to page through a collection of items in a performant manner. The UITableView uses the GetCell method of the UITableViewDataSource delegate to create or reuse instances of cells as they become visible on screen.
Great, that sounds efficient, and it is! However, in my application the GetCell
method was being called once for every cell when the UITableView was initialized, including the invisible ones.
What gives?
This post spawned from a stackoverflow question I posted, then immediately found the answer to.
Replicating the Behavior By Example
The UITableView in question was added to a ViewController in the StoryBoard. Within the ViewController’s ViewDidLoad
method, I have the following setup:
public override void ViewDidLoad() {
base.ViewDidLoad();
var model = new UIColor[] {
UIColor.Green,
UIColor.Red,
UIColor.Magenta,
UIColor.Cyan,
UIColor.Blue,
UIColor.Purple
};
SampleTableView.RowHeight = SampleTableView.Frame.Height;
SampleTableView.Setup(model);
}
In short, a model consisting of a collection of colors is created and passed through to a Setup
method on my custom UITableView, SampleTableView
.
In addition to the setup, I already know the height of each row ahead of time, so the RowHeight
property of the UITableView is set. As you can see, each row will take the full height of the UITableView. Swiping up or down will then reveal the next page.
Here’s what the SampleTableView
and related dependencies look like:
public class SampleCell : UITableViewCell {
public void Setup(UIColor color) {
BackgroundColor = color;
}
}
public class SampleTableViewDataSourceDelegate : UITableViewDataSource {
public SampleTableViewDataSourceDelegate(UIColor[] model) {
this.model = model;
}
private readonly UIColor[] model;
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {
Debug.WriteLine($"GetCell called. Row {indexPath.Row}");
var reuseIdentifier = "SampleCell";
var cell = tableView.DequeueReusableCell(reuseIdentifier) as SampleCell;
cell = (cell ?? (cell = new SampleCell()));
cell.Setup(model[indexPath.Row]);
return cell;
}
public override nint NumberOfSections(UITableView tableView) {
return 1;
}
public override nint RowsInSection(UITableView tableView, nint section) {
return model.Length;
}
}
public partial class SampleTableView : UITableView {
public SampleTableView(IntPtr handle) : base(handle) {
PagingEnabled = true;
}
private UIColor[] model;
public void Setup(UIColor[] model) {
this.model = model;
DataSource = new SampleTableViewDataSourceDelegate(model);
}
}
SampleCell
A custom cell that simply sets the background color to the one provided by the model.
SampleTableViewDataSourceDelegate
A derived class of UITableViewDataSource
, which handles implementing the GetCell
method. This class is responsible creating and reusing cells as they become visible.
SampleTableView
A derived class of UITableView
, which handles initial setup of the UITableView via the Setup
method, invoked by the containing ViewController.
We now have a fairly slimmed down example of a UITableView implementation. In the GetCell
delegate method there’s a line of code that outputs a message to the log when it’s called. After running the application with Visual Studio in a simulator, the Visual Studio Output console shows the following:
[0:] GetCell called. Row 0
[0:] GetCell called. Row 1
[0:] GetCell called. Row 2
[0:] GetCell called. Row 3
[0:] GetCell called. Row 4
[0:] GetCell called. Row 5
Therein lies the issue; the GetCell
method is called once for each cell in our model. Why? Didn’t I specify the RowHeight
to be the height of the UITableView? If that’s true, and we know that GetCell
should only be called when cells become visible, then why is it being called Model.Length
number of times?
UITableView EstimatedRowHeight
Our RowHeight
property is set to take up the entire UITableView. When the UITableView data source is initialized, however, the UITableView attempts to estimate the required row height for us on-the-fly via the EstimatedRowHeight
property. The EstimatedRowHeight
property was not set, effectively relying on the table view to calculate it for us. The solution to this particular problem then is to set the EstimateRowHeight
property in addition to the RowHeight
property.
Back in the ViewDidLoad
method of the ViewController
, adding the following line of code above the RowHeight
setter did the trick:
SampleTableView.EstimatedRowHeight = SampleTableView.Frame.Height;
The Output log now shows what we’d expect, a single call to GetCell
on initialize:
[0:] GetCell called. Row 0
Hi, I’m Sam.
I’m a programmer and a DIYer. When I’m not finding things to build I enjoy cooking, hiking, camping and traveling the world with my best friend. Say Hello!