Skip to content

Commit 609499c

Browse files
authored
Improve iOS CollectionView performance by leveraging the new platform level invalidation mechanism (#28225)
* Use native invalidation mechanism to detect TemplatedCell size change. * Updated tests and improved solution * Add support for resizing header and footer * Fix CV1 invalidation * Fixes * Other fixes and addressing PR review * Update layout passes based on CI run
1 parent 43fd3fa commit 609499c

File tree

24 files changed

+313
-167
lines changed

24 files changed

+313
-167
lines changed

src/Controls/src/Core/Handlers/Items/iOS/ItemsViewCell.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected void InitializeContentConstraints(UIView platformView)
3737
ContentView.TrailingAnchor.ConstraintEqualTo(TrailingAnchor).Active = true;
3838

3939
// And we want the ContentView to be the same size as the root renderer for the Forms element
40+
// TODO: we should probably remove this to support `Margin` applied to the cell's root `VirtualView`
4041
ContentView.TopAnchor.ConstraintEqualTo(platformView.TopAnchor).Active = true;
4142
ContentView.BottomAnchor.ConstraintEqualTo(platformView.BottomAnchor).Active = true;
4243
ContentView.LeadingAnchor.ConstraintEqualTo(platformView.LeadingAnchor).Active = true;

src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public abstract class ItemsViewController<TItemsView> : UICollectionViewControll
2929
protected ItemsViewLayout ItemsViewLayout { get; set; }
3030

3131
bool _initialized;
32+
bool _laidOut;
3233
bool _isEmpty = true;
3334
bool _emptyViewDisplayed;
3435
bool _disposed;
@@ -193,18 +194,48 @@ public override void LoadView()
193194
public override void ViewWillAppear(bool animated)
194195
{
195196
base.ViewWillAppear(animated);
196-
ConstrainItemsToBounds();
197197
}
198198

199199
public override void ViewWillLayoutSubviews()
200200
{
201201
ConstrainItemsToBounds();
202+
203+
if (CollectionView is Items.MauiCollectionView { NeedsCellLayout: true } collectionView)
204+
{
205+
InvalidateLayoutIfItemsMeasureChanged();
206+
collectionView.NeedsCellLayout = false;
207+
}
208+
202209
base.ViewWillLayoutSubviews();
203210
InvalidateMeasureIfContentSizeChanged();
204211
LayoutEmptyView();
212+
213+
_laidOut = true;
205214
}
206215

216+
void InvalidateLayoutIfItemsMeasureChanged()
217+
{
218+
var visibleCells = CollectionView.VisibleCells;
219+
List<NSIndexPath> invalidatedPaths = null;
207220

221+
var visibleCellsLength = visibleCells.Length;
222+
for (int n = 0; n < visibleCellsLength; n++)
223+
{
224+
if (visibleCells[n] is TemplatedCell { MeasureInvalidated: true } cell)
225+
{
226+
invalidatedPaths ??= new List<NSIndexPath>(visibleCellsLength);
227+
var path = CollectionView.IndexPathForCell(cell);
228+
invalidatedPaths.Add(path);
229+
}
230+
}
231+
232+
if (invalidatedPaths != null)
233+
{
234+
var layoutInvalidationContext = new UICollectionViewFlowLayoutInvalidationContext();
235+
layoutInvalidationContext.InvalidateItems(invalidatedPaths.ToArray());
236+
CollectionView.CollectionViewLayout.InvalidateLayout(layoutInvalidationContext);
237+
}
238+
}
208239

209240
void MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view)
210241
{
@@ -284,7 +315,7 @@ void ConstrainItemsToBounds()
284315
{
285316
var contentBounds = CollectionView.AdjustedContentInset.InsetRect(CollectionView.Bounds);
286317
var constrainedSize = contentBounds.Size;
287-
ItemsViewLayout.UpdateConstraints(constrainedSize);
318+
ItemsViewLayout.UpdateConstraints(constrainedSize, !_laidOut);
288319
}
289320

290321
void EnsureLayoutInitialized()
@@ -373,7 +404,6 @@ protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath
373404

374405
protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
375406
{
376-
cell.ContentSizeChanged -= CellContentSizeChanged;
377407
cell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
378408

379409
var bindingContext = ItemsSource[indexPath];
@@ -382,7 +412,6 @@ protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath index
382412
if (_measurementCells != null && _measurementCells.TryGetValue(bindingContext, out TemplatedCell measurementCell))
383413
{
384414
_measurementCells.Remove(bindingContext);
385-
measurementCell.ContentSizeChanged -= CellContentSizeChanged;
386415
measurementCell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
387416
cell.UseContent(measurementCell);
388417
}
@@ -391,7 +420,6 @@ protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath index
391420
cell.Bind(ItemsView.ItemTemplate, ItemsSource[indexPath], ItemsView);
392421
}
393422

394-
cell.ContentSizeChanged += CellContentSizeChanged;
395423
cell.LayoutAttributesChanged += CellLayoutAttributesChanged;
396424

397425
ItemsViewLayout.PrepareCellForLayout(cell);
@@ -407,29 +435,6 @@ protected object GetItemAtIndex(NSIndexPath index)
407435
return ItemsSource[index];
408436
}
409437

410-
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
411-
void CellContentSizeChanged(object sender, EventArgs e)
412-
{
413-
if (_disposed)
414-
return;
415-
416-
if (!(sender is TemplatedCell cell))
417-
{
418-
return;
419-
}
420-
421-
var visibleCells = CollectionView.VisibleCells;
422-
423-
for (int n = 0; n < visibleCells.Length; n++)
424-
{
425-
if (cell == visibleCells[n])
426-
{
427-
ItemsViewLayout?.InvalidateLayout();
428-
return;
429-
}
430-
}
431-
}
432-
433438
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
434439
void CellLayoutAttributesChanged(object sender, LayoutAttributesChangedEventArgs args)
435440
{

src/Controls/src/Core/Handlers/Items/iOS/ItemsViewLayout.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ protected virtual void HandlePropertyChanged(PropertyChangedEventArgs propertyCh
104104
}
105105
}
106106

107-
internal virtual bool UpdateConstraints(CGSize size)
107+
internal virtual bool UpdateConstraints(CGSize size, bool forceUpdate = false)
108108
{
109-
if (size.IsCloseTo(_currentSize))
109+
if (size.IsCloseTo(_currentSize) && !forceUpdate)
110110
{
111111
return false;
112112
}

src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ internal class MauiCollectionView : UICollectionView, IUIViewLifeCycleEvents, IP
1010
bool _invalidateParentWhenMovedToWindow;
1111

1212
WeakReference<ICustomMauiCollectionViewDelegate>? _customDelegate;
13+
14+
internal bool NeedsCellLayout { get; set; }
15+
1316
public MauiCollectionView(CGRect frame, UICollectionViewLayout layout) : base(frame, layout)
1417
{
1518
}
@@ -27,10 +30,12 @@ void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMoved
2730

2831
void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
2932
{
30-
if (!isPropagating)
33+
if (isPropagating)
3134
{
32-
SetNeedsLayout();
35+
NeedsCellLayout = true;
3336
}
37+
38+
SetNeedsLayout();
3439
}
3540

3641
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = IUIViewLifeCycleEvents.UnconditionalSuppressMessage)]

src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,6 @@ internal override void Disconnect()
3131
{
3232
base.Disconnect();
3333

34-
if (_headerViewFormsElement is not null)
35-
{
36-
_headerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
37-
}
38-
39-
if (_footerViewFormsElement is not null)
40-
{
41-
_footerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
42-
}
43-
4434
_headerUIView = null;
4535
_headerViewFormsElement = null;
4636
_footerUIView = null;
@@ -86,27 +76,23 @@ protected override CGRect DetermineEmptyViewFrame()
8676

8777
public override void ViewWillLayoutSubviews()
8878
{
89-
base.ViewWillLayoutSubviews();
90-
91-
// This update is only relevant if you have a footer view because it's used to place the footer view
92-
// based on the ContentSize so we just update the positions if the ContentSize has changed
93-
if (_footerUIView != null)
79+
var hasHeaderOrFooter = _footerViewFormsElement is not null || _headerViewFormsElement is not null;
80+
if (hasHeaderOrFooter && CollectionView is MauiCollectionView { NeedsCellLayout: true } collectionView)
9481
{
95-
var emptyView = CollectionView.ViewWithTag(EmptyTag);
96-
97-
if (IsHorizontal)
82+
if (_headerViewFormsElement is not null)
9883
{
99-
if (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width ||
100-
_footerUIView.Frame.X < emptyView?.Frame.X)
101-
UpdateHeaderFooterPosition();
84+
RemeasureLayout(_headerViewFormsElement);
10285
}
103-
else
86+
87+
if (_footerViewFormsElement is not null)
10488
{
105-
if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height ||
106-
_footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height))
107-
UpdateHeaderFooterPosition();
89+
RemeasureLayout(_footerViewFormsElement);
10890
}
91+
92+
UpdateHeaderFooterPosition();
10993
}
94+
95+
base.ViewWillLayoutSubviews();
11096
}
11197

11298
internal void UpdateFooterView()
@@ -123,15 +109,13 @@ internal void UpdateHeaderView()
123109
UpdateHeaderFooterPosition();
124110
}
125111

126-
127112
internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag, ref UIView uiView, ref VisualElement formsElement)
128113
{
129114
uiView?.RemoveFromSuperview();
130115

131116
if (formsElement != null)
132117
{
133118
ItemsView.RemoveLogicalChild(formsElement);
134-
formsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
135119
}
136120

137121
UpdateView(view, viewTemplate, ref uiView, ref formsElement);
@@ -150,7 +134,6 @@ internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag
150134
if (formsElement != null)
151135
{
152136
RemeasureLayout(formsElement);
153-
formsElement.MeasureInvalidated += OnFormsElementMeasureInvalidated;
154137
}
155138
else
156139
{
@@ -176,7 +159,11 @@ void UpdateHeaderFooterPosition()
176159
}
177160

178161
if (_footerUIView != null && (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width || emptyWidth > 0))
179-
_footerUIView.Frame = new CoreGraphics.CGRect(ItemsViewLayout.CollectionViewContentSize.Width + emptyWidth, 0, footerWidth, CollectionView.Frame.Height);
162+
{
163+
_footerUIView.Frame = new CoreGraphics.CGRect(
164+
ItemsViewLayout.CollectionViewContentSize.Width + emptyWidth, 0, footerWidth,
165+
CollectionView.Frame.Height);
166+
}
180167

181168
if (CollectionView.ContentInset.Left != headerWidth || CollectionView.ContentInset.Right != footerWidth)
182169
{
@@ -249,14 +236,5 @@ protected override void HandleFormsElementMeasureInvalidated(VisualElement forms
249236
var size = base.GetSize();
250237
return new Size(size.Value.Width, size.Value.Height + (_headerUIView?.Frame.Height ?? 0) + (_footerUIView?.Frame.Height ?? 0));
251238
}
252-
253-
internal void UpdateLayoutMeasurements()
254-
{
255-
if (_headerViewFormsElement != null)
256-
HandleFormsElementMeasureInvalidated(_headerViewFormsElement);
257-
258-
if (_footerViewFormsElement != null)
259-
HandleFormsElementMeasureInvalidated(_footerViewFormsElement);
260-
}
261239
}
262240
}

0 commit comments

Comments
 (0)