diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs index 622e72211714..b37d6fd8e53f 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs @@ -117,7 +117,8 @@ protected virtual (bool VisibleItems, NSIndexPath First, NSIndexPath Center, NSI if (collectionView is null) return default; - var indexPathsForVisibleItems = collectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList(); + // Sort visible item index paths by section and then by row for consistent order in both grouped and ungrouped sources + var indexPathsForVisibleItems = collectionView.IndexPathsForVisibleItems.OrderBy(x => x.Section).ThenBy(x => x.Row).ToList(); var visibleItems = indexPathsForVisibleItems.Count > 0; NSIndexPath firstVisibleItemIndex = null, centerItemIndex = null, lastVisibleItemIndex = null; diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs index ec38e061e904..1d36fc20d139 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs @@ -118,7 +118,8 @@ protected virtual (bool VisibleItems, NSIndexPath First, NSIndexPath Center, NSI if (collectionView is null) return default; - var indexPathsForVisibleItems = collectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList(); + // Sort visible item index paths by section and then by row for consistent order in both grouped and ungrouped sources + var indexPathsForVisibleItems = collectionView.IndexPathsForVisibleItems.OrderBy(x => x.Section).ThenBy(x => x.Row).ToList(); var visibleItems = indexPathsForVisibleItems.Count > 0; NSIndexPath firstVisibleItemIndex = null, centerItemIndex = null, lastVisibleItemIndex = null; diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue17664.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue17664.cs new file mode 100644 index 000000000000..707e06ad5aa2 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue17664.cs @@ -0,0 +1,120 @@ +using System.Collections.ObjectModel; + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 17664, "Incorrect ItemsViewScrolledEventArgs in CollectionView when IsGrouped is set to true", PlatformAffected.iOS)] +public class Issue17664 : ContentPage +{ + CollectionView _collectionView; + Label descriptionLabel; + ObservableCollection _groupedItems; + + public Issue17664() + { + Button scrollButton = new Button + { + AutomationId = "Issue17664ScrollBtn", + Text = "Scroll to Category C, Item #2" + }; + scrollButton.Clicked += ScrollButton_Clicked; + + descriptionLabel = new Label + { + AutomationId = "Issue17664DescriptionLabel", + Text = "Use the button above to scroll the CollectionView.", + FontSize = 14, + HorizontalOptions = LayoutOptions.Center + }; + + _collectionView = new CollectionView + { + IsGrouped = true, + GroupHeaderTemplate = new DataTemplate(() => + { + Label label = new Label + { + FontAttributes = FontAttributes.Bold, + BackgroundColor = Colors.LightGray, + Padding = 10 + }; + + label.SetBinding(Label.TextProperty, "Name"); + return label; + }), + ItemTemplate = new DataTemplate(() => + { + Label textLabel = new Label + { + FontAttributes = FontAttributes.Bold, + Padding = 30 + }; + + textLabel.SetBinding(Label.TextProperty, "."); + return textLabel; + }) + }; + + _collectionView.Scrolled += (s, e) => + { + var flatItems = _groupedItems.SelectMany(group => group).ToList(); + if (e.LastVisibleItemIndex >= 0 && e.LastVisibleItemIndex < flatItems.Count) + { + descriptionLabel.Text = flatItems[e.LastVisibleItemIndex]; + } + }; + + List categories = new List { "Category A", "Category B", "Category C" }; + + _groupedItems = new ObservableCollection(); + + foreach (var category in categories) + { + List items = new List(); + + for (int i = 0; i < 5; i++) + { + items.Add($"{category} item #{i}"); + } + + _groupedItems.Add(new Issue17664_ItemModelGroup(category, items)); + } + + _collectionView.ItemsSource = _groupedItems; + + Grid grid = new Grid + { + RowSpacing = 10, + Padding = 10, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + + grid.Add(scrollButton, 0, 0); + grid.Add(descriptionLabel, 0, 1); + grid.Add(_collectionView, 0, 2); + + Content = grid; + } + + private void ScrollButton_Clicked(object sender, EventArgs e) + { + var targetGroup = _groupedItems.FirstOrDefault(group => group.Name == "Category C"); + var targetItem = targetGroup.FirstOrDefault(item => item == "Category C item #2"); + + _collectionView.ScrollTo(targetItem, targetGroup, ScrollToPosition.End); + } +} + +public class Issue17664_ItemModelGroup : ObservableCollection +{ + public string Name { get; set; } + + public Issue17664_ItemModelGroup(string name, IEnumerable items) : base(items) + { + Name = name; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17664.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17664.cs new file mode 100644 index 000000000000..71874bd0c082 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17664.cs @@ -0,0 +1,30 @@ +#if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS // Android fix: https://github.com/dotnet/maui/pull/31437 +// Windows: The Scrolled event is not consistently triggered in the CI environment during automated +// scrolling, so the label text is never updated. This is a test infrastructure limitation on Windows; +// the fix itself is iOS/MacCatalyst-only and works correctly on iOS and MacCatalyst. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue17664 : _IssuesUITest +{ + public Issue17664(TestDevice device) : base(device) + { + } + + public override string Issue => "Incorrect ItemsViewScrolledEventArgs in CollectionView when IsGrouped is set to true"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void VerifyGroupedCollectionViewVisibleItemIndices() + { + App.WaitForElement("Issue17664ScrollBtn"); + App.Tap("Issue17664ScrollBtn"); + + var resultItem = App.WaitForElement("Issue17664DescriptionLabel").GetText(); + Assert.That(resultItem, Is.EqualTo("Category C item #2")); + } +} +#endif