Skip to content

[iOS] CollectionView tries to invalidate cells with invalid indexes #35244

Description

@filipnavara

When there are pending animations for data source changes and the data source changes during the animation, UICollectionView.VisibleCells will still report the cells that are being animated. The InvalidateLayoutIfItemsMeasureChanged method inspects VisibleCells during layout and proceeds to call IndexPathForCell for them and then passing them into InvalidateItems. These indexes may refer to the stale data source and be no longer valid, resulting in the following crash:


ObjCRuntime.ObjCException: Objective-C exception thrown.  Name: NSInternalInconsistencyException Reason: attempting to invalidate an item at an invalid indexPath: <NSIndexPath: 0xb7fcaeb1efb3e544> {length = 2, path = 0 - 30} globalIndex: 30 numItems: 0
Native stack trace:
	0   CoreFoundation                      0x00000001860eabf0 __exceptionPreprocess + 176
	1   libobjc.A.dylib                     0x0000000185b7691c objc_exception_throw + 88
	2   Foundation                          0x00000001879781a0 -[NSCalendarDate initWithCoder:] + 0
	3   UIKitCore                           0x00000001bf2b5a8c -[UICollectionViewData invalidateItemsAtIndexPaths:] + 532
	4   UIKitCore                           0x00000001be87d4d0 __49-[UICollectionView _invalidateLayoutWithContext:]_block_invoke_5 + 32
	5   UIKitCore                           0x00000001be87d404 -[UICollectionView _invalidateWithBlock:] + 76
	6   UIKitCore                           0x00000001be87cae4 -[UICollectionView _invalidateLayoutWithContext:] + 992
	7   UIKitCore                           0x00000001be87c678 -[UICollectionViewLayout invalidateLayoutWithContext:] + 144
	8   UIKitCore                           0x00000001be87c3a0 -[UICollectionViewFlowLayout invalidateLayoutWithContext:] + 520
	9   CollectionViewFuzzTest              0x000000010086f694 xamarin_dyn_objc_msgSendSuper + 164
	10  CollectionViewFuzzTest              0x0000000100ac20d4 do_icall + 328
	11  CollectionViewFuzzTest              0x0000000100ac05a4 do_icall_wrapper + 356
	12  CollectionViewFuzzTest              0x0000000100aba19c mono_interp_exec_method + 6648
	13  CollectionViewFuzzTest              0x0000000100ab6f50 interp_runtime_invoke + 244
	14  CollectionViewFuzzTest              0x0000000100a057dc mono_jit_runtime_invoke + 1284
	15  CollectionViewFuzzTest              0x00000001009ad0e8 mono_runtime_invoke_checked + 148
	16  CollectionViewFuzzTest              0x00000001009b05a0 mono_runtime_invoke + 600
	17  CollectionViewFuzzTest              0x0000000100867fc0 xamarin_invoke_trampoline + 5516
	18  CollectionViewFuzzTest              0x000000010086e234 xamarin_arch_trampoline + 160
	19  CollectionViewFuzzTest              0x000000010086f458 xamarin_arm64_common_trampoline + 64
	20  UIKitCore                           0x00000001bee478e4 block_destroy_helper.13 + 19976
	21  UIKitCore                           0x00000001bee47ce0 block_destroy_helper.13 + 20996
	22  UIKitCore                           0x00000001be8584a8 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1196
	23  QuartzCore                          0x0000000190339820 _ZN2CA5Layer15perform_update_EPS0_P7CALayerjNS_17LayerUpdateReasonEPNS_11TransactionE + 460
	24  QuartzCore                          0x0000000190339370 _ZN2CA5Layer17update_if_needed_EPNS_11TransactionENS_17LayerUpdateReasonE + 692
	25  UIKitCore                           0x00000001be857c18 -[UIView(Hierarchy) layoutBelowIfNeeded] + 320
	26  UIKitCore                           0x00000001bf2995a8 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:animationHandler:] + 268
	27  CollectionViewFuzzTest              0x000000010086f694 xamarin_dyn_objc_msgSendSuper + 164
	28  CollectionViewFuzzTest              0x0000000100ac2148 do_icall + 444
	29  CollectionViewFuzzTest              0x0000000100ac05a4 do_icall_wrapper + 356
	30  CollectionViewFuzzTest              0x0000000100aba19c mono_interp_exec_method + 6648
	31  CollectionViewFuzzTest              0x0000000100ab6f50 interp_runtime_invoke + 244
	32  CollectionViewFuzzTest              0x0000000100a057dc mono_jit_runtime_invoke + 1284
	33  CollectionViewFuzzTest              0x00000001009ae74c mono_runtime_try_invoke + 156
	34  CollectionViewFuzzTest              0x00000001009b053c mono_runtime_invoke + 500
	35  CollectionViewFuzzTest              0x0000000100af8654 _ZL30native_to_managed_trampoline_9P11objc_objectP13objc_selectorPP11_MonoMethodj + 276
	36  CollectionViewFuzzTest              0x0000000100b54b0c -[__MonoMac_NSAsyncSynchronizationContextDispatcher xamarinApplySelector] + 44
	37  Foundation                          0x00000001878e3acc __NSThreadPerformPerform + 264
	38  CoreFoundation                      0x000000018607cbc0 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28
	39  CoreFoundation                      0x000000018607cb54 __CFRunLoopDoSource0 + 172
	40  CoreFoundation                      0x000000018607c8c0 __CFRunLoopDoSources0 + 232
	41  CoreFoundation                      0x000000018607b4e4 __CFRunLoopRun + 820
	42  CoreFoundation                      0x000000018614dbe0 _CFRunLoopRunSpecificWithOptions + 532
	43  HIToolbox                           0x0000000192e50560 RunCurrentEventLoopInMode + 320
	44  HIToolbox                           0x0000000192e538bc ReceiveNextEventCommon + 488
	45  HIToolbox                           0x0000000192fdd13c _BlockUntilNextEventMatchingListInMode + 48
	46  AppKit                              0x000000018ab531a4 _DPSBlockUntilNextEventMatchingListInMode + 228
	47  AppKit                              0x000000018a4a7084 _DPSNextEvent + 576
	48  AppKit                              0x000000018b03c69c -[NSApplication(NSEventRouting) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 688
	49  AppKit                              0x000000018b03c3a8 -[NSApplication(NSEventRouting) nextEventMatchingMask:untilDate:inMode:dequeue:] + 72
	50  AppKit                              0x000000018a49a13c -[NSApplication run] + 368
	51  AppKit                              0x000000018a4727b0 NSApplicationMain + 880
	52  AppKit                              0x000000018a6b9930 +[NSWindow _savedFrameFromString:] + 0
	53  UIKitMacHelper                      0x00000001a43681bc UINSApplicationMain + 972
	54  UIKitCore                           0x00000001be7d0ed4 UIApplicationMain + 144
	55  CollectionViewFuzzTest              0x0000000100840618 xamarin_UIApplicationMain + 60
	56  CollectionViewFuzzTest              0x0000000100ac2000 do_icall + 116
	57  CollectionViewFuzzTest              0x0000000100ac05a4 do_icall_wrapper + 356
	58  CollectionViewFuzzTest              0x0000000100aba19c mono_interp_exec_method + 6648
	59  CollectionViewFuzzTest              0x0000000100ab6f50 interp_runtime_invoke + 244
	60  CollectionViewFuzzTest              0x0000000100a057dc mono_jit_runtime_invoke + 1284
	61  CollectionViewFuzzTest              0x00000001009ad0e8 mono_runtime_invoke_checked + 148
	62  CollectionViewFuzzTest              0x00000001009b26f0 mono_runtime_exec_main_checked + 116
	63  CollectionViewFuzzTest              0x0000000100a54278 mono_jit_exec + 360
	64  CollectionViewFuzzTest              0x000000010086e0f0 xamarin_main + 856
	65  CollectionViewFuzzTest              0x0000000100ba820c main + 64
	66  dyld                                0x0000000185c03da4 start + 6992

   at UIKit.UICollectionViewLayout.InvalidateLayout(UICollectionViewLayoutInvalidationContext context) in /Users/builder/azdo/_work/1/s/macios/src/build/dotnet/maccatalyst/generated-sources/UIKit/UICollectionViewLayout.g.cs:line 511
   at Microsoft.Maui.Controls.Handlers.Items.ItemsViewController`1[[Microsoft.Maui.Controls.ReorderableItemsView, Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]].InvalidateLayoutIfItemsMeasureChanged()
   at Microsoft.Maui.Controls.Handlers.Items.ItemsViewController`1[[Microsoft.Maui.Controls.ReorderableItemsView, Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]].ViewWillLayoutSubviews()
   at MailClient.Mobile.iOS.Handlers.CollectionViewController.ViewWillLayoutSubviews() in /Users/navara/Projects/emclient/MailClient.Mobile/MailClient.Mobile.iOS/Handlers/CollectionView/CollectionViewController.cs:line 133
   at UIKit.UICollectionView.PerformBatchUpdates(Action updates, UICompletionHandler completed) in /Users/builder/azdo/_work/1/s/macios/src/build/dotnet/maccatalyst/generated-sources/UIKit/UICollectionView.g.cs:line 707
   at MailClient.Mobile.iOS.Handlers.CollectionViewController.UpdateItemsSource() in /Users/navara/Projects/emclient/MailClient.Mobile/MailClient.Mobile.iOS/Handlers/CollectionView/CollectionViewController.cs:line 321
   at Microsoft.Maui.Controls.Handlers.Items.ItemsViewHandler`1[[Microsoft.Maui.Controls.ReorderableItemsView, Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]].MapItemsSource(ItemsViewHandler`1 handler, ItemsView itemsView)
   at Microsoft.Maui.PropertyMapper`2.<>c__DisplayClass5_0[[Microsoft.Maui.Controls.ReorderableItemsView, Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null],[Microsoft.Maui.Controls.Handlers.Items.ItemsViewHandler`1[[Microsoft.Maui.Controls.ReorderableItemsView, Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]], Microsoft.Maui.Controls, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]].<Add>b__0(IElementHandler h, IElement v)
   at Microsoft.Maui.PropertyMapper.TryUpdatePropertyCore(String key, IElementHandler viewHandler, IElement virtualView)
   at Microsoft.Maui.PropertyMapper.UpdateProperty(IElementHandler viewHandler, IElement virtualView, String property)
   at Microsoft.Maui.Handlers.ElementHandler.UpdateValue(String property)
   at Microsoft.Maui.Controls.Element.UpdateHandlerValue(String property, Boolean valueChanged)
   at Microsoft.Maui.Controls.VisualElement.UpdateHandlerValue(String property, Boolean valueChanged)
   at Microsoft.Maui.Controls.Element.OnBindablePropertySet(BindableProperty property, Object original, Object value, Boolean changed, Boolean willFirePropertyChanged)
   at Microsoft.Maui.Controls.BindableObject.SetValueActual(BindableProperty property, BindablePropertyContext context, Object value, Boolean currentlyApplying, SetValueFlags attributes, SetterSpecificity specificity, Boolean silent)
   at Microsoft.Maui.Controls.BindableObject.SetValueCore(BindableProperty property, Object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes, SetterSpecificity specificity)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value)
   at Microsoft.Maui.Controls.ItemsView.set_ItemsSource(IEnumerable value)
   at CollectionViewFuzzTest.MainPage.<>c__DisplayClass20_0.<RunReproIterationAsync>b__0() in /Users/navara/Projects/emclient/MailClient.Mobile/CollectionViewFuzzTest/MainPage.cs:line 378

Reproducing this is extremely difficult because it needs to hit several conditions at once:

  • An animation started by change to CollectionView's bound data source
  • Switching/clearing the data source on a given CollectionView while the animation is still running
  • Causing the layout of the cells that are being animated but no longer part of the data source to be invalidated

Now, we did eventually manage to reproduce the scenario in isolation as part of our internal collection view fuzzing tool with the following code:

	/// <summary>
	/// Exercises the SideBar disconnect crash path (NSInternalInconsistencyException:
	/// "attempting to invalidate an item at an invalid indexPath … numItems: 0").
	///
	/// Production sequence:
	/// 1. Exchange sync inserts new delegated folders (ObservableCollection.Insert)
	///    → ObservableItemsSource fires InsertItems inside PerformBatchUpdates
	///    → UICollectionView internal state is "dirtied", cells become sticky through ReloadData
	/// 2. Some cells have MeasureInvalidated=true from binding updates (text changes)
	/// 3. User switches to Calendar → SideBarView.InitWithFolderType → Disconnect()
	///    → ItemsSource = null → UpdateItemsSource → base calls ReloadData
	///    → BUT cells survive (sticky from step 1) with MeasureInvalidated still set
	///    → PerformBatchUpdates flush triggers ViewWillLayoutSubviews
	///    → MAUI iterates stale cells, calls InvalidateLayout with stale indexPaths
	///    → UIKit throws because numItems=0
	///
	/// Key insight: inserts FIRST (dirty UICollectionView state), THEN text mutations
	/// (set MeasureInvalidated on the now-sticky cells), THEN disconnect.
	/// </summary>
	private async Task<string> RunReproIterationAsync(int iteration)
	{
		var items = new System.Collections.ObjectModel.ObservableCollection<ReproItem>();
		for (int i = 0; i < 30; i++)
			items.Add(new ReproItem { Text = $"i{i}" });

		var cv = new CollectionViewEx
		{
			IsGrouped = false,
			SelectionMode = SelectionMode.None,
			ItemSizingStrategy = ItemSizingStrategy.MeasureFirstItem,
			ItemTemplate = new DataTemplate(() =>
			{
				var label = new Label
				{
					Padding = new Thickness(12, 6),
					FontSize = 14,
					HorizontalOptions = LayoutOptions.Start,
					VerticalOptions = LayoutOptions.Start,
				};
				label.SetBinding(Label.TextProperty, nameof(ReproItem.Text));
				return label;
			}),
			ItemsSource = items,
		};

		var page = new ContentPage { Content = cv, Title = $"Repro {iteration}" };
		await Navigation.PushModalAsync(page, animated: false);

		// Let UIKit lay out at least one frame so visible cells exist & are bound.
		await Task.Delay(250);

		Exception? caught = null;
		var tcs = new TaskCompletionSource();

		MainThread.BeginInvokeOnMainThread(() =>
		{
			try
			{
				// Step 1: Insert items — this puts UICollectionView into a "dirty" internal
				// state (pending batch updates via ObservableItemsSource) where subsequent
				// ReloadData won't immediately clear VisibleCells.
				items.Insert(0, new ReproItem { Text = "new-delegated-0" });
				items.Insert(1, new ReproItem { Text = "new-delegated-1" });
				items.Insert(2, new ReproItem { Text = "new-delegated-2" });

				// Step 2: Mutate text on existing cells to set MeasureInvalidated=true.
				// Because cells are now "sticky" (from step 1), they'll survive through
				// base.UpdateItemsSource()'s ReloadData AND carry their invalidation flags.
				for (int i = 3; i < items.Count; i++)
					items[i].Text = $"i{i} — much longer text now to force a remeasure of the templated cell";

				// Step 3: Disconnect — mirrors SideBarView.InitWithFolderType teardown.
				// Sets ItemsSource = null → UpdateItemsSource → PerformBatchUpdates flush
				// → ViewWillLayoutSubviews → InvalidateLayout on stale indexPaths → CRASH
				cv.ItemsSource = null;
				cv.Disconnect();
			}
			catch (Exception ex)
			{
				caught = ex;
				Debug.Write(ex);
			}
			finally
			{
				tcs.TrySetResult();
			}
		});

		await tcs.Task;
		await Task.Delay(50); // let any deferred layout pass run

		await Navigation.PopModalAsync(animated: false);

		if (caught is not null)
			return $"CRASH {caught.GetType().Name}: {caught.Message} {caught.StackTrace}";
		return $"ok (count={items.Count})";
	}

The code may not necessarily be adapted cleanly for upstream testing since it uses our subclassed controls and handlers, so I am including it mostly as a pseudo-code to explain how the problem was triggered in a reproducible session.

Fixing the issue with invalid indexes, however, uncovered a second issue. The same repro code can also trigger layout on "unused" measurement cells which is still propagated to ancestors all the way to the VerticalCell measurement cell from a stale data source. It can cause various race conditions because the cells become unrooted by clearing _measurmentCells dictionary in the CollectionView handler code and GC can collect those objects.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-controls-collectionviewCollectionView, CarouselView, IndicatorViewplatform/ioss/triagedIssue has been revieweds/verifiedVerified / Reproducible Issue ready for Engineering Triage

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions