Xamarin.forms: [Bug] CollectionView is broken on IOS, when items have different sizes and ItemSizingStrategy="MeasureAllItems"

Created on 11 Jun 2020  路  10Comments  路  Source: xamarin/Xamarin.Forms

Description

CollectionView is broken on IOS when items have different sizes and ItemSizingStrategy="MeasureAllItems". It means that we have no way to use items with different sizes

Steps to Reproduce

  1. We have CollectionView with items of different sizes
    Grid or FlexLayout (where Grid or Flex is child of DataTemplate) with 3 labels located in 3 rows (2 of them can be invisible in some cases which means that CollectionView cell height will be different from time to time). Of course we use ItemSizingStrategy="MeasureAllItems".
  2. On page opening our items are overlapping each other. (It seems start to be broken after 4th item - so cell sizes were calculated correctly before item height start increase from item 4 to item 5, but interesting that it is correctly handled on decreasing size between items 2-3 )
    Screenshot 2020-06-11 at 11 07 51
  3. Just move collection view a little bit (small swipe to top or to down - Scroll.Y value on collection view changed?) and it start to look and work correctly. So it seems that something block CollectionView on first loading from measure items sizes correctly.
    Screenshot 2020-06-11 at 11 15 47

Expected Behavior

All items size set correctly like after scrolling (screenshot 2)

Actual Behavior

Some of items overlap each other

Basic Information

  • Version with issue: 4.6.0.847 (Xamarin.Forms)
  • Last known good version: None
  • IDE: Any version of VS or VS for MAc
  • Platform Target Frameworks:

    • iOS: 13.18.2.1 Xamarin.iOS

    • Android: no

    • UWP: no

  • Android Support Library Version: no
  • Nuget Packages: 4.6.0.847 (Xamarin.Forms)
  • Affected Devices: iOS, iPadOS

Probably related issues

10625 #5455 #9000 #5455 #7788

Screenshots

posted in Steps to Reproduce

Reproduction Link

no

Workaround

no

collectionview 7 high impact iOS 馃崕 bug

Most helpful comment

I can confirm as well. Reason number 70 why CollectionView should still be experimental.

All 10 comments

Grid or FlexLayout (where Grid or Flex is child of DataTemplate) with 3 labels located in 3 rows

Can you post the markup of your DataTemplate?

(2 of them can be invisible in some cases which means that CollectionView cell height will be different from time to time).

How are you handling the visibility of the rows? Are you setting IsVisible via DataBinding? Or some other way?

@hartez, Visibility is handling by text existence - if there is no text in row there will be no label, so no height in "Auto" height row.

<CollectionView Style="{StaticResource ChatsCollectionViewStyle}" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=SelectedItem}" EmptyView="{x:Static p:ResourceManager.PlaceholderEmptyChat}"
                            EmptyViewTemplate="{StaticResource EmptyCollectionViewTemplate}" SelectionMode="Single">
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="{x:Type cm:ChatDetailViewModel}">
                        <Grid AutomationId="{x:Static t:TestResources.ChatsListItemCell}" Style="{StaticResource ChatItemGridStyle}">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="63" />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="Auto" />
                                <RowDefinition Height="1" />
                            </Grid.RowDefinitions>
                            <Label Grid.Row="0" Grid.Column="0" Text="{Binding Chat, Converter={StaticResource ChatTitlePreviewConverter}}" Style="{StaticResource TitleLabelStyle}" StyleClass="BoldWhenUnread" />
                            <Label Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="End" HorizontalOptions="End" Text="{Binding Path=Chat.Updated, Converter={StaticResource DateTimeToDayConverterWithCustomDateFormat}}" Style="{StaticResource DateLabelStyle}" StyleClass="BoldWhenUnread" />
                            <Label Grid.Row="1" Grid.Column="0" Margin="8,2,0,0" Text="{Binding Chat, Converter={StaticResource ChatParticipantsPreviewConverter}}" Style="{StaticResource SmallerItemLabel}" StyleClass="BoldWhenUnread" />
                            <Label Grid.Row="2" Grid.Column="0" Margin="8,2,0,8" Text="{Binding Chat, Converter={StaticResource ChatLastMessagePreviewConverter}}" Style="{StaticResource SmallerItemLabel}" StyleClass="ItalicBoldWhenUnread" />
                            <Frame HorizontalOptions="End" Grid.Row="2" Grid.Column="1" Margin="8,2,8,8" WidthRequest="20" IsVisible="{Binding Chat.Unread}" Style="{StaticResource BadgeFrameStyle}">
                                <Label Text="{Binding Chat.UnreadCount, Converter={StaticResource NumberToStringLimited}}" MaxLines="1" Style="{StaticResource BadgeLabelStyle}" />
                            </Frame>
                            <BoxView Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource SeparatorStyle}" />
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>

Also it is reproducible in a different types of Root elements for DataTemplate.
When I tested it I just created FlexLayout instead of grid and put there 3 labels from sample of code above (3 labels as 3 rows inside FlexLayout) - and result was the same - rows were cutted when cells height is different.

I can confirm this issue. In my case, I have a DataTemplateSelector which selects two very different DataTemplates based on a property in my ItemViewModel. It often happens when the smaller Template is used in the lower third of the screen.

Is there any workaround we could implement for the time being?

I can confirm as well. Reason number 70 why CollectionView should still be experimental.

Had the same problem. Because of the different Labels' heights their parent containers were overlapping. As a workaround I've set Labels' TextType proberty to HTML, and wrapped their values. Something like: $"<p style='color:#8f92a1;font-size:16px;font-family: Arial;display:inline-block;'>{Text}</p>";. Not good, but seem to work..

I've the same problem.
Is there any workaround or resolution date?

I had a similar problem with dynamically resizing items not being correctly resized and solved it by calling this every time after I change an item:

collectionView.ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem;
collectionView.ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems;

This seems to remeasure all items.. Don't forget to call it on a main thread.

After a lot of attempt, I achieved to have a solution that is working on iOS with a CollecitonView Grouped containing different DataTemplate of different Sizes (using DataTemplateSelector) with hundred of items.

If it can help :
In my opinion, there's two main causes with the current implementation

1) Self Sizing. The implementation used by ItemSizingStrategy.MeasureAllItems is based on self sizing. It seems that, there's still some weird behavior using this approach... I think that's a better practice to define a fixed size for each possible DataTemplate (should be ok for a lot of scenario). Inspired by https://github.com/xamarin/Xamarin.Forms/issues/10842#issuecomment-688709149, I've added a mechanism that provides the expected cell size for each item type in order to avoid the use of EstimatedSize things if possible...:

public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
    //use CollectionView ItemsSource to get ViewModel type associated to indexPath
    var itemType = GetCellItemViewModel(indexPath);

    if (itemType == null)
    {
        return ItemsViewLayout.EstimatedItemSize;
    }

    //Get Height from your Shared Assembly
    var cellHeight = ProduitItemViewModelsCellAssociations.GetCellHeight(itemType);
    var cellWidth = (double)collectionView.Frame.Width;

    //Special treatments if using GridViewLayout ?
    if (_itemsViewLayout is GridViewLayoutAdvanced gridViewLayoutAdvanced)
    {
        cellWidth /= gridViewLayoutAdvanced.Span;
        cellWidth -= gridViewLayoutAdvanced.HorizontalItemSpacing;
    }

    return new CGSize(cellWidth, cellHeight);
}

2) ReuseIdentifier mechanism. Foreach VerticalCell, the same ReuseIdentifier is used: see the RegisterViewTypes method https://github.com/xamarin/Xamarin.Forms/blob/f35ae07a0a8471d255f7a1ebdd51499e10e0a4cb/Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs. I've added a mechanism that creates a ReuseIdentifier dedicated for each DataTemplate (=> ItemViewModel type).

//Custom Controller
 private class GroupableItemsViewAdvancedController<TItemsView> : GroupableItemsViewController<TItemsView>
            where TItemsView : GroupableItemsView
{
    public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
    {
        var cellItemViewModel = GetCellItemViewModel(indexPath);

        if (cellItemViewModel == null)
        {
            return base.GetCell(collectionView, indexPath);
        }

        var defaultCellReuseIdentifier = DetermineCellReuseId();
        var cellReuseIdentifier = defaultCellReuseIdentifier + cellItemViewModel.GetType().ToString() ?? string.Empty;

        //Ensure that each type of item (particular DataTemplate) has its proper identifier
        //to avoid recycling problems and follow iOS best practice
        if (!_customCellsIdentidiers.Contains(cellReuseIdentifier))
        {
            //Get VerticalCell internal type if not already get
            _defaultCellInstanceType = _defaultCellInstanceType ?? CollectionView.DequeueReusableCell(defaultCellReuseIdentifier, indexPath).GetType();

            CollectionView.RegisterClassForCell(_defaultCellInstanceType, cellReuseIdentifier);
            _customCellsIdentidiers.Add(cellReuseIdentifier);
        }

        var nativeCell = collectionView.DequeueReusableCell(cellReuseIdentifier, indexPath) as UICollectionViewCell;

        //Code from base.GetCell
        switch (nativeCell)
        {
            case DefaultCell defaultCell:
                UpdateDefaultCell(defaultCell, indexPath);
                break;
            case TemplatedCell templatedCell:
                UpdateTemplatedCell(templatedCell, indexPath);
                break;
        }

        return nativeCell;
    }
}

Feel free to comment/enhance my solution :)

Was this page helpful?
0 / 5 - 0 ratings