[iOS] Update cell size estimates after empty data source (#4688) fixes #4600
authorE.Z. Hart <hartez@users.noreply.github.com>
Thu, 13 Dec 2018 23:52:32 +0000 (16:52 -0700)
committerRui Marinho <me@ruimarinho.net>
Thu, 13 Dec 2018 23:52:32 +0000 (23:52 +0000)
* Create gallery to test issue

* Prevent DetermineCellSize loop; allow autolayout to size first cell after
initially empty data source; update estimates after initially empty data source;
fixes #4600

* Automated test

* Fix typo in comments

* Using Ignore to avoid running test because UI tests seems to ignore Explicit

* Change `_previousCount` to `bool _wasEmpty`

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue4600.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ItemAdder.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCodeCollectionViewGridGallery.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/ObservableCollectionGallery.cs
Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs
Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewController.cs
Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs
Xamarin.Forms.Platform.iOS/CollectionView/TemplatedCell.cs
Xamarin.Forms.Platform.iOS/CollectionView/VerticalTemplatedCell.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue4600.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue4600.cs
new file mode 100644 (file)
index 0000000..daa56fa
--- /dev/null
@@ -0,0 +1,38 @@
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+
+#if UITEST
+using Xamarin.Forms.Core.UITests;
+using Xamarin.UITest;
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+       [Category(UITestCategories.ManualReview)]
+       [Ignore("Ignoring until we have a lane to run CollectionView test (or CV is not behind a flag")] 
+#endif
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.Github, 4600, "[iOS] CollectionView crash with empty ObservableCollection", PlatformAffected.iOS)]
+       public class Issue4600 : TestNavigationPage
+       {
+               protected override void Init()
+               {
+#if APP
+                       PushAsync(
+                               new GalleryPages.CollectionViewGalleries.ObservableCodeCollectionViewGallery(initialItems: 0));
+#endif
+               }
+
+#if UITEST
+               [Test]
+               public void InitiallyEmptySourceDisplaysAddedItem()
+               {
+                       RunningApp.WaitForElement("Insert");
+                       RunningApp.Tap("Insert");
+                       RunningApp.WaitForElement("Inserted");
+               }
+#endif
+       }
+}
\ No newline at end of file
index 51694b0..040d90c 100644 (file)
       <DependentUpon>Issue4360.xaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
+    <Compile Include="$(MSBuildThisFileDirectory)Issue4600.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)LegacyComponents\NonAppCompatSwitch.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)MapsModalCrash.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)ModalActivityIndicatorTest.cs" />
index dfb4f29..d519ea1 100644 (file)
@@ -13,7 +13,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
                {
                        var index = indexes[0];
 
-                       if (index > -1 && index < observableCollection.Count)
+                       if (index > -1 && (index < observableCollection.Count || index == 0))
                        {
                                var item = new CollectionViewGalleryTestItem(DateTime.Now, "Inserted", "oasis.jpg", index);
                                observableCollection.Insert(index, item);
index 0dbbbb9..830c03f 100644 (file)
@@ -3,7 +3,7 @@
        internal class ObservableCodeCollectionViewGallery : ContentPage
        {
                public ObservableCodeCollectionViewGallery(ItemsLayoutOrientation orientation = ItemsLayoutOrientation.Vertical, 
-                       bool grid = true)
+                       bool grid = true, int initialItems = 1000)
                {
                        var layout = new Grid
                        { 
@@ -26,7 +26,7 @@
 
                        var collectionView = new CollectionView {ItemsLayout = itemsLayout, ItemTemplate = itemTemplate};
 
-                       var generator = new ItemsSourceGenerator(collectionView);
+                       var generator = new ItemsSourceGenerator(collectionView, initialItems);
                        
                        var remover = new ItemRemover(collectionView);
                        var adder = new ItemAdder(collectionView);
index 8b9a235..73ccba7 100644 (file)
                                                        new ObservableCodeCollectionViewGallery(grid: false), Navigation),
 
                                                GalleryBuilder.NavButton("Add/Remove Items (grid)", () =>
-                                                       new ObservableCodeCollectionViewGallery(), Navigation)
+                                                       new ObservableCodeCollectionViewGallery(), Navigation),
+
+                                               GalleryBuilder.NavButton("Add/Remove Items (grid, initially empty)", () =>
+                                                       new ObservableCodeCollectionViewGallery(initialItems: 0), Navigation)
                                        }
                                }
                        };
index 039f162..c217f30 100644 (file)
@@ -9,6 +9,7 @@
                public const string BoxView = "BoxView";
                public const string Button = "Button";
                public const string Cells = "Cells";
+               public const string CollectionView = "CollectionView";
                public const string ContextActions = "ContextActions";
                public const string DatePicker = "DatePicker";
                public const string DisplayAlert = "DisplayAlert";
index 72b941d..e8d9382 100644 (file)
@@ -12,6 +12,7 @@ namespace Xamarin.Forms.Platform.iOS
                readonly ItemsView _itemsView;
                readonly ItemsViewLayout _layout;
                bool _initialConstraintsSet;
+               bool _wasEmpty;
 
                public CollectionViewController(ItemsView itemsView, ItemsViewLayout layout) : base(layout)
                {
@@ -42,7 +43,18 @@ namespace Xamarin.Forms.Platform.iOS
 
                public override nint GetItemsCount(UICollectionView collectionView, nint section)
                {
-                       return _itemsSource.Count;
+                       var count = _itemsSource.Count;
+
+                       if (_wasEmpty && count > 0)
+                       {
+                               // We've moved from no items to having at least one item; it's likely that the layout needs to update
+                               // its cell size/estimate
+                               _layout?.SetNeedCellSizeUpdate();
+                       }
+
+                       _wasEmpty = count == 0;
+
+                       return count;
                }
 
                public override void ViewDidLoad()
@@ -50,7 +62,6 @@ namespace Xamarin.Forms.Platform.iOS
                        base.ViewDidLoad();
                        AutomaticallyAdjustsScrollViewInsets = false;
                        RegisterCells();
-                       CollectionView.WeakDelegate = _layout;
                }
 
                public override void ViewWillLayoutSubviews()
index 825f5a3..623ddf5 100644 (file)
@@ -89,7 +89,7 @@ namespace Xamarin.Forms.Platform.iOS
                        _collectionViewController = new CollectionViewController(newElement, _layout);
                        SetNativeControl(_collectionViewController.View);
                        _collectionViewController.CollectionView.BackgroundColor = UIColor.Clear;
-                       _collectionViewController.CollectionView.Delegate = _layout;
+                       _collectionViewController.CollectionView.WeakDelegate = _layout;
 
                        // Listen for ScrollTo requests
                        newElement.ScrollToRequested += ScrollToRequested;
index fb7b504..c0c7971 100644 (file)
@@ -1,7 +1,6 @@
 using System;
 using System.ComponentModel;
 using System.Diagnostics;
-using System.Runtime.CompilerServices;
 using CoreGraphics;
 using Foundation;
 using UIKit;
@@ -13,6 +12,7 @@ namespace Xamarin.Forms.Platform.iOS
                readonly ItemsLayout _itemsLayout;
                bool _determiningCellSize;
                bool _disposed;
+               bool _needCellSizeUpdate;
 
                protected ItemsViewLayout(ItemsLayout itemsLayout)
                {
@@ -55,7 +55,6 @@ namespace Xamarin.Forms.Platform.iOS
 
                protected virtual void HandlePropertyChanged(PropertyChangedEventArgs  propertyChanged)
                {
-                       // Nothing to do here for now; may need something here when we implement Snapping
                }
 
                public nfloat ConstrainedDimension { get; set; }
@@ -67,8 +66,18 @@ namespace Xamarin.Forms.Platform.iOS
 
                public abstract void ConstrainTo(CGSize size);
 
+               [Export("collectionView:willDisplayCell:forItemAtIndexPath:")]
+               public virtual void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath path)
+               {
+                       if (_needCellSizeUpdate)
+                       {
+                               // Our cell size/estimate is out of date, probably because we moved from zero to one item; update it
+                               _needCellSizeUpdate = false;
+                               DetermineCellSize();
+                       }
+               }
+
                [Export("collectionView:layout:insetForSectionAtIndex:")]
