--- /dev/null
+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
<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" />
{
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);
internal class ObservableCodeCollectionViewGallery : ContentPage
{
public ObservableCodeCollectionViewGallery(ItemsLayoutOrientation orientation = ItemsLayoutOrientation.Vertical,
- bool grid = true)
+ bool grid = true, int initialItems = 1000)
{
var layout = new Grid
{
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);
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)
}
}
};
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";
readonly ItemsView _itemsView;
readonly ItemsViewLayout _layout;
bool _initialConstraintsSet;
+ bool _wasEmpty;
public CollectionViewController(ItemsView itemsView, ItemsViewLayout layout) : base(layout)
{
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()
base.ViewDidLoad();
AutomaticallyAdjustsScrollViewInsets = false;
RegisterCells();
- CollectionView.WeakDelegate = _layout;
}
public override void ViewWillLayoutSubviews()
_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;
using System;
using System.ComponentModel;
using System.Diagnostics;
-using System.Runtime.CompilerServices;
using CoreGraphics;
using Foundation;
using UIKit;
readonly ItemsLayout _itemsLayout;
bool _determiningCellSize;
bool _disposed;
+ bool _needCellSizeUpdate;
protected ItemsViewLayout(ItemsLayout itemsLayout)
{
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; }
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)
{
}
[Export("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:")]
- [CompilerGenerated]
public virtual nfloat GetMinimumInteritemSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
}
[Export("collectionView:layout:minimumLineSpacingForSectionAtIndex:")]
- [CompilerGenerated]
public virtual nfloat GetMinimumLineSpacingForSection(UICollectionView collectionView,
UICollectionViewLayout layout, nint section)
{
_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
}
else
{
+ // Autolayout is now enabled, and this is the size used to guess scrollbar size and progress
EstimatedItemSize = measure;
}
ScrollDirection = scrollDirection;
}
- void UpdateCellConstraints()
+ internal void UpdateCellConstraints()
{
var cells = CollectionView.VisibleCells;
ConstrainTo(size);
UpdateCellConstraints();
}
+
+ public void SetNeedCellSizeUpdate()
+ {
+ _needCellSizeUpdate = true;
+ }
}
}
\ No newline at end of file
nativeView.Frame = new CGRect(CGPoint.Empty, size);
VisualElementRenderer.Element.Layout(nativeView.Frame.ToRectangle());
- layoutAttributes.Frame = VisualElementRenderer.NativeView.Frame;
+ layoutAttributes.Frame = nativeView.Frame;
return layoutAttributes;
}
-using System;
+using System;
using System.Diagnostics;
using CoreGraphics;
using Foundation;