Implement CollectionView grouping on iOS (#6590)
authorE.Z. Hart <hartez@users.noreply.github.com>
Tue, 2 Jul 2019 21:34:45 +0000 (15:34 -0600)
committerGitHub <noreply@github.com>
Tue, 2 Jul 2019 21:34:45 +0000 (15:34 -0600)
* Implement CollectionView grouping for iOS

* Add grouping UI tests

* Fix invalidcast when source is not INotifyCollectionChanged

* Remove old TODO comments

* Apply suggestions from code review

Co-Authored-By: Stephane Delcroix <stephane@delcroix.org>
* IsGroupingEnabled -> IsGrouped

* Update empty source workaround

* Fix incorrect group counting for empty collections

* Iron out all section counts; remove unsavory empty view workaround;

* Fix crash when filter is null

* Handle sections for add/remove to/from empty sources; check for empty sources when handling sections;

* Prevent random test failures for test 5793

* Remove now-unnecessary size check on transition from empty to non-empty source

* Update Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewGrouping.cs

Co-Authored-By: Samantha Houts <samhouts@users.noreply.github.com>
42 files changed:
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewGrouping.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/CollectionViewGallery.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/DemoFilteredItemSource.cs
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingGallery.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ObservableGrouping.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ViewModel.cs [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/MultiTestObservableCollection.cs
Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
Xamarin.Forms.Core/Items/CollectionView.cs
Xamarin.Forms.Core/Items/GroupableItemsView.cs [new file with mode: 0644]
Xamarin.Forms.Core/Items/ListItemsLayout.cs
Xamarin.Forms.Platform.iOS/CollectionView/CollectionViewRenderer.cs
Xamarin.Forms.Platform.iOS/CollectionView/EmptySource.cs
Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewController.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewRenderer.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/HorizontalDefaultSupplementalView.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/HorizontalTemplatedHeaderView.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/IItemsViewSource.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsSourceFactory.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs
Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewLayout.cs
Xamarin.Forms.Platform.iOS/CollectionView/ListSource.cs
Xamarin.Forms.Platform.iOS/CollectionView/ObservableGroupedSource.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/ObservableItemsSource.cs
Xamarin.Forms.Platform.iOS/CollectionView/SelectableItemsViewController.cs
Xamarin.Forms.Platform.iOS/CollectionView/UICollectionViewDelegator.cs
Xamarin.Forms.Platform.iOS/CollectionView/VerticalDefaultSupplementalView.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/CollectionView/VerticalTemplatedHeaderView.cs [new file with mode: 0644]
Xamarin.Forms.Platform.iOS/Xamarin.Forms.Platform.iOS.csproj

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewGrouping.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/CollectionViewGrouping.cs
new file mode 100644 (file)
index 0000000..bf25b99
--- /dev/null
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+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.CollectionView)]
+#endif
+
+       [Preserve(AllMembers = true)]
+       [Issue(IssueTracker.None, 4539135, "CollectionView: Grouping", PlatformAffected.All)]
+       public class CollectionViewGrouping : TestNavigationPage
+       {
+               protected override void Init()
+               {
+#if APP
+                       Device.SetFlags(new List<string>(Device.Flags ?? new List<string>()) { "CollectionView_Experimental" });
+
+                       PushAsync(new GalleryPages.CollectionViewGalleries.GroupingGalleries.ObservableGrouping());
+#endif
+               }
+
+#if UITEST && __IOS__ // Grouping is not implemented on Android yet
+               [Test]
+               public void RemoveSelectedItem()
+               {
+                       RunningApp.WaitForElement("Hawkeye");
+                       RunningApp.Tap("Hawkeye");      
+                       RunningApp.Tap("RemoveItem");
+                       RunningApp.WaitForNoElement("Hawkeye");
+               }
+
+               [Test]
+               public void AddItem()
+               {
+                       RunningApp.WaitForElement("Hawkeye");
+                       RunningApp.Tap("Hawkeye");
+                       RunningApp.Tap("AddItem");
+                       RunningApp.WaitForElement("Spider-Man");
+               }
+
+               [Test]
+               public void ReplaceItem()
+               {
+                       RunningApp.WaitForElement("Iron Man");
+                       RunningApp.Tap("Iron Man");
+                       RunningApp.Tap("ReplaceItem");
+                       RunningApp.WaitForNoElement("Iron Man");
+                       RunningApp.WaitForElement("Spider-Man");
+               }
+
+               [Test]
+               public void RemoveGroup()
+               {
+                       RunningApp.WaitForElement("Avengers");
+                       RunningApp.Tap("RemoveGroup");
+                       RunningApp.WaitForNoElement("Avengers");
+               }
+
+               [Test]
+               public void AddGroup()
+               {
+                       RunningApp.WaitForElement("AddGroup");
+                       RunningApp.Tap("AddGroup");
+                       RunningApp.WaitForElement("Excalibur");
+               }
+
+               [Test]
+               public void ReplaceGroup()
+               {
+                       RunningApp.WaitForElement("Fantastic Four");
+                       RunningApp.Tap("ReplaceGroup");
+                       RunningApp.WaitForElement("Alpha Flight");
+               }
+
+               [Test]
+               public void MoveGroup()
+               {
+                       RunningApp.WaitForElement("MoveGroup");
+                       RunningApp.Tap("MoveGroup");
+               }
+
+
+#endif
+       }
+}
index e383872..8148ca5 100644 (file)
@@ -9,6 +9,7 @@
     <Import_RootNamespace>Xamarin.Forms.Controls.Issues</Import_RootNamespace>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)CollectionViewGrouping.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue5412.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Helpers\GarbageCollectionHelper.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Issue4879.cs" />
index 79e9237..1e06a83 100644 (file)
@@ -1,7 +1,6 @@
 using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.EmptyViewGalleries;
+using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries;
 using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SelectionGalleries;
-using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ItemSizeGalleries;
-using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.SpacingGalleries;
 
 namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 {
@@ -23,8 +22,7 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
                                        GalleryBuilder.NavButton("EmptyView Galleries", () => new EmptyViewGallery(), Navigation),
                                        GalleryBuilder.NavButton("Selection Galleries", () => new SelectionGallery(), Navigation),
                                        GalleryBuilder.NavButton("Propagation Galleries", () => new PropagationGallery(), Navigation),
-                                       GalleryBuilder.NavButton("Item Size Galleries", () => new ItemsSizeGallery(), Navigation),
-                                       GalleryBuilder.NavButton("Spacing Galleries", () => new ItemsSpacingGallery(), Navigation),
+                                       GalleryBuilder.NavButton("Grouping Galleries", () => new GroupingGallery(), Navigation),
                                }
                        };
                }
index 1ea1e44..6f15e93 100644 (file)
@@ -39,7 +39,8 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 
                private bool ItemMatches(string filter, CollectionViewGalleryTestItem item)
                {
-                       return item.Caption.ToLower().Contains(filter.ToLower());
+                       filter = filter ?? "";
+                       return item.Caption.ToLower().Contains(filter?.ToLower());
                }
 
                public void FilterItems(string filter)
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml
new file mode 100644 (file)
index 0000000..5332e54
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.BasicGrouping">
+       <ContentPage.Content>
+               <CollectionView x:Name="CollectionView" IsGrouped="True">
+
+                       <CollectionView.ItemTemplate>
+                               <DataTemplate>
+                                       <StackLayout>
+                                               <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+                                       </StackLayout>
+                               </DataTemplate>
+                       </CollectionView.ItemTemplate>
+                       
+                       <CollectionView.GroupHeaderTemplate>
+                               <DataTemplate>
+                                       
+                                               <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+                                       
+                               </DataTemplate>
+                       </CollectionView.GroupHeaderTemplate>
+                       
+                       <CollectionView.GroupFooterTemplate>
+                               <DataTemplate>
+                                       <StackLayout>
+                                               <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange" 
+                                                          Margin="0,0,0,15"/>
+                                       </StackLayout>
+                               </DataTemplate>
+                       </CollectionView.GroupFooterTemplate>
+                       
+               </CollectionView>
+       </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/BasicGrouping.xaml.cs
new file mode 100644 (file)
index 0000000..694b66c
--- /dev/null
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       [Preserve (AllMembers = true)]
+       public partial class BasicGrouping : ContentPage
+       {
+               public BasicGrouping ()
+               {
+                       InitializeComponent ();
+
+                       CollectionView.ItemsSource = new SuperTeams();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingGallery.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingGallery.cs
new file mode 100644 (file)
index 0000000..00b9142
--- /dev/null
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+    class GroupingGallery : ContentPage
+    {
+               public GroupingGallery()
+               {
+                       var descriptionLabel =
+                               new Label { Text = "Grouping Galleries", Margin = new Thickness(2, 2, 2, 2) };
+
+                       Title = "Grouping Galleries";
+
+                       Content = new ScrollView
+                       {
+                               Content = new StackLayout
+                               {
+                                       Children =
+                                       {
+                                               descriptionLabel,
+                                               GalleryBuilder.NavButton("Basic Grouping", () =>
+                                                       new BasicGrouping(), Navigation),
+                                               GalleryBuilder.NavButton("Grouping, some empty groups", () =>
+                                                       new SomeEmptyGroups(), Navigation),
+                                               GalleryBuilder.NavButton("Grouping, no templates", () =>
+                                                       new GroupingNoTemplates(), Navigation),
+                                               GalleryBuilder.NavButton("Grouping, with selection", () =>
+                                                       new GroupingPlusSelection(), Navigation),
+                                               GalleryBuilder.NavButton("Grouping, switchable", () =>
+                                                       new SwitchGrouping(), Navigation),
+                                               GalleryBuilder.NavButton("Grouping, Observable", () =>
+                                                       new ObservableGrouping(), Navigation),
+                                       }
+                               }
+                       };
+               }
+    }
+}
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml
new file mode 100644 (file)
index 0000000..81991f6
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.GroupingNoTemplates">
+    <ContentPage.Content>
+               <CollectionView x:Name="CollectionView" IsGrouped="True" />
+       </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingNoTemplates.xaml.cs
new file mode 100644 (file)
index 0000000..c152507
--- /dev/null
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class GroupingNoTemplates : ContentPage
+       {
+               public GroupingNoTemplates()
+               {
+                       InitializeComponent();
+                       CollectionView.ItemsSource = new SuperTeams();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml
new file mode 100644 (file)
index 0000000..0dbd226
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.GroupingPlusSelection">
+    <ContentPage.Content>
+         <CollectionView x:Name="CollectionView" IsGrouped="True" 
+                                                SelectionMode="Single">
+
+                       <CollectionView.ItemTemplate>
+                               <DataTemplate>
+                                       <StackLayout>
+                                               <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+                                       </StackLayout>
+                               </DataTemplate>
+                       </CollectionView.ItemTemplate>
+                       
+                       <CollectionView.GroupHeaderTemplate>
+                               <DataTemplate>
+                                       
+                                               <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+                                       
+                               </DataTemplate>
+                       </CollectionView.GroupHeaderTemplate>
+                       
+                       <CollectionView.GroupFooterTemplate>
+                               <DataTemplate>
+                                       <StackLayout>
+                                               <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange" 
+                                                          Margin="0,0,0,15"/>
+                                       </StackLayout>
+                               </DataTemplate>
+                       </CollectionView.GroupFooterTemplate>
+                       
+               </CollectionView>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/GroupingPlusSelection.xaml.cs
new file mode 100644 (file)
index 0000000..cf1506f
--- /dev/null
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class GroupingPlusSelection : ContentPage
+       {
+               public GroupingPlusSelection ()
+               {
+                       InitializeComponent ();
+                       CollectionView.ItemsSource = new SuperTeams();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml
new file mode 100644 (file)
index 0000000..6ce49ef
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.MeasureFirstStrategy">
+    <ContentPage.Content>
+        <StackLayout>
+
+            <Label Text="Using ItemSizingStrategy.MeasureFirstItem. On scrolling, some items may disappear; this is a current known issue ()"></Label>
+
+            <CollectionView x:Name="CollectionView" ItemSizingStrategy="MeasureFirstItem" IsGrouped="True">
+
+                <CollectionView.ItemTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.ItemTemplate>
+
+                <CollectionView.GroupHeaderTemplate>
+                    <DataTemplate>
+
+                        <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+                    </DataTemplate>
+                </CollectionView.GroupHeaderTemplate>
+
+                <CollectionView.GroupFooterTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange" 
+                                                                  Margin="0,0,0,15"/>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.GroupFooterTemplate>
+
+            </CollectionView>
+        </StackLayout>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/MeasureFirstStrategy.xaml.cs
new file mode 100644 (file)
index 0000000..e1b6465
--- /dev/null
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class MeasureFirstStrategy : ContentPage
+       {
+               public MeasureFirstStrategy()
+               {
+                       InitializeComponent();
+
+                       CollectionView.ItemsSource = new SuperTeams();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ObservableGrouping.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ObservableGrouping.cs
new file mode 100644 (file)
index 0000000..d97d508
--- /dev/null
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       internal class ObservableGrouping : ContentPage
+       {
+               public ObservableGrouping()
+               {
+                       Title = "Observable Grouped List";
+
+                       var buttonStyle = new Style(typeof(Button)) { };
+                       buttonStyle.Setters.Add(new Setter() { Property = Button.HeightRequestProperty, Value = 20 });
+                       buttonStyle.Setters.Add(new Setter() { Property = Button.FontSizeProperty, Value = 10 });
+
+                       var layout = new Grid
+                       {
+                               RowDefinitions = new RowDefinitionCollection
+                               {
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Auto },
+                                       new RowDefinition { Height = GridLength.Star }
+                               },
+                               ColumnDefinitions = new ColumnDefinitionCollection
+                               {
+                                       new ColumnDefinition(), new ColumnDefinition()
+                               }
+
+                       };
+
+                       var collectionView = new CollectionView
+                       {
+                               ItemTemplate = ItemTemplate(),
+                               GroupFooterTemplate = GroupFooterTemplate(),
+                               GroupHeaderTemplate = GroupHeaderTemplate(),
+                               IsGrouped = true,
+                               SelectionMode = SelectionMode.Single
+                       };
+
+                       var itemsSource = new ObservableSuperTeams();
+
+                       collectionView.ItemsSource = itemsSource;
+
+                       var remover = new Button { Text = "Remove Selected", AutomationId = "RemoveItem", Style = buttonStyle };
+                       remover.Clicked += (obj, args) => {
+                               var selectedMember = collectionView.SelectedItem as Member;
+                               var team = FindTeam(itemsSource, selectedMember);
+                               team?.Remove(selectedMember);
+                       };
+
+                       var adder = new Button { Text = "Add After Selected", AutomationId = "AddItem", Style = buttonStyle };
+                       adder.Clicked += (obj, args) => {
+                               var selectedMember = collectionView.SelectedItem as Member;
+                               var team = FindTeam(itemsSource, selectedMember);
+
+                               if (team == null)
+                               {
+                                       return;
+                               }
+
+                               team.Insert(team.IndexOf(selectedMember) + 1, new Member("Spider-Man"));
+                       };
+
+                       AddStuffToGridRow(layout, 0, remover, adder);
+
+                       var replacer = new Button { Text = "Replace Selected", AutomationId = "ReplaceItem", Style = buttonStyle };
+                       replacer.Clicked += (obj, args) => {
+                               var selectedMember = collectionView.SelectedItem as Member;
+                               var team = FindTeam(itemsSource, selectedMember);
+
+                               if (team == null)
+                               {
+                                       return;
+                               }
+
+                               team.Insert(team.IndexOf(selectedMember) + 1, new Member("Spider-Man"));
+                               team.Remove(selectedMember);
+                       };
+
+                       var mover = new Button { Text = $"Move Selected To {itemsSource[0].Name}", AutomationId = "MoveItem",
+                               Style = buttonStyle };
+                       mover.Clicked += (obj, args) => {
+                               var selectedMember = collectionView.SelectedItem as Member;
+                               var team = FindTeam(itemsSource, selectedMember);
+
+                               if (team == null || team == itemsSource[0])
+                               {
+                                       return;
+                               }
+
+                               team.Remove(selectedMember);
+                               itemsSource[0].Add(selectedMember);
+                       };
+
+                       AddStuffToGridRow(layout, 1, replacer, mover);
+
+                       var groupRemover = new Button { Text = $"Remove {itemsSource[0].Name}", AutomationId = "RemoveGroup",
+                               Style = buttonStyle };
+                       groupRemover.Clicked += (obj, args) => {
+                               itemsSource?.Remove(itemsSource[0]);
+                               groupRemover.Text = $"Remove {itemsSource[0].Name}";
+                               mover.Text = $"Move Selected To {itemsSource[0].Name}";
+                       };
+
+                       var groupAdder = new Button { Text = $"Insert New Group at position 2", AutomationId = "AddGroup",
+                               Style = buttonStyle };
+                       groupAdder.Clicked += (obj, args) => {
+                               itemsSource?.Insert(1, new ObservableTeam("Excalibur", new List<Member>()));
+                       };
+
+                       AddStuffToGridRow(layout, 2, groupRemover, groupAdder);
+
+                       var groupMover = new Button { Text = "Move 3rd Group to 1st", AutomationId = "MoveGroup", Style = buttonStyle };
+                       groupMover.Clicked += (obj, args) => {
+                               var group = itemsSource[2];
+                               itemsSource.Remove(group);
+                               itemsSource.Insert(0, group);
+                               groupRemover.Text = $"Remove {itemsSource[0].Name}";
+                               mover.Text = $"Move Selected To {itemsSource[0].Name}";
+                       };
+
+                       var groupReplacer = new Button { Text = "Replace 2nd Group", AutomationId = "ReplaceGroup", Style = buttonStyle };
+                       groupReplacer.Clicked += (obj, args) => {
+                               var group = itemsSource[1];
+                               itemsSource.Remove(group);
+                               itemsSource?.Insert(1, new ObservableTeam("Alpha Flight", new List<Member> { new Member("Guardian"),
+                                       new Member("Sasquatch"), new Member("Northstar") }));
+                       };
+
+                       AddStuffToGridRow(layout, 3, groupMover, groupReplacer);
+
+                       layout.Children.Add(collectionView);
+                       Grid.SetRow(collectionView, 6);
+                       Grid.SetColumnSpan(collectionView, 2);
+
+                       Content = layout;
+               }
+
+               void AddStuffToGridRow(Grid grid, int row, params View[] views)
+               {
+                       var col = 0;
+
+                       foreach (var view in views)
+                       {
+                               grid.Children.Add(view);
+                               Grid.SetRow(view, row);
+                               Grid.SetColumn(view, col);
+                               col = col + 1;
+                       }
+               }
+
+               DataTemplate ItemTemplate()
+               {
+                       return new DataTemplate(() =>
+                       {
+                               var layout = new StackLayout();
+                               var label = new Label()
+                               {
+                                       Margin = new Thickness(5, 0, 0, 0),
+
+                               };
+
+                               label.SetBinding(Label.TextProperty, new Binding("Name"));
+                               layout.Children.Add(label);
+
+                               return layout;
+                       });
+               }
+
+               DataTemplate GroupHeaderTemplate()
+               {
+                       return new DataTemplate(() =>
+                       {
+                               var layout = new StackLayout();
+                               var label = new Label()
+                               {
+                                       FontSize = 16,
+                                       FontAttributes = FontAttributes.Bold,
+                                       BackgroundColor = Color.LightGreen
+
+                               };
+
+                               label.SetBinding(Label.TextProperty, new Binding("Name"));
+                               layout.Children.Add(label);
+
+                               return layout;
+                       });
+               }
+
+               DataTemplate GroupFooterTemplate()
+               {
+                       return new DataTemplate(() =>
+                       {
+                               var layout = new StackLayout();
+                               var label = new Label()
+                               {
+                                       Margin = new Thickness(0, 0, 0, 15),
+                                       BackgroundColor = Color.LightBlue
+                               };
+
+                               label.SetBinding(Label.TextProperty, new Binding("Count", stringFormat: "Total members: {0:D}"));
+                               layout.Children.Add(label);
+
+                               return layout;
+                       });
+               }
+
+               ObservableTeam FindTeam(ObservableSuperTeams teams, Member member)
+               {
+                       if (member == null)
+                       {
+                               return null;
+                       }
+
+                       for (int i = 0; i < teams.Count; i++)
+                       {
+                               var group = teams[i];
+                               
+                               if (group.Contains(member))
+                               {
+                                       return group;
+                               }
+                       }
+
+                       return null;
+               }
+       }
+}
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml
new file mode 100644 (file)
index 0000000..c55fbd6
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.SomeEmptyGroups">
+    <ContentPage.Content>
+
+        <StackLayout>
+
+            <Label x:Name="Description">
+                <Label.Text>
+                    <OnPlatform x:TypeArguments="x:String" 
+                                Default="The CollectionView below should be grouped and some of the groups should be empty, but still display headers/footers.">
+                        <On Platform="iOS">The CollectionView below should be grouped and some of the groups should be empty, but still display headers/footers. On iOS 10 and lower, the group headers/footers may all be clumped at the top; this is a known issue.</On>
+                    </OnPlatform>
+                </Label.Text>
+                
+            </Label>
+
+            <CollectionView x:Name="CollectionView" IsGrouped="True">
+
+                <CollectionView.ItemTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.ItemTemplate>
+
+                <CollectionView.GroupHeaderTemplate>
+                    <DataTemplate>
+
+                        <Label  Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+
+                    </DataTemplate>
+                </CollectionView.GroupHeaderTemplate>
+
+                <CollectionView.GroupFooterTemplate>
+                    <DataTemplate>
+                        <StackLayout>
+                            <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange" 
+                                                          Margin="0,0,0,15"/>
+                        </StackLayout>
+                    </DataTemplate>
+                </CollectionView.GroupFooterTemplate>
+
+            </CollectionView>
+
+        </StackLayout>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SomeEmptyGroups.xaml.cs
new file mode 100644 (file)
index 0000000..4f1437a
--- /dev/null
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class SomeEmptyGroups : ContentPage
+       {
+               public SomeEmptyGroups()
+               {
+                       InitializeComponent();
+
+                       var teams = new List<Team>
+                       {
+                               new Team("Avengers", new List<Member>
+                               {
+                                       new Member("Thor"),
+                                       new Member("Captain America")
+                               }),
+
+                               new Team("Thundercats", new List<Member>()),
+
+                               new Team("Avengers", new List<Member>
+                               {
+                                       new Member("Thor"),
+                                       new Member("Captain America")
+                               }),
+                                                                               
+                               new Team("Bionic Six", new List<Member>()),
+
+                               new Team("Fantastic Four", new List<Member>
+                               {
+                                       new Member("The Thing"),
+                                       new Member("The Human Torch"),
+                                       new Member("The Invisible Woman"),
+                                       new Member("Mr. Fantastic"),
+                               })
+                       };
+
+                       CollectionView.ItemsSource = teams;
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml
new file mode 100644 (file)
index 0000000..00260e9
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
+             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+             x:Class="Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries.SwitchGrouping">
+    <ContentPage.Content>
+        <StackLayout>
+                       
+                       <StackLayout Orientation="Horizontal">
+                               <Label Text="Is Grouped:"></Label><Switch x:Name="GroupingSwitch" 
+                    BindingContext="{x:Reference Name=CollectionView}" IsToggled="{Binding IsGrouped}"></Switch>
+                       </StackLayout>
+                       <CollectionView x:Name="CollectionView">
+                               
+                               <CollectionView.ItemTemplate>
+                                       <DataTemplate>
+                                               <StackLayout>
+                                                       <Label Text="{Binding Name}" Margin="5,0,0,0"/>
+                                               </StackLayout>
+                                       </DataTemplate>
+                               </CollectionView.ItemTemplate>
+                       
+                               <CollectionView.GroupHeaderTemplate>
+                                       <DataTemplate>
+                                       
+                                                       <Label Text="{Binding Name}" BackgroundColor="LightGreen" FontSize="16" FontAttributes="Bold"/>
+                                       
+                                       </DataTemplate>
+                               </CollectionView.GroupHeaderTemplate>
+                       
+                               <CollectionView.GroupFooterTemplate>
+                                       <DataTemplate>
+                                               <StackLayout>
+                                                       <Label Text="{Binding Count, StringFormat='{}Total members: {0:D}'}" BackgroundColor="Orange" 
+                                                                  Margin="0,0,0,15"/>
+                                               </StackLayout>
+                                       </DataTemplate>
+                               </CollectionView.GroupFooterTemplate>
+                       
+                       </CollectionView>
+        </StackLayout>
+    </ContentPage.Content>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/SwitchGrouping.xaml.cs
new file mode 100644 (file)
index 0000000..87a6e13
--- /dev/null
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Xamarin.Forms;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class SwitchGrouping : ContentPage
+       {
+               public SwitchGrouping()
+               {
+                       InitializeComponent();
+
+                       CollectionView.ItemsSource = new SuperTeams();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ViewModel.cs b/Xamarin.Forms.Controls/GalleryPages/CollectionViewGalleries/GroupingGalleries/ViewModel.cs
new file mode 100644 (file)
index 0000000..59b2184
--- /dev/null
@@ -0,0 +1,213 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries
+{
+       [Preserve(AllMembers = true)]
+       class Team : List<Member>
+       {
+               public Team(string name, List<Member> members) : base(members)
+               {
+                       Name = name;
+               }
+
+               public string Name { get; set; }
+
+               public override string ToString()
+               {
+                       return Name;
+               }
+       }
+
+       [Preserve(AllMembers = true)]
+       class Member
+       {
+               public Member(string name) => Name = name;
+
+               public string Name { get; set; }
+
+               public override string ToString()
+               {
+                       return Name;
+               }
+       }
+
+       [Preserve(AllMembers = true)]
+       class SuperTeams : List<Team>
+       {
+               public SuperTeams()
+               {
+                       Add(new Team("Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Thor"),
+                                       new Member("Captain America"),
+                                       new Member("Iron Man"),
+                                       new Member("The Hulk"),
+                                       new Member("Ant-Man"),
+                                       new Member("Wasp"),
+                                       new Member("Hawkeye"),
+                                       new Member("Black Panther"),
+                                       new Member("Black Widow"),
+                                       new Member("Doctor Druid"),
+                                       new Member("She-Hulk"),
+                                       new Member("Mockingbird"),
+                               }
+                       ));
+
+                       Add(new Team("Fantastic Four", 
+                               new List<Member>
+                               {
+                                       new Member("The Thing"),
+                                       new Member("The Human Torch"),
+                                       new Member("The Invisible Woman"),
+                                       new Member("Mr. Fantastic"),
+                               }
+                       ));
+
+                       Add(new Team("Defenders", 
+                               new List<Member>
+                               {
+                                       new Member("Doctor Strange"),
+                                       new Member("Namor"),
+                                       new Member("Hulk"),
+                                       new Member("Silver Surfer"),
+                                       new Member("Hellcat"),
+                                       new Member("Nighthawk"),
+                                       new Member("Yellowjacket"),
+                               }
+                       ));
+                       
+                       Add(new Team("Heroes for Hire", 
+                               new List<Member>
+                               {
+                                       new Member("Luke Cage"),
+                                       new Member("Iron Fist"),
+                                       new Member("Misty Knight"),
+                                       new Member("Colleen Wing"),
+                                       new Member("Shang-Chi"),
+                               }
+                       ));
+
+                       Add(new Team("West Coast Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Hawkeye"),
+                                       new Member("Mockingbird"),
+                                       new Member("War Machine"),
+                                       new Member("Wonder Man"),
+                                       new Member("Tigra"),
+                               }
+                       ));
+
+                       Add(new Team("Great Lakes Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Squirrel Girl"),
+                                       new Member("Dinah Soar"),
+                                       new Member("Mr. Immortal"),
+                                       new Member("Flatman"),
+                                       new Member("Doorman"),
+                               }
+                       ));
+               }
+       }
+
+       [Preserve(AllMembers = true)]
+       class ObservableTeam : ObservableCollection<Member>
+       {
+               public ObservableTeam(string name, List<Member> members) : base(members)
+               {
+                       Name = name;
+               }
+
+               public string Name { get; set; }
+
+               public override string ToString()
+               {
+                       return Name;
+               }
+       }
+
+       [Preserve(AllMembers = true)]
+       class ObservableSuperTeams : ObservableCollection<ObservableTeam>
+       {
+               public ObservableSuperTeams ()
+               {
+                       Add(new ObservableTeam("Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Thor"),
+                                       new Member("Captain America"),
+                                       new Member("Iron Man"),
+                                       new Member("The Hulk"),
+                                       new Member("Ant-Man"),
+                                       new Member("Wasp"),
+                                       new Member("Hawkeye"),
+                                       new Member("Black Panther"),
+                                       new Member("Black Widow"),
+                                       new Member("Doctor Druid"),
+                                       new Member("She-Hulk"),
+                                       new Member("Mockingbird"),
+                               }
+                       ));
+
+                       Add(new ObservableTeam("Fantastic Four", 
+                               new List<Member>
+                               {
+                                       new Member("The Thing"),
+                                       new Member("The Human Torch"),
+                                       new Member("The Invisible Woman"),
+                                       new Member("Mr. Fantastic"),
+                               }
+                       ));
+
+                       Add(new ObservableTeam("Defenders", 
+                               new List<Member>
+                               {
+                                       new Member("Doctor Strange"),
+                                       new Member("Namor"),
+                                       new Member("Hulk"),
+                                       new Member("Silver Surfer"),
+                                       new Member("Hellcat"),
+                                       new Member("Nighthawk"),
+                                       new Member("Yellowjacket"),
+                               }
+                       ));
+                       
+                       Add(new ObservableTeam("Heroes for Hire", 
+                               new List<Member>
+                               {
+                                       new Member("Luke Cage"),
+                                       new Member("Iron Fist"),
+                                       new Member("Misty Knight"),
+                                       new Member("Colleen Wing"),
+                                       new Member("Shang-Chi"),
+                               }
+                       ));
+
+                       Add(new ObservableTeam("West Coast Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Hawkeye"),
+                                       new Member("Mockingbird"),
+                                       new Member("War Machine"),
+                                       new Member("Wonder Man"),
+                                       new Member("Tigra"),
+                               }
+                       ));
+
+                       Add(new ObservableTeam("Great Lakes Avengers", 
+                               new List<Member>
+                               {
+                                       new Member("Squirrel Girl"),
+                                       new Member("Dinah Soar"),
+                                       new Member("Mr. Immortal"),
+                                       new Member("Flatman"),
+                                       new Member("Doorman"),
+                               }
+                       ));
+               }
+       }
+}
index 511a257..66e54c7 100644 (file)
@@ -2,11 +2,16 @@
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Linq;
+using Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.GroupingGalleries;
 
 namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 {
        internal class MultiTestObservableCollection<T> : List<T>, INotifyCollectionChanged
        {
+               public MultiTestObservableCollection(List<T> members) : base(members) { }
+
+               public MultiTestObservableCollection() { }
+
                // This is a testing class which implements INotifyCollectionChanged and, unlike the regular
                // ObservableCollection, will actually fire Add and Remove with multiple items at once
 
@@ -77,7 +82,12 @@ namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries
 
                public void TestReset()
                {
+                       var random = new Random();
+                       var randomized = GetRange(1, Count - 1).Select(item => new { Item = item, Index = random.Next(100000) })
+                               .OrderBy(x => x.Index).Select(x => x.Item).ToList();
+
                        RemoveRange(0, Count);
+                       InsertRange(0, randomized);
 
                        var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
                        OnNotifyCollectionChanged(this, args);
index 5ef591f..a9a73ba 100644 (file)
       <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
     </EmbeddedResource>
   </ItemGroup>
+
+  <ItemGroup>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\BasicGrouping.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\GroupingNoTemplates.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\GroupingPlusSelection.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\MeasureFirstStrategy.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\SomeEmptyGroups.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+    <None Update="GalleryPages\CollectionViewGalleries\GroupingGalleries\SwitchGrouping.xaml">
+      <Generator>MSBuild:Compile</Generator>
+    </None>
+  </ItemGroup>
   <Target Name="CreateControllGalleryConfig" BeforeTargets="Build">
     <CreateItem Include="blank.config">
       <Output TaskParameter="Include" ItemName="ConfigFile" />
index e49e84d..0021141 100644 (file)
@@ -10,7 +10,7 @@ using Xamarin.Forms.Platform;
 namespace Xamarin.Forms
 {
        [RenderWith(typeof(_CollectionViewRenderer))]
-       public class CollectionView : SelectableItemsView
+       public class CollectionView : GroupableItemsView
        {
                internal const string CollectionViewExperimental = "CollectionView_Experimental";
 
diff --git a/Xamarin.Forms.Core/Items/GroupableItemsView.cs b/Xamarin.Forms.Core/Items/GroupableItemsView.cs
new file mode 100644 (file)
index 0000000..27664e0
--- /dev/null
@@ -0,0 +1,32 @@
+namespace Xamarin.Forms
+{
+       public class GroupableItemsView : SelectableItemsView
+       {
+               public static readonly BindableProperty IsGroupedProperty =
+                       BindableProperty.Create(nameof(IsGrouped), typeof(bool), typeof(GroupableItemsView), false);
+
+               public bool IsGrouped
+               {
+                       get => (bool)GetValue(IsGroupedProperty);
+                       set => SetValue(IsGroupedProperty, value);
+               }
+
+               public static readonly BindableProperty GroupHeaderTemplateProperty =
+                       BindableProperty.Create(nameof(GroupHeaderTemplate), typeof(DataTemplate), typeof(GroupableItemsView), default(DataTemplate));
+
+               public DataTemplate GroupHeaderTemplate
+               {
+                       get => (DataTemplate)GetValue(GroupHeaderTemplateProperty);
+                       set => SetValue(GroupHeaderTemplateProperty, value);
+               }
+
+               public static readonly BindableProperty GroupFooterTemplateProperty =
+                       BindableProperty.Create(nameof(GroupFooterTemplate), typeof(DataTemplate), typeof(GroupableItemsView), default(DataTemplate));
+
+               public DataTemplate GroupFooterTemplate
+               {
+                       get => (DataTemplate)GetValue(GroupFooterTemplateProperty);
+                       set => SetValue(GroupFooterTemplateProperty, value);
+               }
+       }
+}
index 6400f48..b64678d 100644 (file)
@@ -4,7 +4,7 @@ namespace Xamarin.Forms
 {
        public class ListItemsLayout : ItemsLayout
        {
-               public ListItemsLayout(ItemsLayoutOrientation orientation) : base(orientation)
+               public ListItemsLayout([Parameter("Orientation")] ItemsLayoutOrientation orientation) : base(orientation)
                {
                }
 
index 5bd2a96..23a6a66 100644 (file)
@@ -1,4 +1,4 @@
 namespace Xamarin.Forms.Platform.iOS
 {
-       public class CollectionViewRenderer : SelectableItemsViewRenderer { }
+       public class CollectionViewRenderer : GroupableItemsViewRenderer { }
 }
\ No newline at end of file
index 123709b..3aa0447 100644 (file)
@@ -1,12 +1,30 @@
 using System;
+using Foundation;
 
 namespace Xamarin.Forms.Platform.iOS
 {
-       sealed class EmptySource : IItemsViewSource
+       internal class EmptySource : IItemsViewSource
        {
-               public int Count => 0;
+               public int GroupCount => 0;
 
-               public object this[int index] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
+               public int ItemCount => 0;
+
+               public object this[NSIndexPath indexPath] => throw new IndexOutOfRangeException("IItemsViewSource is empty");
+
+               public int ItemCountInGroup(nint group)
+               {
+                       return 0;
+               }
+
+               public object Group(NSIndexPath indexPath)
+               {
+                       throw new IndexOutOfRangeException("IItemsViewSource is empty");
+               }
+
+               public NSIndexPath GetIndexForItem(object item)
+               {
+                       throw new IndexOutOfRangeException("IItemsViewSource is empty");
+               }
 
                public void Dispose()
                {
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewController.cs b/Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewController.cs
new file mode 100644 (file)
index 0000000..1c76203
--- /dev/null
@@ -0,0 +1,174 @@
+using System;
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       public class GroupableItemsViewController : SelectableItemsViewController
+       {
+               GroupableItemsView GroupableItemsView => (GroupableItemsView)ItemsView;
+
+               // Keep a cached value for the current state of grouping around so we can avoid hitting the 
+               // BindableProperty all the time 
+               bool _isGrouped;
+
+               public GroupableItemsViewController(GroupableItemsView groupableItemsView, ItemsViewLayout layout) 
+                       : base(groupableItemsView, layout)
+               {
+                       _isGrouped = GroupableItemsView.IsGrouped;
+               }
+
+               protected override IItemsViewSource CreateItemsViewSource()
+               {
+                       // Use the BindableProperty here (instead of _isGroupingEnabled) because the cached value might not be set yet
+                       if (GroupableItemsView.IsGrouped) 
+                       {
+                               return ItemsSourceFactory.CreateGrouped(GroupableItemsView.ItemsSource, CollectionView);
+                       }
+
+                       return base.CreateItemsViewSource();
+               }
+
+               public override void UpdateItemsSource()
+               {
+                       _isGrouped = GroupableItemsView.IsGrouped;
+                       base.UpdateItemsSource();
+               }
+
+               protected override void RegisterViewTypes()
+               {
+                       base.RegisterViewTypes();
+
+                       RegisterSupplementaryViews(UICollectionElementKindSection.Header);
+                       RegisterSupplementaryViews(UICollectionElementKindSection.Footer);
+               }
+
+               private void RegisterSupplementaryViews(UICollectionElementKindSection kind)
+               {
+                       CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalTemplatedSupplementalView),
+                               kind, HorizontalTemplatedSupplementalView.ReuseId);
+                       CollectionView.RegisterClassForSupplementaryView(typeof(VerticalTemplatedSupplementalView),
+                               kind, VerticalTemplatedSupplementalView.ReuseId);
+                       CollectionView.RegisterClassForSupplementaryView(typeof(HorizontalDefaultSupplementalView),
+                               kind, HorizontalDefaultSupplementalView.ReuseId);
+                       CollectionView.RegisterClassForSupplementaryView(typeof(VerticalDefaultSupplementalView),
+                               kind, VerticalDefaultSupplementalView.ReuseId);
+               }
+
+               public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, 
+                       NSString elementKind, NSIndexPath indexPath)
+               {
+                       var reuseId = DetermineViewReuseId(elementKind);
+
+                       var view = collectionView.DequeueReusableSupplementaryView(elementKind, reuseId, indexPath) as UICollectionReusableView;
+
+                       switch (view)
+                       {
+                               case DefaultCell defaultCell:
+                                       UpdateDefaultSupplementaryView(defaultCell, elementKind, indexPath);
+                                       break;
+                               case TemplatedCell templatedCell:
+                                       UpdateTemplatedSupplementaryView(templatedCell, elementKind, indexPath);
+                                       break;
+                       }
+
+                       return view;
+               }
+
+               void UpdateDefaultSupplementaryView(DefaultCell cell, NSString elementKind, NSIndexPath indexPath)
+               {
+                       cell.Label.Text = ItemsSource.Group(indexPath).ToString();
+
+                       if (cell is ItemsViewCell constrainedCell)
+                       {
+                               cell.ConstrainTo(ItemsViewLayout.ConstrainedDimension);
+                       }
+               }
+
+               void UpdateTemplatedSupplementaryView(TemplatedCell cell, NSString elementKind, NSIndexPath indexPath)
+               {
+                       ApplyTemplateAndDataContext(cell, elementKind, indexPath);
+
+                       if (cell is ItemsViewCell constrainedCell)
+                       {
+                               cell.ConstrainTo(ItemsViewLayout.ConstrainedDimension);
+                       }
+               }
+
+               void ApplyTemplateAndDataContext(TemplatedCell cell, NSString elementKind, NSIndexPath indexPath)
+               {
+                       DataTemplate template;
+
+                       if (elementKind == UICollectionElementKindSectionKey.Header)
+                       {
+                               template = GroupableItemsView.GroupHeaderTemplate;
+                       }
+                       else
+                       {
+                               template = GroupableItemsView.GroupFooterTemplate;
+                       }
+
+                       var templateElement = template.CreateContent() as View;
+                       var renderer = CreateRenderer(templateElement);
+
+                       BindableObject.SetInheritedBindingContext(renderer.Element, ItemsSource.Group(indexPath));
+                       cell.SetRenderer(renderer);
+               }
+
+               string DetermineViewReuseId(NSString elementKind)
+               {
+                       if (elementKind == UICollectionElementKindSectionKey.Header)
+                       {
+                               return DetermineViewReuseId(GroupableItemsView.GroupHeaderTemplate);
+                       }
+
+                       return DetermineViewReuseId(GroupableItemsView.GroupFooterTemplate);
+               }
+
+               string DetermineViewReuseId(DataTemplate template)
+               {
+                       if (template == null)
+                       {
+                               // No template, fall back the the default supplemental views
+                               return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+                                       ? HorizontalDefaultSupplementalView.ReuseId
+                                       : VerticalDefaultSupplementalView.ReuseId;
+                       }
+
+                       return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+                               ? HorizontalTemplatedSupplementalView.ReuseId
+                               : VerticalTemplatedSupplementalView.ReuseId;
+               }
+
+               internal CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+               {
+                       if (!_isGrouped)
+                       {
+                               return CGSize.Empty;
+                       }
+
+                       // Currently we explicitly measure all of the headers/footers 
+                       // Long-term, we might want to look at performance hints (similar to ItemSizingStrategy) for 
+                       // headers/footers (if the dev knows for sure they'll all the be the same size)
+
+                       var cell = GetViewForSupplementaryElement(collectionView, UICollectionElementKindSectionKey.Header, 
+                               NSIndexPath.FromItemSection(0, section)) as ItemsViewCell;
+
+                       return cell.Measure();
+               }
+
+               internal CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+               {
+                       if (!_isGrouped)
+                       {
+                               return CGSize.Empty;
+                       }
+
+                       var cell = GetViewForSupplementaryElement(collectionView, UICollectionElementKindSectionKey.Footer, 
+                               NSIndexPath.FromItemSection(0, section)) as ItemsViewCell;
+
+                       return cell.Measure();
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewRenderer.cs b/Xamarin.Forms.Platform.iOS/CollectionView/GroupableItemsViewRenderer.cs
new file mode 100644 (file)
index 0000000..c6ad925
--- /dev/null
@@ -0,0 +1,25 @@
+using System.ComponentModel;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       public class GroupableItemsViewRenderer : SelectableItemsViewRenderer
+       {
+               GroupableItemsView GroupableItemsView => (GroupableItemsView)Element;
+               GroupableItemsViewController GroupableItemsViewController => (GroupableItemsViewController)ItemsViewController;
+
+               protected override ItemsViewController CreateController(ItemsView itemsView, ItemsViewLayout layout)
+               {
+                       return new GroupableItemsViewController(itemsView as GroupableItemsView, layout);
+               }
+
+               protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs changedProperty)
+               {
+                       base.OnElementPropertyChanged(sender, changedProperty);
+
+                       if (changedProperty.Is(GroupableItemsView.IsGroupedProperty))
+                       {
+                               GroupableItemsViewController?.UpdateItemsSource();
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/HorizontalDefaultSupplementalView.cs b/Xamarin.Forms.Platform.iOS/CollectionView/HorizontalDefaultSupplementalView.cs
new file mode 100644 (file)
index 0000000..a77ab1a
--- /dev/null
@@ -0,0 +1,31 @@
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       internal sealed class HorizontalDefaultSupplementalView : DefaultCell
+       {
+               public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.HorizontalDefaultSupplementalView");
+
+               [Export("initWithFrame:")]
+               public HorizontalDefaultSupplementalView(CGRect frame) : base(frame)
+               {
+                       Label.Font = UIFont.PreferredHeadline;
+
+                       Constraint = Label.HeightAnchor.ConstraintEqualTo(Frame.Height);
+                       Constraint.Active = true;
+               }
+
+               public override void ConstrainTo(CGSize constraint)
+               {
+                       Constraint.Constant = constraint.Height;
+               }
+
+               public override CGSize Measure()
+               {
+                       return new CGSize(Label.IntrinsicContentSize.Width, Constraint.Constant);
+               }
+       }
+
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/HorizontalTemplatedHeaderView.cs b/Xamarin.Forms.Platform.iOS/CollectionView/HorizontalTemplatedHeaderView.cs
new file mode 100644 (file)
index 0000000..3069ae4
--- /dev/null
@@ -0,0 +1,32 @@
+using CoreGraphics;
+using Foundation;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       public class HorizontalTemplatedSupplementalView : TemplatedCell
+       {
+               public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.HorizontalTemplatedSupplementalView");
+
+               [Export("initWithFrame:")]
+               public HorizontalTemplatedSupplementalView(CGRect frame) : base(frame)
+               {
+               }
+
+               public override CGSize Measure()
+               {
+                       var measure = VisualElementRenderer.Element.Measure(double.PositiveInfinity, 
+                               ConstrainedDimension, MeasureFlags.IncludeMargins);
+
+                       var width = VisualElementRenderer.Element.Width > 0 
+                               ? VisualElementRenderer.Element.Width : measure.Request.Width;
+
+                       return new CGSize(width, ConstrainedDimension);
+               }
+
+               public override void ConstrainTo(CGSize constraint)
+               {
+                       ConstrainedDimension = constraint.Height;
+                       Layout(constraint);
+               }
+       }
+}
\ No newline at end of file
index 6ebbd52..0adaa32 100644 (file)
@@ -2,9 +2,13 @@
 
 namespace Xamarin.Forms.Platform.iOS
 {
-       internal interface IItemsViewSource : IDisposable
+       public interface IItemsViewSource : IDisposable
        {
-               int Count { get; }
-               object this[int index] { get; }
+               int ItemCount { get; }
+               int ItemCountInGroup(nint group);
+               int GroupCount { get; }
+               object this[Foundation.NSIndexPath indexPath] { get; }
+               object Group(Foundation.NSIndexPath indexPath);
+               Foundation.NSIndexPath GetIndexForItem(object item);
        }
 }
\ No newline at end of file
index 7fc4d7e..e637cf3 100644 (file)
@@ -24,5 +24,15 @@ namespace Xamarin.Forms.Platform.iOS
 
                        return new ListSource(itemsSource);
                }
+
+               public static IItemsViewSource CreateGrouped(IEnumerable itemsSource, UICollectionView collectionView)
+               {
+                       if (itemsSource == null)
+                       {
+                               return new EmptySource();
+                       }
+
+                       return new ObservableGroupedSource(itemsSource, collectionView);
+               }
        }
 }
\ No newline at end of file
index ebe06c5..2a1724a 100644 (file)
@@ -9,12 +9,11 @@ namespace Xamarin.Forms.Platform.iOS
        // TODO hartez 2018/06/01 14:21:24 Add a method for updating the layout 
        public class ItemsViewController : UICollectionViewController
        {
-               IItemsViewSource _itemsSource;
-               readonly ItemsView _itemsView;
-               ItemsViewLayout _layout;
+               protected IItemsViewSource ItemsSource { get; set; }
+               public ItemsView ItemsView { get; }
+               protected ItemsViewLayout ItemsViewLayout { get; set; }
                bool _initialConstraintsSet;
-               bool _safeForReload;
-               bool _wasEmpty;
+               bool _isEmpty;
                bool _currentBackgroundIsEmptyView;
                bool _disposed;
 
@@ -26,35 +25,31 @@ namespace Xamarin.Forms.Platform.iOS
 
                public ItemsViewController(ItemsView itemsView, ItemsViewLayout layout) : base(layout)
                {
-                       _itemsView = itemsView;
-                       _itemsSource = ItemsSourceFactory.Create(_itemsView.ItemsSource, CollectionView);
-                       
-                       // If we already have data, the UICollectionView will have items and we'll be safe to call
-                       // ReloadData if the ItemsSource changes in the future (see UpdateItemsSource for more).
-                       _safeForReload = _itemsSource?.Count > 0;
+                       ItemsView = itemsView;
+                       ItemsSource = CreateItemsViewSource();
 
                        UpdateLayout(layout);
                }
 
                public void UpdateLayout(ItemsViewLayout layout)
                {
-                       _layout = layout;
-                       _layout.GetPrototype = GetPrototype;
+                       ItemsViewLayout = layout;
+                       ItemsViewLayout.GetPrototype = GetPrototype;
 
                        // If we're updating from a previous layout, we should keep any settings for the SelectableItemsViewController around
                        var selectableItemsViewController = Delegator?.SelectableItemsViewController;
-                       Delegator = new UICollectionViewDelegator(_layout, this);
+                       Delegator = new UICollectionViewDelegator(ItemsViewLayout, this);
 
                        CollectionView.Delegate = Delegator;
 
-                       if (CollectionView.CollectionViewLayout != _layout)
+                       if (CollectionView.CollectionViewLayout != ItemsViewLayout)
                        {
                                // We're updating from a previous layout
 
                                // Make sure the new layout is sized properly
-                               _layout.ConstrainTo(CollectionView.Bounds.Size);
+                               ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
                                
-                               CollectionView.SetCollectionViewLayout(_layout, false);
+                               CollectionView.SetCollectionViewLayout(ItemsViewLayout, false);
                                
                                // Reload the data so the currently visible cells get laid out according to the new layout
                                CollectionView.ReloadData();
@@ -67,7 +62,7 @@ namespace Xamarin.Forms.Platform.iOS
                        {
                                if (disposing)
                                {
-                                       _itemsSource?.Dispose();
+                                       ItemsSource?.Dispose();
                                }
 
                                _disposed = true;
@@ -78,7 +73,7 @@ namespace Xamarin.Forms.Platform.iOS
 
                public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
                {
-                       var cell = collectionView.DequeueReusableCell(DetermineCellReusedId(), indexPath) as UICollectionViewCell;
+                       var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(), indexPath) as UICollectionViewCell;
 
                        switch (cell)
                        {
@@ -95,27 +90,30 @@ namespace Xamarin.Forms.Platform.iOS
 
                public override nint GetItemsCount(UICollectionView collectionView, nint section)
                {
-                       var count = _itemsSource.Count;
+                       var count = ItemsSource.ItemCountInGroup(section);
 
-                       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();
-                       }
+                       CheckForEmptySource();
+
+                       return count;
+               }
 
-                       _wasEmpty = count == 0;
+               void CheckForEmptySource()
+               {
+                       var wasEmpty = _isEmpty;
 
-                       UpdateEmptyViewVisibility(_wasEmpty);
+                       _isEmpty = ItemsSource.ItemCount == 0;
 
-                       return count;
+                       if (wasEmpty != _isEmpty)
+                       {
+                               UpdateEmptyViewVisibility(_isEmpty);
+                       }
                }
 
                public override void ViewDidLoad()
                {
                        base.ViewDidLoad();
                        AutomaticallyAdjustsScrollViewInsets = false;
-                       RegisterCells();
+                       RegisterViewTypes();
                }
 
                public override void ViewWillLayoutSubviews()
@@ -128,73 +126,36 @@ namespace Xamarin.Forms.Platform.iOS
                        // are set up the first time this method is called.
                        if (!_initialConstraintsSet)
                        {
-                               _layout.ConstrainTo(CollectionView.Bounds.Size);
+                               ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
                                _initialConstraintsSet = true;
                        }
                }
 
-               public virtual void UpdateItemsSource()
+               protected virtual IItemsViewSource CreateItemsViewSource()
                {
-                       if (_safeForReload)
-                       {
-                               UpdateItemsSourceAndReload();
-                       }
-                       else
-                       {
-                               // Okay, thus far this UICollectionView has never had any items in it. At this point, if
-                               // we set the ItemsSource and try to call ReloadData(), it'll crash. AFAICT this is a bug, but
-                               // until it's fixed (or we can figure out another way to go from empty -> having items), we'll
-                               // have to use this crazy workaround
-                               EmptyCollectionViewReloadWorkaround();
-                       }
+                       return ItemsSourceFactory.Create(ItemsView.ItemsSource, CollectionView);
                }
 
-               void UpdateItemsSourceAndReload()
+               public virtual void UpdateItemsSource()
                {
-                       _itemsSource = ItemsSourceFactory.Create(_itemsView.ItemsSource, CollectionView);
+                       ItemsSource = CreateItemsViewSource();
                        CollectionView.ReloadData();
                        CollectionView.CollectionViewLayout.InvalidateLayout();
                }
 
-               void EmptyCollectionViewReloadWorkaround()
+               public override nint NumberOfSections(UICollectionView collectionView)
                {
-                       var enumerator = _itemsView.ItemsSource.GetEnumerator();
-
-                       if (!enumerator.MoveNext())
-                       {
-                               // The source we're updating to is empty, so we can just update as normal; it won't crash
-                               UpdateItemsSourceAndReload();
-                       }
-                       else
-                       {
-                               // Grab the first item from the new ItemsSource and create a usable source for the UICollectionView
-                               // from that
-                               var firstItem = new List<object> { enumerator.Current };
-                               _itemsSource = ItemsSourceFactory.Create(firstItem, CollectionView);
-
-                               // Insert that item into the UICollectionView
-                               // TODO ezhart When we implement grouping, this will need to be the index of the first actual item
-                               // Which might not be zero,zero if we have empty groups
-                               var indexesToInsert = new NSIndexPath[1] { NSIndexPath.Create(0, 0) };
-
-                               UIView.PerformWithoutAnimation(() =>
-                               {
-                                       CollectionView.InsertItems(indexesToInsert);
-                               });
-
-                               // Okay, from now on we can just call ReloadData and things will work fine
-                               _safeForReload = true;
-                               UpdateItemsSource();
-                       }
+                       CheckForEmptySource(); 
+                       return ItemsSource.GroupCount;
                }
 
                protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath)
                {
-                       cell.Label.Text = _itemsSource[indexPath.Row].ToString();
+                       cell.Label.Text = ItemsSource[indexPath].ToString();
 
                        if (cell is ItemsViewCell constrainedCell)
                        {
-                               _layout.PrepareCellForLayout(constrainedCell);
+                               ItemsViewLayout.PrepareCellForLayout(constrainedCell);
                        }
                }
 
@@ -204,35 +165,27 @@ namespace Xamarin.Forms.Platform.iOS
 
                        if (cell is ItemsViewCell constrainedCell)
                        {
-                               _layout.PrepareCellForLayout(constrainedCell);
+                               ItemsViewLayout.PrepareCellForLayout(constrainedCell);
                        }
                }
 
                public virtual NSIndexPath GetIndexForItem(object item)
                {
-                       for (int n = 0; n < _itemsSource.Count; n++)
-                       {
-                               if (_itemsSource[n] == item)
-                               {
-                                       return NSIndexPath.Create(0, n);
-                               }
-                       }
-
-                       return NSIndexPath.Create(-1, -1);
+                       return ItemsSource.GetIndexForItem(item);
                }
 
                protected object GetItemAtIndex(NSIndexPath index)
                {
-                       return _itemsSource[index.Row];
+                       return ItemsSource[index];
                }
 
                void ApplyTemplateAndDataContext(TemplatedCell cell, NSIndexPath indexPath)
                {
-                       var template = _itemsView.ItemTemplate;
-                       var item = _itemsSource[indexPath.Row];
+                       var template = ItemsView.ItemTemplate;
+                       var item = ItemsSource[indexPath];
 
                        // Run this through the extension method in case it's really a DataTemplateSelector
-                       template = template.SelectDataTemplate(item, _itemsView);
+                       template = template.SelectDataTemplate(item, ItemsView);
 
                        // Create the content and renderer for the view and 
                        var view = template.CreateContent() as View;
@@ -240,10 +193,10 @@ namespace Xamarin.Forms.Platform.iOS
                        cell.SetRenderer(renderer);
 
                        // Bind the view to the data item
-                       view.BindingContext = _itemsSource[indexPath.Row];
+                       view.BindingContext = ItemsSource[indexPath];
 
                        // And make sure it's a "child" of the ItemsView
-                       _itemsView.AddLogicalChild(view);
+                       ItemsView.AddLogicalChild(view);
 
                        cell.ContentSizeChanged += CellContentSizeChanged;
                }
@@ -263,14 +216,14 @@ namespace Xamarin.Forms.Platform.iOS
                                if (oldView != null)
                                {
                                        oldView.BindingContext = null;
-                                       _itemsView.RemoveLogicalChild(oldView);
+                                       ItemsView.RemoveLogicalChild(oldView);
                                }
 
                                templatedCell.PrepareForRemoval();
                        }
                }
 
-               IVisualElementRenderer CreateRenderer(View view)
+               protected IVisualElementRenderer CreateRenderer(View view)
                {
                        if (view == null)
                        {
@@ -283,33 +236,49 @@ namespace Xamarin.Forms.Platform.iOS
                        return renderer;
                }
 
-               string DetermineCellReusedId()
+               string DetermineCellReuseId()
                {
-                       if (_itemsView.ItemTemplate != null)
+                       if (ItemsView.ItemTemplate != null)
                        {
-                               return _layout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+                               return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
                                        ? HorizontalTemplatedCell.ReuseId
                                        : VerticalTemplatedCell.ReuseId;
                        }
 
-                       return _layout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
+                       return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
                                ? HorizontalDefaultCell.ReuseId
                                : VerticalDefaultCell.ReuseId;
                }
 
                UICollectionViewCell GetPrototype()
                {
-                       if (_itemsSource.Count == 0)
+                       if (ItemsSource.ItemCount == 0)
                        {
                                return null;
                        }
 
-                       // TODO hartez assuming this works, we'll need to evaluate using this nsindexpath (what about groups?)
-                       var indexPath = NSIndexPath.Create(0, 0);
+                       var group = 0;
+
+                       if (ItemsSource.GroupCount > 1)
+                       {
+                               // If we're in a grouping situation, then we need to make sure we find an actual data item
+                               // to use for our prototype cell. It's possible that we have empty groups.
+                               for (int n = 0; n < ItemsSource.GroupCount; n++)
+                               {
+                                       if (ItemsSource.ItemCountInGroup(n) > 0)
+                                       {
+                                               group = n;
+                                               break;
+                                       }
+                               }
+                       }
+
+                       var indexPath = NSIndexPath.Create(group, 0);
+
                        return GetCell(CollectionView, indexPath);
                }
 
-               void RegisterCells()
+               protected virtual void RegisterViewTypes()
                {
                        CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell), HorizontalDefaultCell.ReuseId);
                        CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell), VerticalDefaultCell.ReuseId);
@@ -321,7 +290,7 @@ namespace Xamarin.Forms.Platform.iOS
                internal void UpdateEmptyView()
                {
                        // Is EmptyView set on the ItemsView?
-                       var emptyView = _itemsView?.EmptyView;
+                       var emptyView = ItemsView?.EmptyView;
 
                        if (emptyView == null)
                        {
@@ -333,13 +302,13 @@ namespace Xamarin.Forms.Platform.iOS
                        {
                                // Create the native renderer for the EmptyView, and keep the actual Forms element (if any)
                                // around for updating the layout later
-                               var (NativeView, FormsElement) = RealizeEmptyView(emptyView, _itemsView.EmptyViewTemplate);
+                               var (NativeView, FormsElement) = RealizeEmptyView(emptyView, ItemsView.EmptyViewTemplate);
                                _emptyUIView = NativeView;
                                _emptyViewFormsElement = FormsElement;
                        }
 
                        // If the empty view is being displayed, we might need to update it
-                       UpdateEmptyViewVisibility(_itemsSource?.Count == 0);
+                       UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0);
                }
 
                void UpdateEmptyViewVisibility(bool isEmpty)
@@ -381,7 +350,7 @@ namespace Xamarin.Forms.Platform.iOS
                        if (emptyViewTemplate != null)
                        {
                                // Run this through the extension method in case it's really a DataTemplateSelector
-                               emptyViewTemplate = emptyViewTemplate.SelectDataTemplate(emptyView, _itemsView);
+                               emptyViewTemplate = emptyViewTemplate.SelectDataTemplate(emptyView, ItemsView);
 
                                // We have a template; turn it into a Forms view 
                                var templateElement = emptyViewTemplate.CreateContent() as View;
index 21f398a..57e8768 100644 (file)
@@ -13,7 +13,6 @@ namespace Xamarin.Forms.Platform.iOS
                readonly ItemsLayout _itemsLayout;
                bool _determiningCellSize;
                bool _disposed;
-               bool _needCellSizeUpdate;
 
                protected ItemsViewLayout(ItemsLayout itemsLayout)
                {
@@ -80,16 +79,6 @@ namespace Xamarin.Forms.Platform.iOS
 
                public abstract void ConstrainTo(CGSize size);
 
-               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();
-                       }
-               }
-
                public virtual UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
                        nint section)
                {
@@ -172,6 +161,19 @@ namespace Xamarin.Forms.Platform.iOS
                                }
                        }
 
+                       if (Forms.IsiOS11OrNewer)
+                       {
+                               return base.ShouldInvalidateLayout(preferredAttributes, originalAttributes);
+                       }
+
+                       // For iOS 10 and lower, we have to invalidate on header/footer changes here; otherwise, all of the 
+                       // headers and footers will draw on top of one another
+                       if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header
+                               || preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
+                       {
+                               return true;
+                       }
+
                        return base.ShouldInvalidateLayout(preferredAttributes, originalAttributes);
                }
 
@@ -259,11 +261,6 @@ namespace Xamarin.Forms.Platform.iOS
                        UpdateCellConstraints();
                }
 