-               [CompilerGenerated]
                public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
                        nint section)
                {
@@ -76,7 +85,6 @@ namespace Xamarin.Forms.Platform.iOS
                }
 
                [Export("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:")]
-               [CompilerGenerated]
                public virtual nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView,
                        UICollectionViewLayout layout, nint section)
                {
@@ -84,7 +92,6 @@ namespace Xamarin.Forms.Platform.iOS
                }
 
                [Export("collectionView:layout:minimumLineSpacingForSectionAtIndex:")]
-               [CompilerGenerated]
                public virtual nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView,
                        UICollectionViewLayout layout, nint section)
                {
@@ -129,23 +136,28 @@ namespace Xamarin.Forms.Platform.iOS
 
                        _determiningCellSize = true;
 
-                       if (!Forms.IsiOS10OrNewer)
-                       {
-                               // iOS 9 will throw an exception during auto layout if no EstimatedSize is set
-                               EstimatedItemSize = new CGSize(1, 1);
-                       }
-
+                       // We set the EstimatedItemSize here for two reasons:
+                       // 1. If we don't set it, iOS versions below 10 will crash
+                       // 2. If GetPrototype() cannot return a cell because the items source is empty, we need to have
+                       //              an estimate set so that when a cell _does_ become available (i.e., when the items source
+                       //              has at least one item), Autolayout will kick in for the first cell and size it correctly
+                       // If GetPrototype() _can_ return a cell, this estimate will be updated once that cell is measured
+                       EstimatedItemSize = new CGSize(1, 1);
+                       
                        if (!(GetPrototype() is ItemsViewCell prototype))
                        {
+                               _determiningCellSize = false;
                                return;
                        }
 
+                       // Constrain and measure the prototype cell
                        prototype.ConstrainTo(ConstrainedDimension);
 
                        var measure = prototype.Measure();
 
                        if (UniformSize)
                        {
+                               // This is the size we'll give all of our cells from here on out
                                ItemSize = measure;
 
                                // Make sure autolayout is disabled 
@@ -153,6 +165,7 @@ namespace Xamarin.Forms.Platform.iOS
                        }
                        else
                        {
+                               // Autolayout is now enabled, and this is the size used to guess scrollbar size and progress
                                EstimatedItemSize = measure;
                        }
 
@@ -174,7 +187,7 @@ namespace Xamarin.Forms.Platform.iOS
                        ScrollDirection = scrollDirection;
                }
 
-               void UpdateCellConstraints()
+               internal void UpdateCellConstraints()
                {
                        var cells = CollectionView.VisibleCells;
 
@@ -197,5 +210,10 @@ namespace Xamarin.Forms.Platform.iOS
                        ConstrainTo(size);
                        UpdateCellConstraints();
                }
+
+               public void SetNeedCellSizeUpdate()
+               {
+                       _needCellSizeUpdate = true;
+               }
        }
 }
\ No newline at end of file
index 89f75ff..b9e873d 100644 (file)
@@ -32,7 +32,7 @@ namespace Xamarin.Forms.Platform.iOS
                        nativeView.Frame = new CGRect(CGPoint.Empty, size);
                        VisualElementRenderer.Element.Layout(nativeView.Frame.ToRectangle());
 
-                       layoutAttributes.Frame = VisualElementRenderer.NativeView.Frame;
+                       layoutAttributes.Frame = nativeView.Frame;
 
                        return layoutAttributes;
                }