Skip to content

[iOS] Support for KeepLastItemInView for CollectionView2 on iOS #31104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ public static void MapIsVisible(ItemsViewHandler2<TItemsView> handler, ItemsView

public static void MapItemsUpdatingScrollMode(ItemsViewHandler2<TItemsView> handler, ItemsView itemsView)
{
// TODO: Fix handler._layout.ItemsUpdatingScrollMode = itemsView.ItemsUpdatingScrollMode;
if (handler.ItemsView is StructuredItemsView structuredItemsView && structuredItemsView.ItemsLayout is ItemsLayout itemsLayout)
{
itemsLayout.ItemsUpdatingScrollMode = itemsView.ItemsUpdatingScrollMode;
}
}

//TODO: this is being called 2 times on startup, one from OnCreatePlatformView and otehr from the mapper for the layout
Expand Down
63 changes: 54 additions & 9 deletions src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ static NSCollectionLayoutBoundarySupplementaryItem[] CreateSupplementaryItems(La
return [];
}

static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo layoutHeaderFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double itemSpacing, Func<Thickness>? peekAreaInsetsFunc)
static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo layoutHeaderFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double itemSpacing, Func<Thickness>? peekAreaInsetsFunc, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
{
var layoutConfiguration = new UICollectionViewCompositionalLayoutConfiguration();
layoutConfiguration.ScrollDirection = scrollDirection;
Expand Down Expand Up @@ -137,14 +137,14 @@ static UICollectionViewLayout CreateListLayout(UICollectionViewScrollDirection s
groupHeight);

return section;
}, layoutConfiguration);
}, layoutConfiguration, itemsUpdatingScrollMode);

return layout;
}



static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double verticalItemSpacing, double horizontalItemSpacing, int columns)
static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection scrollDirection, LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo, LayoutSnapInfo snapInfo, NSCollectionLayoutDimension itemWidth, NSCollectionLayoutDimension itemHeight, NSCollectionLayoutDimension groupWidth, NSCollectionLayoutDimension groupHeight, double verticalItemSpacing, double horizontalItemSpacing, int columns, ItemsUpdatingScrollMode itemsUpdatingScrollMode)
{
var layoutConfiguration = new UICollectionViewCompositionalLayoutConfiguration();
layoutConfiguration.ScrollDirection = scrollDirection;
Expand Down Expand Up @@ -189,7 +189,7 @@ static UICollectionViewLayout CreateGridLayout(UICollectionViewScrollDirection s
groupHeight);

return section;
}, layoutConfiguration);
}, layoutConfiguration, itemsUpdatingScrollMode);

return layout;
}
Expand All @@ -207,7 +207,8 @@ public static UICollectionViewLayout CreateVerticalList(LinearItemsLayout linear
NSCollectionLayoutDimension.CreateFractionalWidth(1f),
NSCollectionLayoutDimension.CreateEstimated(30f),
linearItemsLayout.ItemSpacing,
null);
null,
linearItemsLayout.ItemsUpdatingScrollMode);


public static UICollectionViewLayout CreateHorizontalList(LinearItemsLayout linearItemsLayout,
Expand All @@ -223,7 +224,8 @@ public static UICollectionViewLayout CreateHorizontalList(LinearItemsLayout line
NSCollectionLayoutDimension.CreateEstimated(30f),
NSCollectionLayoutDimension.CreateFractionalHeight(1f),
linearItemsLayout.ItemSpacing,
null);
null,
linearItemsLayout.ItemsUpdatingScrollMode);

public static UICollectionViewLayout CreateVerticalGrid(GridItemsLayout gridItemsLayout,
LayoutGroupingInfo groupingInfo, LayoutHeaderFooterInfo headerFooterInfo)
Expand All @@ -241,7 +243,8 @@ public static UICollectionViewLayout CreateVerticalGrid(GridItemsLayout gridItem
NSCollectionLayoutDimension.CreateEstimated(30f),
gridItemsLayout.VerticalItemSpacing,
gridItemsLayout.HorizontalItemSpacing,
gridItemsLayout.Span);
gridItemsLayout.Span,
gridItemsLayout.ItemsUpdatingScrollMode);


public static UICollectionViewLayout CreateHorizontalGrid(GridItemsLayout gridItemsLayout,
Expand All @@ -260,7 +263,8 @@ public static UICollectionViewLayout CreateHorizontalGrid(GridItemsLayout gridIt
NSCollectionLayoutDimension.CreateFractionalHeight(1f),
gridItemsLayout.VerticalItemSpacing,
gridItemsLayout.HorizontalItemSpacing,
gridItemsLayout.Span);
gridItemsLayout.Span,
gridItemsLayout.ItemsUpdatingScrollMode);