-               public void SetNeedCellSizeUpdate()
-               {
-                       _needCellSizeUpdate = true;
-               }
-
                public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
                {
                        var snapPointsType = _itemsLayout.SnapPointsType;
@@ -370,5 +367,58 @@ namespace Xamarin.Forms.Platform.iOS
 
                        InvalidateLayout();
                }
+
+               public override UICollectionViewLayoutInvalidationContext GetInvalidationContext(UICollectionViewLayoutAttributes preferredAttributes, UICollectionViewLayoutAttributes originalAttributes)
+               {
+                       if (Forms.IsiOS11OrNewer)
+                       {
+                               return base.GetInvalidationContext(preferredAttributes, originalAttributes);
+                       }
+
+                       var indexPath = preferredAttributes.IndexPath;
+
+                       try
+                       {
+                               UICollectionViewLayoutInvalidationContext invalidationContext =
+                                       base.GetInvalidationContext(preferredAttributes, originalAttributes);
+
+                               // Ensure that if this invalidation was triggered by header/footer changes, the header/footer
+                               // are being invalidated
+                               if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Header)
+                               {
+                                       invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Header,
+                                               new[] { indexPath });
+                               }
+                               else if (preferredAttributes.RepresentedElementKind == UICollectionElementKindSectionKey.Footer)
+                               {
+                                       invalidationContext.InvalidateSupplementaryElements(UICollectionElementKindSectionKey.Footer,
+                                               new[] { indexPath });
+                               }
+
+                               return invalidationContext;
+                       }
+                       catch (MonoTouchException)
+                       {
+                               // This happens on iOS 10 if we have any empty groups in our ItemsSource. Catching here and 
+                               // returning a UICollectionViewFlowLayoutInvalidationContext means that the application does not
+                               // crash, though any group headers/footers will initially draw in the wrong location. It's possible to 
+                               // work around this problem by forcing a full layout update after the headers/footers have been 
+                               // drawn in the wrong places
+                       }
+
+                       return new UICollectionViewFlowLayoutInvalidationContext();
+               }
+
+               public override UICollectionViewLayoutAttributes LayoutAttributesForSupplementaryView(NSString kind, NSIndexPath indexPath)
+               {
+                       if (Forms.IsiOS11OrNewer)
+                       {
+                               return base.LayoutAttributesForSupplementaryView(kind, indexPath);
+                       }
+
+                       // iOS 10 and lower doesn't create these and will throw an exception in GetViewForSupplementaryElement 
+                       // without them, so we need to do it manually here
+                       return UICollectionViewLayoutAttributes.CreateForSupplementaryView(kind, indexPath);
+               }
        }
 }
