Xamarin.forms: [Bug] CollectionView infinite scroll does not work with a grouped list on iOS

Created on 5 Nov 2019  路  8Comments  路  Source: xamarin/Xamarin.Forms

Description

When enabling grouping and setting RemainingItemsThreshold on a CollectionView on iOS RemainingItemsThresholdReachedCommand is only executed once. After that RemainingItemsThresholdReached and RemainingItemsThresholdReachedCommand are not called when the threshold is reached. On Android the CollectionView behaves as expected.

Steps to Reproduce

  1. Use the reproduction project, or set RemainingItemsThreshold to 0 and IsGrouped True, load Collectionview with test items and bind RemainingItemsThresholdReachedCommand to a command that loads more test items.
  2. Run the iOS project on an iOS simulator.
  3. Scroll down and observe that only one group with items is loaded

Expected Behavior

When scrolling down, new items should automatically load when reaching the end of the list, like on Android.

Actual Behavior

When scrolling down, only one group is loaded, when scrolling again to the end of the list no subsequent groups are loaded. This occurs on the iOS simulator and on an iPhone 8 with iOS 13.2.

Basic Information

  • Version with issue: 4.5.0.1336-nightly, 4.3.0.947036 (latest stable when submitting issue)
  • Last known good version: N/A
  • IDE: VS for Mac
  • Platform Target Frameworks:

    • iOS: 13.2

    • Android: 9.0 (API level 28)

  • Android Support Library Version: 28.0.0.3
  • Nuget Packages: Xamarin.Essentials, Xamarin.Forms
  • Affected Devices: iPhone 8

Reproduction Link

https://github.com/kramer-e/CollectionViewInfiniteScrollingBug

5 high impact iOS 馃崕 bug

Most helpful comment

Thank you for investigating and sharing the workaround, I just tested it and works. @rmarinho I hope someone from the Forms team can verify this fix.

All 8 comments

Same for me.
Any update?

Recently I ran into the same problem as described here. Upon debugging / looking at the source I figured out the event is triggered by the "scrolled" method inside "ItemsViewDelegator" class. This method does not take the grouping in account when calculating the "*itemIndex". Therefor the switch statement responsible for triggering the "itemsThreshold" event will never produce anything usable. My work-around is as follows:

Custom renderer

  public class CustomCollectionViewRenderer : CollectionViewRenderer
    {
        protected override GroupableItemsViewController<GroupableItemsView> CreateController(GroupableItemsView itemsView, ItemsViewLayout layout)
        {
            return new CustomGroupableItemsViewController<GroupableItemsView>(itemsView, layout);
        }
    }

Custom controller

    public class CustomGroupableItemsViewController<TItemsView> : GroupableItemsViewController<TItemsView>
        where TItemsView : GroupableItemsView
    {
        public CustomGroupableItemsViewController(TItemsView groupableItemsView, ItemsViewLayout layout)
            : base(groupableItemsView, layout)
        {
        }

        protected override UICollectionViewDelegateFlowLayout CreateDelegator()
        {
            return new CustomGroupableItemsViewDelegator<TItemsView, CustomGroupableItemsViewController<TItemsView>>(ItemsViewLayout, this);
        }
    }

Custom delegator

    public class CustomGroupableItemsViewDelegator<TItemsView, TViewController> : GroupableItemsViewDelegator<TItemsView, TViewController>
        where TItemsView : GroupableItemsView
        where TViewController : GroupableItemsViewController<TItemsView>
    {
        public CustomGroupableItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
            : base(itemsViewLayout, itemsViewController)
        {
        }

        public override void Scrolled(UIScrollView scrollView)
        {
            List<NSIndexPath> indexPathsForVisibleItems =
                ViewController.CollectionView.IndexPathsForVisibleItems.OrderBy(x => x.Row).ToList();

            if (indexPathsForVisibleItems.Count == 0)
                return;

            TItemsView itemsView = ViewController.ItemsView;
            IItemsViewSource source = ViewController.ItemsSource;

            UIEdgeInsets contentInset = scrollView.ContentInset;
            nfloat contentOffsetX = scrollView.ContentOffset.X + contentInset.Left;
            nfloat contentOffsetY = scrollView.ContentOffset.Y + contentInset.Top;

            UICollectionView collectionView = ViewController.CollectionView;
            CGPoint centerPoint = new CGPoint(collectionView.Center.X + collectionView.ContentOffset.X,
                collectionView.Center.Y + collectionView.ContentOffset.Y);

            NSIndexPath centerIndexPath = collectionView.IndexPathForItemAtPoint(centerPoint);

            NSIndexPath firstVisibleItem = indexPathsForVisibleItems.First();
            NSIndexPath centerItem = centerIndexPath ?? firstVisibleItem;
            NSIndexPath lastVisibleItem = indexPathsForVisibleItems.Last();
            // Changed code
            int firstVisibleItemIndex = GetItemIndexWithGrouping(firstVisibleItem, source);
            int centerItemIndex = GetItemIndexWithGrouping(centerItem, source);
            int lastVisibleItemIndex = GetItemIndexWithGrouping(lastVisibleItem, source);

            ItemsViewScrolledEventArgs itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs
            {
                HorizontalDelta = contentOffsetX - PreviousHorizontalOffset,
                VerticalDelta = contentOffsetY - PreviousVerticalOffset,
                HorizontalOffset = contentOffsetX,
                VerticalOffset = contentOffsetY,
                FirstVisibleItemIndex = firstVisibleItemIndex,
                CenterItemIndex = centerItemIndex,
                LastVisibleItemIndex = lastVisibleItemIndex
            };

            itemsView.SendScrolled(itemsViewScrolledEventArgs);

            PreviousHorizontalOffset = (float)contentOffsetX;
            PreviousVerticalOffset = (float)contentOffsetY;

            switch (itemsView.RemainingItemsThreshold)
            {
                case -1:
                    return;
                case 0:
                    if (lastVisibleItemIndex == source.ItemCount - 1)
                        itemsView.SendRemainingItemsThresholdReached();
                    break;
                default:
                    if (source.ItemCount - 1 - lastVisibleItemIndex <= itemsView.RemainingItemsThreshold)
                        itemsView.SendRemainingItemsThresholdReached();
                    break;
            }
        }
        // Added code
        private static int GetItemIndexWithGrouping(NSIndexPath indexPath, IItemsViewSource itemSource)
        {
            int index = (int)indexPath.Item;

            if (indexPath.Section > 0)
            {
                for (int i = 0; i < indexPath.Section; i++)
                {
                    index += itemSource.ItemCountInGroup(i);
                }
            }

            return index;
        }
    }

If it's useful/wanted I can make a PR for this repository, but I'm not sure it's the best fix possible. :)

Thank you for investigating and sharing the workaround, I just tested it and works. @rmarinho I hope someone from the Forms team can verify this fix.

This kind of code should be in a common class shared by the different platforms.

@RSchipper if you haven't done so already, feel free to open a PR. That will make it easier for us to test this and verify the actual fix since we can just look at the code as it is integrated in our solution instead of having to copy/paste and figure it out ourselves. Thanks :)

@jfversluis Do you have any idea when this is going to be fixed ? Thanks

Hi any news on this one? many thanks

@samhouts Any progress ?

Was this page helpful?
5 / 5 - 1 ratings