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.
When there are pending animations for data source changes and the data source changes during the animation,
UICollectionView.VisibleCellswill still report the cells that are being animated. TheInvalidateLayoutIfItemsMeasureChangedmethod inspectsVisibleCellsduring layout and proceeds to callIndexPathForCellfor them and then passing them intoInvalidateItems. These indexes may refer to the stale data source and be no longer valid, resulting in the following crash:Reproducing this is extremely difficult because it needs to hit several conditions at once:
CollectionView's bound data sourceCollectionViewwhile the animation is still runningNow, we did eventually manage to reproduce the scenario in isolation as part of our internal collection view fuzzing tool with the following code:
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
VerticalCellmeasurement cell from a stale data source. It can cause various race conditions because the cells become unrooted by clearing_measurmentCellsdictionary in the CollectionView handler code and GC can collect those objects.