index 2d79d1a..901e486 100644 (file)
@@ -1,5 +1,7 @@
-using System.Collections;
+using System;
+using System.Collections;
 using System.Collections.Generic;
+using Foundation;
 
 namespace Xamarin.Forms.Platform.iOS
 {
@@ -26,5 +28,50 @@ namespace Xamarin.Forms.Platform.iOS
                {
 
                }
+
+               public object this[NSIndexPath indexPath]
+               {
+                       get
+                       {
+                               if (indexPath.Section > 0)
+                               {
+                                       throw new ArgumentOutOfRangeException(nameof(indexPath));
+                               }
+
+                               return this[indexPath.Row];
+                       }
+               }
+
+               public int GroupCount => Count == 0 ? 0 : 1;
+
+               public int ItemCount => Count;
+
+               public NSIndexPath GetIndexForItem(object item)
+               {
+                       for (int n = 0; n < Count; n++)
+                       {
+                               if (this[n] == item)
+                               {
+                                       return NSIndexPath.Create(0, n);
+                               }
+                       }
+
+                       return NSIndexPath.Create(-1, -1);
+               }
+
+               public object Group(NSIndexPath indexPath)
+               {
+                       return null;
+               }
+
+               public int ItemCountInGroup(nint group)
+               {
+                       if (group > 0)
+                       {
+                               throw new ArgumentOutOfRangeException(nameof(group));
+                       }
+
+                       return Count;
+               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/ObservableGroupedSource.cs b/Xamarin.Forms.Platform.iOS/CollectionView/ObservableGroupedSource.cs
new file mode 100644 (file)
index 0000000..a4d7704
--- /dev/null
@@ -0,0 +1,248 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       internal class ObservableGroupedSource : IItemsViewSource
+       {
+               readonly UICollectionView _collectionView;
+               readonly IList _groupSource;
+               bool _disposed;
+               List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
+
+               public ObservableGroupedSource(IEnumerable groupSource, UICollectionView collectionView)
+               {
+                       _collectionView = collectionView;
+                       _groupSource = groupSource as IList ?? new ListSource(groupSource);
+
+                       if (_groupSource is INotifyCollectionChanged incc)
+                       {
+                               incc.CollectionChanged += CollectionChanged;
+                       }
+
+                       ResetGroupTracking();
+               }
+
+               public object this[NSIndexPath indexPath]
+               {
+                       get
+                       {
+                               var group = (IList)_groupSource[indexPath.Section];
+
+                               if (group.Count == 0)
+                               {
+                                       return null;
+                               }
+
+                               return group[indexPath.Row];
+                       }
+               }
+
+               public int GroupCount => _groupSource.Count;
+
+               int IItemsViewSource.ItemCount
+               {
+                       get
+                       {
+                               // TODO hartez We should probably cache this value
+                               var total = 0;
+
+                               for (int n = 0; n < _groupSource.Count; n++)
+                               {
+                                       var group = (IList)_groupSource[n];
+                                       total += group.Count;
+                               }
+
+                               return total;
+                       }
+               }
+
+               public NSIndexPath GetIndexForItem(object item)
+               {
+                       for (int i = 0; i < _groupSource.Count; i++)
+                       {
+                               var group = (IList)_groupSource[i];
+
+                               for (int j = 0; j < group.Count; j++)
+                               {
+                                       if (group[j] == item)
+                                       {
+                                               return NSIndexPath.Create(i, j);
+                                       }
+                               }
+                       }
+
+                       return NSIndexPath.Create(-1, -1);
+               }
+
+               public object Group(NSIndexPath indexPath)
+               {
+                       return _groupSource[indexPath.Section];
+               }
+
+               public int ItemCountInGroup(nint group)
+               {
+                       return ((IList)_groupSource[(int)group]).Count;
+               }
+
+               public void Dispose()
+               {
+                       Dispose(true);
+               }
+
+               protected virtual void Dispose(bool disposing)
+               {
+                       if (_disposed)
+                       {
+                               return;
+                       }
+
+                       _disposed = true;
+
+                       if (disposing)
+                       {
+                               ClearGroupTracking();
+                               if (_groupSource is INotifyCollectionChanged incc)
+                               {
+                                       incc.CollectionChanged -= CollectionChanged;
+                               }
+                       }
+               }
+
+               void ClearGroupTracking()
+               {
+                       for (int n = _groups.Count - 1; n >= 0; n--)
+                       {
+                               _groups[n].Dispose();
+                               _groups.RemoveAt(n);
+                       }
+               }
+
+               void ResetGroupTracking()
+               {
+                       ClearGroupTracking();
+
+                       for (int n = 0; n < _groupSource.Count; n++)
+                       {
+                               if (_groupSource[n] is INotifyCollectionChanged incc && _groupSource[n] is IList list)
+                               {
+                                       _groups.Add(new ObservableItemsSource(list, _collectionView, n));
+                               }
+                       }
+               }
+
+               void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
+               {
+                       switch (args.Action)
+                       {
+                               case NotifyCollectionChangedAction.Add:
+                                       Add(args);
+                                       break;
+                               case NotifyCollectionChangedAction.Remove:
+                                       Remove(args);
+                                       break;
+                               case NotifyCollectionChangedAction.Replace:
+                                       Replace(args);
+                                       break;
+                               case NotifyCollectionChangedAction.Move:
+                                       Move(args);
+                                       break;
+                               case NotifyCollectionChangedAction.Reset:
+                                       Reload();
+                                       break;
+                               default:
+                                       throw new ArgumentOutOfRangeException();
+                       }
+               }
+
+               void Reload()
+               {
+                       ResetGroupTracking();
+                       _collectionView.ReloadData();
+                       _collectionView.CollectionViewLayout.InvalidateLayout();
+               }
+
+               NSIndexSet CreateIndexSetFrom(int startIndex, int count)
+               {
+                       return NSIndexSet.FromNSRange(new NSRange(startIndex, count));
+               }
+
+               void Add(NotifyCollectionChangedEventArgs args)
+               {
+                       var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
+                       var count = args.NewItems.Count;
+
+                       // Adding a group will change the section index for all subsequent groups, so the easiest thing to do
+                       // is to reset all the group tracking to get it up-to-date
+                       ResetGroupTracking();
+
+                       _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count));
+               }
+
+               void Remove(NotifyCollectionChangedEventArgs args)
+               {
+                       var startIndex = args.OldStartingIndex;
+
+                       if (startIndex < 0)
+                       {
+                               // INCC implementation isn't giving us enough information to know where the removed items were in the
+                               // collection. So the best we can do is a ReloadData()
+                               Reload();
+                               return;
+                       }
+
+                       // If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
+                       var count = args.OldItems.Count;
+
+                       // Removing a group will change the section index for all subsequent groups, so the easiest thing to do
+                       // is to reset all the group tracking to get it up-to-date
+                       ResetGroupTracking();
+
+                       _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count));
+               }
+
+               void Replace(NotifyCollectionChangedEventArgs args)
+               {
+                       var newCount = args.NewItems.Count;
+
+                       if (newCount == args.OldItems.Count)
+                       {
+                               ResetGroupTracking();
+
+                               var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
+
+                               // We are replacing one set of items with a set of equal size; we can do a simple item range update
+                               _collectionView.ReloadSections(CreateIndexSetFrom(startIndex, newCount));
+                               return;
+                       }
+
+                       // The original and replacement sets are of unequal size; this means that everything currently in view will 
+                       // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
+                       Reload();
+               }
+
+               void Move(NotifyCollectionChangedEventArgs args)
+               {
+                       var count = args.NewItems.Count;
+
+                       ResetGroupTracking();
+
+                       if (count == 1)
+                       {
+                               // For a single item, we can use MoveSection and get the animation
+                               _collectionView.MoveSection(args.OldStartingIndex, args.NewStartingIndex);
+                               return;
+                       }
+
+                       var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
+                       var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
+
+                       _collectionView.ReloadSections(CreateIndexSetFrom(start, end));
+               }
+       }
+
+}
index fb14d08..4f4979b 100644 (file)
@@ -9,12 +9,18 @@ namespace Xamarin.Forms.Platform.iOS
        internal class ObservableItemsSource : IItemsViewSource
        {
                readonly UICollectionView _collectionView;
+               readonly bool _grouped;
+               readonly int _section;
                readonly IList _itemsSource;
                bool _disposed;
 
-               public ObservableItemsSource(IList itemSource, UICollectionView collectionView)
+               public ObservableItemsSource(IList itemSource, UICollectionView collectionView, int group = -1)
                {
                        _collectionView = collectionView;
+               
+                       _section = group < 0 ? 0 : group;
+                       _grouped = group >= 0;
+
                        _itemsSource = itemSource;
 
                        ((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
@@ -42,6 +48,46 @@ namespace Xamarin.Forms.Platform.iOS
                        }
                }
 
+               public int ItemCountInGroup(nint group)
+               {
+                       return _itemsSource.Count;
+               }
+
+               public object Group(NSIndexPath indexPath)
+               {
+                       return null;
+               }
+
+               public NSIndexPath GetIndexForItem(object item)
+               {
+                       for (int n = 0; n < _itemsSource.Count; n++)
+                       {
+                               if (this[n] == item)
+                               {
+                                       return NSIndexPath.Create(_section, n);
+                               }
+                       }
+
+                       return NSIndexPath.Create(-1, -1);
+               }
+
+               public int GroupCount => _itemsSource.Count == 0 ? 0 : 1;
+
+               public int ItemCount => _itemsSource.Count;
+
+               public object this[NSIndexPath indexPath]
+               {
+                       get
+                       {
+                               if (indexPath.Section != _section)
+                               {
+                                       throw new ArgumentOutOfRangeException(nameof(indexPath));
+                               }
+
+                               return this[indexPath.Row];
+                       }
+               }
+
                void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
                {
                        switch (args.Action)
@@ -59,58 +105,26 @@ namespace Xamarin.Forms.Platform.iOS
                                        Move(args);
                                        break;
                                case NotifyCollectionChangedAction.Reset:
-                                       _collectionView.ReloadData();
-                                       _collectionView.CollectionViewLayout.InvalidateLayout();
+                                       Reload();
                                        break;
                                default:
                                        throw new ArgumentOutOfRangeException();
                        }
                }
 
-               void Move(NotifyCollectionChangedEventArgs args)
-               {
-                       var count = args.NewItems.Count;
-
-                       if (count == 1)
-                       {
-                               // For a single item, we can use MoveItem and get the animation
-                               var oldPath = NSIndexPath.Create(0, args.OldStartingIndex);
-                               var newPath = NSIndexPath.Create(0, args.NewStartingIndex);
-
-                               _collectionView.MoveItem(oldPath, newPath);
-                               return;
-                       }
-
-                       var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
-                       var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
-                       _collectionView.ReloadItems(CreateIndexesFrom(start, end));
-               }
-               
-               private void Replace(NotifyCollectionChangedEventArgs args)
+               void Reload()
                {
-                       var newCount = args.NewItems.Count;
-
-                       if (newCount == args.OldItems.Count)
-                       {
-                               var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
-
-                               // We are replacing one set of items with a set of equal size; we can do a simple item range update
-                               _collectionView.ReloadItems(CreateIndexesFrom(startIndex, newCount));
-                               return;
-                       }
-                       
-                       // The original and replacement sets are of unequal size; this means that everything currently in view will 
-                       // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
                        _collectionView.ReloadData();
+                       _collectionView.CollectionViewLayout.InvalidateLayout();
                }
 
-               static NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
+               NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
                {
                        var result = new NSIndexPath[count];
 
                        for (int n = 0; n < count; n++)
                        {
-                               result[n] = NSIndexPath.Create(0, startIndex + n);
+                               result[n] = NSIndexPath.Create(_section, startIndex + n);
                        }
 
                        return result;
@@ -121,7 +135,17 @@ namespace Xamarin.Forms.Platform.iOS
                        var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
                        var count = args.NewItems.Count;
 
-                       _collectionView.InsertItems(CreateIndexesFrom(startIndex, count));
+                       _collectionView.PerformBatchUpdates(() =>
+                       {
+                               if (!_grouped && _collectionView.NumberOfSections() != GroupCount)
+                               {
+                                       // We had an empty non-grouped list, and now we're trying to add an item;
+                                       // we need to give it a section as well
+                                       _collectionView.InsertSections(new NSIndexSet(0));
+                               }
+
+                               _collectionView.InsertItems(CreateIndexesFrom(startIndex, count));
+                       }, null);
                }
 
                void Remove(NotifyCollectionChangedEventArgs args)
@@ -132,13 +156,61 @@ namespace Xamarin.Forms.Platform.iOS
                        {
                                // INCC implementation isn't giving us enough information to know where the removed items were in the
                                // collection. So the best we can do is a ReloadData()
-                               _collectionView.ReloadData();
+                               Reload();
                                return;
                        }
-
+       
                        // If we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
                        var count = args.OldItems.Count;
-                       _collectionView.DeleteItems(CreateIndexesFrom(startIndex, count));
+
+                       _collectionView.PerformBatchUpdates(() =>
+                       {
+                               _collectionView.DeleteItems(CreateIndexesFrom(startIndex, count));
+
+                               if (!_grouped && _collectionView.NumberOfSections() != GroupCount)
+                               {
+                                       // We had a non-grouped list with items, and we're removing the last one;
+                                       // we also need to remove the group it was in
+                                       _collectionView.DeleteSections(new NSIndexSet(0));
+                               }
+                       }, null);
+               }
+
+               void Replace(NotifyCollectionChangedEventArgs args)
+               {
+                       var newCount = args.NewItems.Count;
+
+                       if (newCount == args.OldItems.Count)
+                       {
+                               var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
+
+                               // We are replacing one set of items with a set of equal size; we can do a simple item range update
+                               _collectionView.ReloadItems(CreateIndexesFrom(startIndex, newCount));
+                               return;
+                       }
+
+                       // The original and replacement sets are of unequal size; this means that everything currently in view will 
+                       // have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
+                       Reload();
+               }
+
+               void Move(NotifyCollectionChangedEventArgs args)
+               {
+                       var count = args.NewItems.Count;
+
+                       if (count == 1)
+                       {
+                               // For a single item, we can use MoveItem and get the animation
+                               var oldPath = NSIndexPath.Create(_section, args.OldStartingIndex);
+                               var newPath = NSIndexPath.Create(_section, args.NewStartingIndex);
+
+                               _collectionView.MoveItem(oldPath, newPath);
+                               return;
+                       }
+
+                       var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
+                       var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
+                       _collectionView.ReloadItems(CreateIndexesFrom(start, end));
                }
        }
