Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nbevans/a713dc9c77a8f530b6f4f3cd4fad83c2 to your computer and use it in GitHub Desktop.
Save nbevans/a713dc9c77a8f530b6f4f3cd4fad83c2 to your computer and use it in GitHub Desktop.
// Fixes broken ListView header heights on iOS
public sealed class ListViewRendererHackfix_AutomaticHeaderHeights_20190418 : ListViewRenderer {
private static readonly FieldInfo fieldInfo_ListViewRenderer_dataSource = typeof(ListViewRenderer).GetField("_dataSource", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly Type type_ListViewRenderer_ListViewDataSource = typeof(ListViewRenderer).GetNestedType("ListViewDataSource", BindingFlags.NonPublic);
private static readonly Type type_ListViewRenderer_UnevenListViewDataSource = typeof(ListViewRenderer).GetNestedType("UnevenListViewDataSource", BindingFlags.NonPublic);
private static readonly ConstructorInfo ctorInfo_ListViewRenderer_ListViewDataSource = type_ListViewRenderer_ListViewDataSource.GetConstructor(new[] { fieldInfo_ListViewRenderer_dataSource.FieldType });
private static readonly ConstructorInfo ctorInfo_ListViewRenderer_UnevenListViewDataSource = type_ListViewRenderer_UnevenListViewDataSource.GetConstructor(new[] { fieldInfo_ListViewRenderer_dataSource.FieldType });
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) {
base.OnElementChanged(e);
if (e.NewElement != null) {
Control.Source = new CustomListViewDataSource(Control.Source);
}
if (Control != null) {
// Somewhat optional - not core to the fix.
Control.RowHeight = UITableView.AutomaticDimension;
Control.EstimatedRowHeight = 40;
Control.SectionHeaderHeight = UITableView.AutomaticDimension;
Control.EstimatedSectionHeaderHeight = 40;
Control.SectionFooterHeight = 0;
Control.EstimatedSectionFooterHeight = 0;
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (e.PropertyName == ListView.HasUnevenRowsProperty.PropertyName) {
var ctor = Element.HasUnevenRows ? ctorInfo_ListViewRenderer_UnevenListViewDataSource : ctorInfo_ListViewRenderer_ListViewDataSource;
var dataSource = fieldInfo_ListViewRenderer_dataSource.GetValue(this);
var tvs = (UITableViewSource)ctor.Invoke(new[] { dataSource });
fieldInfo_ListViewRenderer_dataSource.SetValue(this, tvs);
Control.Source = new CustomListViewDataSource(tvs);
Control.ReloadData();
} else {
base.OnElementPropertyChanged(sender, e);
}
}
public sealed class CustomListViewDataSource : UITableViewSource {
private readonly UITableViewSource inner;
private readonly MethodInfo methodInfo_DetermineEstimatedRowHeight;
private PropertyInfo propertyInfo_HeaderWrapperView_Cell;
public CustomListViewDataSource(UITableViewSource inner) {
this.inner = inner;
methodInfo_DetermineEstimatedRowHeight = inner.GetType().BaseType.GetMethod("DetermineEstimatedRowHeight", BindingFlags.Instance | BindingFlags.NonPublic);
}
public override nfloat GetHeightForHeader(UITableView tableView, nint section) {
var r = inner.GetHeightForHeader(tableView, section);
r = UITableView.AutomaticDimension;
return r;
}
public override UIView GetViewForHeader(UITableView tableView, nint section) {
var innerWrapper = inner.GetViewForHeader(tableView, section);
var pi = propertyInfo_HeaderWrapperView_Cell ?? (propertyInfo_HeaderWrapperView_Cell = innerWrapper.GetType().GetProperty("Cell", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public));
var cell = (Cell)pi.GetValue(innerWrapper);
var cellView = innerWrapper.Subviews[0];
var newWrapper = new HeaderWrapperView(cell, cellView);
return newWrapper;
}
public override void HeaderViewDisplayingEnded(UITableView tableView, UIView headerView, nint section) {
if (headerView is HeaderWrapperView wrapper) {
wrapper.Cell?.SendDisappearing();
wrapper.Cell = null;
}
}
public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath) {
// Newer versions of XamForms override this method in UITableViewSource - so to maintain compat with older and newer, we use conditional reflection to pass-thru this method call.
if (methodInfo_DetermineEstimatedRowHeight != null)
methodInfo_DetermineEstimatedRowHeight.Invoke(inner, new object[0]);
}
#region Irrelevant
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {
return inner.GetCell(tableView, indexPath);
}
public override nint RowsInSection(UITableView tableView, nint section) {
return inner.RowsInSection(tableView, section);
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath) {
return inner.GetHeightForRow(tableView, indexPath);
}
public override void DraggingEnded(UIScrollView scrollView, bool willDecelerate) {
inner.DraggingEnded(scrollView, willDecelerate);
}
public override void DraggingStarted(UIScrollView scrollView) {
inner.DraggingStarted(scrollView);
}
public override nint NumberOfSections(UITableView tableView) {
return inner.NumberOfSections(tableView);
}
public override void RowDeselected(UITableView tableView, NSIndexPath indexPath) {
inner.RowDeselected(tableView, indexPath);
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath) {
inner.RowSelected(tableView, indexPath);
}
public override void Scrolled(UIScrollView scrollView) {
inner.Scrolled(scrollView);
}
public override string[] SectionIndexTitles(UITableView tableView) {
return inner.SectionIndexTitles(tableView);
}
#endregion
protected override void Dispose(bool disposing) {
inner.Dispose();
}
private class HeaderWrapperView : UIView {
public Cell Cell { get; set; }
public UIView Subview { get; private set; }
public HeaderWrapperView(Cell cell, UIView subview) {
Cell = cell;
Subview = subview;
AddSubview(subview);
}
public override CGSize SizeThatFits(CGSize size) {
return Subview.SizeThatFits(size);
}
public override CGSize IntrinsicContentSize => Subview.IntrinsicContentSize;
public override void LayoutSubviews() {
base.LayoutSubviews();
Subview.Frame = Bounds;
}
}
}
}
@ryanduffyegov
Copy link

You are my HERO!!!!

@techfooninja
Copy link

This is fantastic! All I needed to do to get it to work for all my ListViews (including the ones that aren't grouped without needing to special case), I changed line 12 to if (e.NewElement != null && e.NewElement.IsGroupingEnabled)

@gbelmont22
Copy link

Just a note for anyone dealing with this same problem in Xamarin: As of Nov 22, 2019 - developing with Xamarin Forms 4.3.0.991211 in Visual Studio Pro 2017 15.9.16 and I still needed to use this renderer to get the grouped headers height to layout properly in iOS 13.1. It might be fixed in Xamarin and I'm doing something wrong, or Xamarin is still dragging their feet on this, but in case anyone else is trying to puzzle this out the provided class worked perfectly with the exception that I had to apply techfooninja's fix to line 12, which excluded the non-grouped ListViews. Thank You Nathan - this fix really saved me from a ton of problems.

@ehuna
Copy link

ehuna commented Nov 26, 2019

Thank you!

Hopefully the Xamarin team will merge the fix soon, but this at least allows us to move forward.

@ehuna
Copy link

ehuna commented Nov 26, 2019

If anyone from the Xamarin team sees this, here are some other references:

  1. Original issue: "[iOS] ListView Group Header size incorrect on iOS"
    xamarin/Xamarin.Forms#3769

  2. Another report with a Xamarin Forms project to easily reproduce the issue: "[Bug] ListView Group Header Height Doesn't Autosize to Content"
    xamarin/Xamarin.Forms#8662
    https://github.com/xamarin/Xamarin.Forms/files/3889774/ListViewGroupHeaderHeightIssueProject.zip

@nielscup
Copy link

Halleluja for @nbevans and this hackfix!!!

@matheusdelvalle
Copy link

Hey, how are you?

I'm using this workaround in my project and I just updated the Xamarin Forms version. I'm using the version 4.8.0.1451 now.

I had to change the follow line(61):
var cellView = innerWrapper.Subviews[0];
to
var cellView = innerWrapper.Subviews.LastODefault();

Apparently, the Subview needed to Add in the "HeaderWrapperView" isn't the first one anymore.

Could you check if this is the right workaround?

It's working perfectly for me.

Thanks!

@allschu
Copy link

allschu commented Nov 4, 2020

Hey, how are you?

I'm using this workaround in my project and I just updated the Xamarin Forms version. I'm using the version 4.8.0.1451 now.

I had to change the follow line(61):
var cellView = innerWrapper.Subviews[0];
to
var cellView = innerWrapper.Subviews.LastODefault();

Apparently, the Subview needed to Add in the "HeaderWrapperView" isn't the first one anymore.

Could you check if this is the right workaround?

It's working perfectly for me.

Thanks!

Thank you for the fix, saved me some debugging

@ilyaqznetsow
Copy link

thank you a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment