diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml new file mode 100644 index 00000000000..42aae91118e --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml.cs new file mode 100644 index 00000000000..e76ec1678f1 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9711.xaml.cs @@ -0,0 +1,128 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Collections.Generic; +using System.Collections.Specialized; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; +using System.Threading.Tasks; +using CategoryAttribute = NUnit.Framework.CategoryAttribute; + +// Thanks to GitHub user [@Matmork](https://github.com/Matmork) for this reproducible test case. +// https://github.com/xamarin/Xamarin.Forms/issues/9711#issuecomment-602520024 + +#if UITEST +using Xamarin.Forms.Core.UITests; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 9711, "[Bug] iOS Failed to marshal the Objective-C object HeaderWrapperView", PlatformAffected.iOS)] + public partial class Issue9711 : TestContentPage + { + protected override void Init() + { +#if APP + InitializeComponent(); + + List> groups = new List>(); + for (int i = 0; i < 105; i++) + { + var group = new ListGroup { Title = $"Group{i}" }; + for (int j = 0; j < 5; j++) + { + group.Add($"Group {i} Item {j}"); + } + + groups.Add(group); + } + + TestListView.AutomationId = "9711TestListView"; + TestListView.ItemsSource = groups; +#endif + } + + private void ViewCell_OnBindingContextChanged(object sender, EventArgs e) + { + if (sender is ViewCell cell && cell.BindingContext is ListGroup list) + { + list.Cell = cell; + } + } + + private async void TapGestureRecognizer_OnTapped(object sender, EventArgs e) + { + if (sender is ContentView cnt && cnt.BindingContext is ListGroup list) + { + for (int i = 0; i <= 50; i++) + { + await Task.Delay(25); + list.IsExpanded = !list.IsExpanded; + } + } + } + + +#if UITEST + [Category(UITestCategories.ListView)] + [Test] + public void TestTappingHeaderDoesNotCrash() + { + // Usually, tapping one header is sufficient to produce the exception. + // However, sometimes it takes two taps, and rarely, three. If the app + // crashes, one of the RunningApp queries will throw, failing the test. + Assert.DoesNotThrowAsync(async () => + { + RunningApp.Tap(x => x.Marked("Group2")); + await Task.Delay(3000); + RunningApp.Tap(x => x.Marked("Group1")); + await Task.Delay(3000); + RunningApp.Tap(x => x.Marked("Group0")); + await Task.Delay(3000); + RunningApp.Query(x => x.Marked("9711TestListView")); + }); + } +#endif + } + + [Preserve(AllMembers = true)] + public sealed class ListGroup : List, INotifyPropertyChanged, INotifyCollectionChanged + { + public string Title { get; set; } + public string AutomationId => Title; + private bool _isExpanded = true; + + public bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded == value) + return; + + if (Cell != null) + Cell.Height = value ? 75 : 40; + + _isExpanded = value; + OnPropertyChanged(); + OnCollectionChanged(); + } + } + + public ViewCell Cell { get; set; } + public event NotifyCollectionChangedEventHandler CollectionChanged; + public event PropertyChangedEventHandler PropertyChanged; + + private void OnCollectionChanged() + { + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 39afa8d7b4b..2dafd30e42a 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -1362,6 +1362,10 @@ + + Issue9711.xaml + Code + @@ -1560,6 +1564,7 @@ Designer MSBuild:UpdateDesignTimeXaml + @@ -2009,4 +2014,4 @@ MSBuild:UpdateDesignTimeXaml - \ No newline at end of file + diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs index 20b906b32be..09c8189a416 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs @@ -4,7 +4,6 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Linq; -using System.Threading.Tasks; using Foundation; using UIKit; using Xamarin.Forms.Internals; @@ -1105,20 +1104,21 @@ public override nfloat GetHeightForHeader(UITableView tableView, nint section) public override UIView GetViewForHeader(UITableView tableView, nint section) { - UIView view = null; - if (!List.IsGroupingEnabled) - return view; + return null; var cell = TemplatedItemsView.TemplatedItems[(int)section]; if (cell.HasContextActions) throw new NotSupportedException("Header cells do not support context actions"); + const string reuseIdentifier = "HeaderWrapper"; + var header = (HeaderWrapperView)tableView.DequeueReusableHeaderFooterView(reuseIdentifier) ?? new HeaderWrapperView(reuseIdentifier); + header.Cell = cell; + var renderer = (CellRenderer)Internals.Registrar.Registered.GetHandlerForObject(cell); - view = new HeaderWrapperView { Cell = cell }; - view.AddSubview(renderer.GetCell(cell, null, tableView)); + header.SetTableViewCell(renderer.GetCell(cell, null, tableView)); - return view; + return header; } public override void HeaderViewDisplayingEnded(UITableView tableView, UIView headerView, nint section) @@ -1457,9 +1457,24 @@ void PreserveActivityIndicatorState(Element element) } } - internal class HeaderWrapperView : UIView + class HeaderWrapperView : UITableViewHeaderFooterView { + public HeaderWrapperView(string reuseIdentifier) : base((NSString)reuseIdentifier) + { + } + + UITableViewCell _tableViewCell; + public Cell Cell { get; set; } + + public void SetTableViewCell(UITableViewCell value) + { + if (ReferenceEquals(_tableViewCell, value)) return; + _tableViewCell?.RemoveFromSuperview(); + _tableViewCell = value; + AddSubview(value); + } + public override void LayoutSubviews() { base.LayoutSubviews();