-}
\ No newline at end of file
+}
index ae4e6ee..7d366c0 100644 (file)
@@ -7,12 +7,11 @@ namespace Xamarin.Forms.Platform.iOS
 {
        public class SelectableItemsViewController : ItemsViewController
        {
-               protected readonly SelectableItemsView SelectableItemsView;
+               SelectableItemsView SelectableItemsView => (SelectableItemsView)ItemsView;
 
                public SelectableItemsViewController(SelectableItemsView selectableItemsView, ItemsViewLayout layout) 
                        : base(selectableItemsView, layout)
                {
-                       SelectableItemsView = selectableItemsView;
                }
 
                // _Only_ called if the user initiates the selection change; will not be called for programmatic selection
index d654792..bfa45c8 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using CoreGraphics;
 using Foundation;
 using UIKit;
 
@@ -13,15 +14,15 @@ namespace Xamarin.Forms.Platform.iOS
                        get => ItemsViewController as SelectableItemsViewController;
                }
 
-               public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController)
+               public GroupableItemsViewController GroupableItemsViewController
                {
-                       ItemsViewLayout = itemsViewLayout;
-                       ItemsViewController = itemsViewController;
+                       get => ItemsViewController as GroupableItemsViewController;
                }
 
-               public override void WillDisplayCell(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath path)
+               public UICollectionViewDelegator(ItemsViewLayout itemsViewLayout, ItemsViewController itemsViewController)
                {
-                       ItemsViewLayout?.WillDisplayCell(collectionView, cell, path);
+                       ItemsViewLayout = itemsViewLayout;
+                       ItemsViewController = itemsViewController;
                }
 
                public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout,