#nullable disable
Expand Down Expand Up @@ -399,9 +403,50 @@ public static UICollectionViewLayout CreateCarouselLayout(
class CustomUICollectionViewCompositionalLayout : UICollectionViewCompositionalLayout
{
LayoutSnapInfo _snapInfo;
public CustomUICollectionViewCompositionalLayout(LayoutSnapInfo snapInfo, UICollectionViewCompositionalLayoutSectionProvider sectionProvider, UICollectionViewCompositionalLayoutConfiguration configuration) : base(sectionProvider, configuration)
ItemsUpdatingScrollMode _itemsUpdatingScrollMode;

public CustomUICollectionViewCompositionalLayout(LayoutSnapInfo snapInfo, UICollectionViewCompositionalLayoutSectionProvider sectionProvider, UICollectionViewCompositionalLayoutConfiguration configuration, ItemsUpdatingScrollMode itemsUpdatingScrollMode) : base(sectionProvider, configuration)
{
_snapInfo = snapInfo;
_itemsUpdatingScrollMode = itemsUpdatingScrollMode;
}

public override void FinalizeCollectionViewUpdates()
{
base.FinalizeCollectionViewUpdates();

if (_itemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
ForceScrollToLastItem(CollectionView);
}
}

void ForceScrollToLastItem(UICollectionView collectionView)
{
var sections = (int)collectionView.NumberOfSections();

if (sections == 0)
{
return;
}

for (int section = sections - 1; section >= 0; section--)
{
var itemCount = collectionView.NumberOfItemsInSection(section);
if (itemCount > 0)
{
var lastIndexPath = NSIndexPath.FromItemSection(itemCount - 1, section);
if (Configuration.ScrollDirection == UICollectionViewScrollDirection.Vertical)
{
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Bottom, true);
}
else
{
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Right, true);
Copy link
Preview

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll position logic assumes left-to-right layout direction. For right-to-left languages or when FlowDirection is RightToLeft, scrolling to UICollectionViewScrollPosition.Right may not provide the expected behavior of showing the 'last' item from the user's perspective.

Suggested change
collectionView.ScrollToItem(lastIndexPath, UICollectionViewScrollPosition.Right, true);
// Adjust scroll position for RTL layouts
var layoutDirection = collectionView.EffectiveUserInterfaceLayoutDirection;
var scrollPosition = layoutDirection == UIUserInterfaceLayoutDirection.RightToLeft
? UICollectionViewScrollPosition.Left
: UICollectionViewScrollPosition.Right;
collectionView.ScrollToItem(lastIndexPath, scrollPosition, true);

Copilot uses AI. Check for mistakes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kubaflo I think this might make sense?

}
return;
}
}
}

public override CGPoint TargetContentOffset(CGPoint proposedContentOffset, CGPoint scrollingVelocity)
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/Items/ItemsLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public abstract class ItemsLayout : BindableObject, IItemsLayout
/// <include file="../../../docs/Microsoft.Maui.Controls/ItemsLayout.xml" path="//Member[@MemberName='Orientation']/Docs/*" />
public ItemsLayoutOrientation Orientation { get; }

internal ItemsUpdatingScrollMode ItemsUpdatingScrollMode { get; set; }

protected ItemsLayout([Parameter("Orientation")] ItemsLayoutOrientation orientation)
{
Orientation = orientation;
Expand Down
49 changes: 49 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue28716.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue28716">
<Grid RowDefinitions="Auto,100,100,100">
<Button Text="Add Item"
Command="{Binding AddItemCommand}"
AutomationId="AddItemButton"
HorizontalOptions="Center"
VerticalOptions="Center"/>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
ItemsSource="{Binding Items}"
Grid.Row="1">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv1'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
FlowDirection="RightToLeft"
ItemsSource="{Binding Items}"
Grid.Row="2">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv2'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<CollectionView
ItemsUpdatingScrollMode="KeepLastItemInView"
ItemsSource="{Binding Items}"
Grid.Row="3">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Text="{Binding . ,StringFormat='{0}cv3'}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
33 changes: 33 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue28716.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.ObjectModel;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 28716, "Support for KeepLastItemInView for CV2", PlatformAffected.iOS)]
public partial class Issue28716 : ContentPage
{
private ObservableCollection<string> _items;
public ObservableCollection<string> Items
{
get => _items;
set
{
if (_items != value)
{
_items = value;
OnPropertyChanged();
}
}
}

public Command AddItemCommand => new(() =>
{
Items.Add($"Item{_items.Count}");
});

public Issue28716()
{
InitializeComponent();
Items = [.. Enumerable.Range(0, 20).Select(x => $"Item{x}")];
BindingContext = this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;
public class Issue28716 : _IssuesUITest
{
public Issue28716(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "Support for KeepLastItemInView for CV2";

[Test]
[Category(UITestCategories.CollectionView)]
public void KeepLastItemInViewShouldWork()
{
App.WaitForElement("AddItemButton");
App.Click("AddItemButton");
App.WaitForElement("Item20cv1");
App.WaitForElement("Item20cv2");
App.WaitForElement("Item20cv3");
}
}
Loading