I've accepted SwiftArchitect's answer however seeing as I was after a code based approach for a TableCell I will add a separate answer. Without his help I
would not have been able to get this far.
I am using MVVMCross and Xamarin iOS and my TableCell inherits from MvxTableViewCell
Creation of SubViews
From the ctor of the Cell I create all the necessary UILabels and turn off AutoResizingMasks by setting view.TranslatesAutoresizingMaskIntoConstraints = false
At the same time I create two UIViews leftColumnContainer and rightColumnContainer. These again TranslatesAutoresizingMaskIntoConstraints
set to false.
The relevant labels are added as subviews to the leftColumnContainer
and rightColumnContainer
UIViews. The two containers are then added as SubViews to the TableCell's ContentView
this.ContentView.AddSubviews(this.leftColumnContainer, this.rightColumnContainer);
this.ContentView.TranslatesAutoresizingMaskIntoConstraints = true;
The UILabels are all data bound via an MVVMCross DelayBind call
Setting Layout Constraints (UpdateConstraints())
The layout of the TableCell is conditional on the data for the cell with 5 of the 8 labels being optional and 4 of the 8 needing to support wrapping of text
The First thing I do is pin the Top and Left of the leftColumnContainer
to the TableCell.ContentView. Then the Top and Right of the 'rightColumnContainer' to the TableCell.ContentView. The design requires the right column to be smaller than the left so this is done using scaling. I am using FluentLayout to apply these constraints
this.ContentView.AddConstraints(
this.leftColumnContainer.AtTopOf(this.ContentView),
this.leftColumnContainer.AtLeftOf(this.ContentView, 3.0f),
this.leftColumnContainer.ToLeftOf(this.rightColumnContainer),
this.rightColumnContainer.AtTopOf(this.ContentView),
this.rightColumnContainer.ToRightOf(this.leftColumnContainer),
this.rightColumnContainer.AtRightOf(this.ContentView),
this.rightColumnContainer.WithRelativeWidth(this.ContentView, 0.35f));
The calls to ToLeftOf and ToRight of are laying the right edge of the Left Column and the left edge of the Right Column next to each other
A key piece of the solution that came from SwiftArchitect was to set the height of the TableCell's ContentView to >= to the height of the leftColumnContainer
and the rightColumnContainer
. It wasn't so obvious how to do these with FluentLayout so they are "longhand"
this.ContentView.AddConstraint(
NSLayoutConstraint.Create(this.ContentView, NSLayoutAttribute.Height, NSLayoutRelation.GreaterThanOrEqual, this.leftColumnContainer, NSLayoutAttribute.Height, 1.0f, 5.0f));
this.ContentView.AddConstraint(
NSLayoutConstraint.Create(this.ContentView, NSLayoutAttribute.Height, NSLayoutRelation.GreaterThanOrEqual, this.rightColumnContainer, NSLayoutAttribute.Height, 1.0f, 5.0f));
I then constraint the top, left and right of the first label in each column to the column container. Here is an example of from the first label in the left column
this.leftColumnContainer.AddConstraints(
this.categoryLabel.AtTopOf(this.leftColumnContainer, CellPadding),
this.categoryLabel.AtRightOf(this.leftColumnContainer, CellPadding),
this.categoryLabel.AtLeftOf(this.leftColumnContainer, CellPadding));
For each of the labels that are optional I first check the MVVMCross DataContext to see if they are visible. If they are visible similar constraints for Left, Top and Right are applied with the Top being constrained to the Bottom of the label above. If they are not visible the are removed from the View like so
this.bodyText.RemoveFromSuperview();
If you are wondering how these cells are going to work with iOSs Cell Reuse I will cover that next.
If the label is going to be the last label in the column (this is dependant on the data) I apply the other key learning from SwiftArcthiect's answer
Let [the columns] calculate their ideal height by adding a single
height constraint to the bottom of the lowest label (leftColumn.bottom
Equal lowestLeftLabel.bottom)
Dealing with cell Reuse
With such a complicated set of Constraints and many optional cells I did not want to have to reapply the constraints everytime the cell was reused with potentially different optional labels. To do this I am building and setting the reuse identifier at runtime.
The TableSource inherits from MvxTableViewSource. In the overridden GetOrCreateCellFor I check for a specific reuseIdentifier (normal use) and if so
call DequeueReusableCell however in this instance I defer to a routine encapsulated in the custom Cell class that knows how to be build a data specific id
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
MvxTableViewCell cell;
if (this.reuseIdentifier != null)
{
cell = (MvxTableViewCell)tableView.DequeueReusableCell(this.reuseIdentifier);
}
else
{
// No single reuse identifier, defer to the cell for the identifer
string identifier = this.itemCell.GetCellIdentifier(item);
if (this.reuseIdentifiers.Contains(identifier) == false)
{
tableView.RegisterClassForCellReuse(this.tableCellType, identifier);
this.reuseIdentifiers.Add(identifier);
}
cell = (MvxTableViewCell)tableView.DequeueReusableCell(identifier);
}
return cell;
}
and the call to build the id
public string GetCellIdentifier(object item)
{
StringBuilder cellIdentifier = new StringBuilder();
var entry = item as EntryItemViewModelBase;
cellIdentifier.AppendFormat("notes{0}", entry.Notes.HasValue() ? "yes" : "no");
cellIdentifier.AppendFormat("_body{0}", !entry.Body.Any() ? "no" : "yes");
cellIdentifier.AppendFormat("_priority{0}", entry.Priority.HasValue() ? "yes" : "no");
cellIdentifier.AppendFormat("_prop1{0}", entry.Prop1.HasValue() ? "yes" : "no");
cellIdentifier.AppendFormat("_prop2{0}", entry.Prop2.HasValue() ? "yes" : "no");
cellIdentifier.AppendFormat("_warningq{0}", !entry.IsWarningQualifier ? "no" : "yes");
cellIdentifier.Append("_MEIC");
return cellIdentifier.ToString();
}