@@ -71,5 +72,25 @@ namespace Xamarin.Forms.Platform.iOS
                {
                        ItemsViewController.PrepareCellForRemoval(cell);
                }
+
+               public override CGSize GetReferenceSizeForHeader(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+               {
+                       if (GroupableItemsViewController == null)
+                       {
+                               return CGSize.Empty;
+                       }
+
+                       return GroupableItemsViewController.GetReferenceSizeForHeader(collectionView, layout, section);
+               }
+
+               public override CGSize GetReferenceSizeForFooter(UICollectionView collectionView, UICollectionViewLayout layout, nint section)
+               {
+                       if (GroupableItemsViewController == null)
+                       {
+                               return CGSize.Empty;
+                       }
+
+                       return GroupableItemsViewController.GetReferenceSizeForFooter(collectionView, layout, section);
+               }
        }
 }
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/VerticalDefaultSupplementalView.cs b/Xamarin.Forms.Platform.iOS/CollectionView/VerticalDefaultSupplementalView.cs
new file mode 100644 (file)
index 0000000..5bcd4c0
--- /dev/null
@@ -0,0 +1,30 @@
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       internal sealed class VerticalDefaultSupplementalView : DefaultCell
+       {
+               public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.VerticalDefaultSupplementalView");
+
+               [Export("initWithFrame:")]
+               public VerticalDefaultSupplementalView(CGRect frame) : base(frame)
+               {
+                       Label.Font = UIFont.PreferredHeadline;
+
+                       Constraint = Label.WidthAnchor.ConstraintEqualTo(Frame.Width);
+                       Constraint.Active = true;
+               }
+
+               public override void ConstrainTo(CGSize constraint)
+               {
+                       Constraint.Constant = constraint.Width;
+               }
+
+               public override CGSize Measure()
+               {
+                       return new CGSize(Constraint.Constant, Label.IntrinsicContentSize.Height);
+               }
+       }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.iOS/CollectionView/VerticalTemplatedHeaderView.cs b/Xamarin.Forms.Platform.iOS/CollectionView/VerticalTemplatedHeaderView.cs
new file mode 100644 (file)
index 0000000..99fe13b
--- /dev/null
@@ -0,0 +1,32 @@
+using CoreGraphics;
+using Foundation;
+
+namespace Xamarin.Forms.Platform.iOS
+{
+       public class VerticalTemplatedSupplementalView : TemplatedCell
+       {
+               public static NSString ReuseId = new NSString("Xamarin.Forms.Platform.iOS.VerticalTemplatedSupplementalView");
+
+               [Export("initWithFrame:")]
+               public VerticalTemplatedSupplementalView(CGRect frame) : base(frame)
+               {
+               }
+
+               public override CGSize Measure()
+               {
+                       var measure = VisualElementRenderer.Element.Measure(ConstrainedDimension, 
+                               double.PositiveInfinity, MeasureFlags.IncludeMargins);
+
+                       var height = VisualElementRenderer.Element.Height > 0 
+                               ? VisualElementRenderer.Element.Height : measure.Request.Height;
+
+                       return new CGSize(ConstrainedDimension, height);
+               }
+
+               public override void ConstrainTo(CGSize constraint)
+               {
+                       ConstrainedDimension = constraint.Width;
+                       Layout(constraint);
+               }
+       }
+}
\ No newline at end of file
index 6ca56bf..512cc6b 100644 (file)
     <Compile Include="CollectionView\CarouselViewRenderer.cs" />
     <Compile Include="CollectionView\CollectionViewRenderer.cs" />
     <Compile Include="CollectionView\EmptySource.cs" />
+    <Compile Include="CollectionView\GroupableItemsViewController.cs" />
+    <Compile Include="CollectionView\GroupableItemsViewRenderer.cs" />
+    <Compile Include="CollectionView\HorizontalDefaultSupplementalView.cs" />
+    <Compile Include="CollectionView\HorizontalTemplatedHeaderView.cs" />
     <Compile Include="CollectionView\IItemsViewSource.cs" />
     <Compile Include="CollectionView\ItemsSourceFactory.cs" />
     <Compile Include="CollectionView\ItemsViewCell.cs" />
     <Compile Include="CollectionView\DefaultCell.cs" />
     <Compile Include="CollectionView\HorizontalDefaultCell.cs" />
     <Compile Include="CollectionView\ItemsViewController.cs" />
+    <Compile Include="CollectionView\ObservableGroupedSource.cs" />
     <Compile Include="CollectionView\ScrollToPositionExtensions.cs" />
     <Compile Include="CollectionView\SelectableItemsViewController.cs" />
     <Compile Include="CollectionView\SelectableItemsViewRenderer.cs" />
     <Compile Include="CollectionView\SnapHelpers.cs" />
     <Compile Include="CollectionView\TemplatedCell.cs" />
     <Compile Include="CollectionView\HorizontalTemplatedCell.cs" />
+    <Compile Include="CollectionView\VerticalDefaultSupplementalView.cs" />
     <Compile Include="CollectionView\VerticalTemplatedCell.cs" />
+    <Compile Include="CollectionView\VerticalTemplatedHeaderView.cs" />
     <Compile Include="DisposeHelpers.cs" />
     <Compile Include="EffectUtilities.cs" />
     <Compile Include="ExportCellAttribute.cs" />