Skip to content

Commit b5a3d25

Browse files
albyrock87PureWeen
andauthored
Fix iOS ScrollView content being measured with non-infinite constraints during LayoutSubviews pass (#27298)
* Fix iOS ScrollView content being measured with non-infinite constraints during LayoutSubviews pass * - add interface to WrapperView and move to ext method --------- Co-authored-by: Shane Neuville <[email protected]>
1 parent d4d95e8 commit b5a3d25

File tree

7 files changed

+69
-15
lines changed

7 files changed

+69
-15
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace Controls.TestCases.HostApp.Issues;
2+
3+
[Issue(IssueTracker.Github, 27169, "Grid inside ScrollView should measure with infinite constraints", PlatformAffected.iOS)]
4+
public class Issue27169 : ContentPage
5+
{
6+
public Issue27169()
7+
{
8+
var grid = new Grid { VerticalOptions = LayoutOptions.Start };
9+
grid.RowDefinitions.Add(new RowDefinition(GridLength.Star));
10+
grid.RowDefinitions.Add(new RowDefinition(GridLength.Star));
11+
var firstContent = new Label { MinimumHeightRequest = 200, AutomationId = "StubLabel", BackgroundColor = Colors.LightBlue };
12+
firstContent.SetBinding(Label.TextProperty, new Binding(nameof(Label.Height), source: firstContent));
13+
var secondContent = new Label { MinimumHeightRequest = 40, Text = "Second Content", BackgroundColor = Colors.SlateBlue };
14+
15+
grid.Add(firstContent);
16+
grid.Add(secondContent, 0 , 1);
17+
18+
var scrollView = new ScrollView { Content = grid };
19+
Content = scrollView;
20+
}
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using NUnit.Framework;
2+
using NUnit.Framework.Legacy;
3+
using UITest.Appium;
4+
using UITest.Core;
5+
6+
namespace Microsoft.Maui.TestCases.Tests.Issues
7+
{
8+
public class Issue27169(TestDevice device) : _IssuesUITest(device)
9+
{
10+
public override string Issue => "Grid inside ScrollView should measure with infinite constraints";
11+
12+
[Test]
13+
[Category(UITestCategories.ScrollView)]
14+
public void ScrollViewContentLayoutMeasuresWithInfiniteConstraints()
15+
{
16+
var measuredHeight = App.WaitForElement("StubLabel").GetText()!;
17+
ClassicAssert.AreEqual("200", measuredHeight);
18+
}
19+
}
20+
}

src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ protected override void ConnectHandler(UIScrollView platformView)
2424
{
2525
base.ConnectHandler(platformView);
2626

27-
if (platformView is MauiScrollView platformScrollView)
27+
if (platformView is ICrossPlatformLayoutBacking platformScrollView)
2828
{
2929
platformScrollView.CrossPlatformLayout = this;
3030
}
@@ -34,7 +34,7 @@ protected override void ConnectHandler(UIScrollView platformView)
3434

3535
protected override void DisconnectHandler(UIScrollView platformView)
3636
{
37-
if (platformView is MauiScrollView platformScrollView)
37+
if (platformView is ICrossPlatformLayoutBacking platformScrollView)
3838
{
3939
platformScrollView.CrossPlatformLayout = null;
4040
}

src/Core/src/Platform/iOS/MauiScrollView.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Microsoft.Maui.Platform
88
{
9-
public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents
9+
public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents, ICrossPlatformLayoutBacking
1010
{
1111
bool _invalidateParentWhenMovedToWindow;
1212
double _lastMeasureHeight;
@@ -16,12 +16,14 @@ public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents
1616

1717
WeakReference<ICrossPlatformLayout>? _crossPlatformLayoutReference;
1818

19-
internal ICrossPlatformLayout? CrossPlatformLayout
19+
ICrossPlatformLayout? ICrossPlatformLayoutBacking.CrossPlatformLayout
2020
{
2121
get => _crossPlatformLayoutReference != null && _crossPlatformLayoutReference.TryGetTarget(out var v) ? v : null;
2222
set => _crossPlatformLayoutReference = value == null ? null : new WeakReference<ICrossPlatformLayout>(value);
2323
}
2424

25+
internal ICrossPlatformLayout? CrossPlatformLayout => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout;
26+
2527
public override void LayoutSubviews()
2628
{
2729
// LayoutSubviews is invoked while scrolling, so we need to arrange the content only when it's necessary.
@@ -36,11 +38,13 @@ public override void LayoutSubviews()
3638
_lastArrangeWidth = widthConstraint;
3739
_lastArrangeHeight = heightConstraint;
3840

39-
// If the SuperView is a MauiView (backing a cross-platform ContentView or Layout), then measurement
40-
// has already happened via SizeThatFits and doesn't need to be repeated in LayoutSubviews. But we
41-
// _do_ need LayoutSubviews to make a measurement pass if the parent is something else (for example,
41+
// If the SuperView is a cross-platform layout backed view (i.e. MauiView, MauiScrollView, LayoutView, ..),
42+
// then measurement has already happened via SizeThatFits and doesn't need to be repeated in LayoutSubviews.
43+
// This is especially important to avoid overriding potentially infinite measurement constraints
44+
// imposed by the parent (i.e. scroll view) with the current bounds.
45+
// But we _do_ need LayoutSubviews to make a measurement pass if the parent is something else (for example,
4246
// the window); there's no guarantee that SizeThatFits has been called in that case.
43-
if (!IsMeasureValid(widthConstraint, heightConstraint) && Superview is not MauiView)
47+
if (!IsMeasureValid(widthConstraint, heightConstraint) && !this.IsFinalMeasureHandledBySuperView())
4448
{
4549
crossPlatformLayout.CrossPlatformMeasure(widthConstraint, heightConstraint);
4650
CacheMeasureConstraints(widthConstraint, heightConstraint);

src/Core/src/Platform/iOS/MauiView.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,13 @@ public override void LayoutSubviews()
125125
var widthConstraint = bounds.Width;
126126
var heightConstraint = bounds.Height;
127127

128-
// If the SuperView is a MauiView (backing a cross-platform ContentView or Layout), then measurement
129-
// has already happened via SizeThatFits and doesn't need to be repeated in LayoutSubviews. But we
130-
// _do_ need LayoutSubviews to make a measurement pass if the parent is something else (for example,
128+
// If the SuperView is a cross-platform layout backed view (i.e. MauiView, MauiScrollView, LayoutView, ..),
129+
// then measurement has already happened via SizeThatFits and doesn't need to be repeated in LayoutSubviews.
130+
// This is especially important to avoid overriding potentially infinite measurement constraints
131+
// imposed by the parent (i.e. scroll view) with the current bounds.
132+
// But we _do_ need LayoutSubviews to make a measurement pass if the parent is something else (for example,
131133
// the window); there's no guarantee that SizeThatFits has been called in that case.
132-
133-
if (!IsMeasureValid(widthConstraint, heightConstraint) && Superview is not MauiView)
134+
if (!IsMeasureValid(widthConstraint, heightConstraint) && !this.IsFinalMeasureHandledBySuperView())
134135
{
135136
CrossPlatformMeasure(widthConstraint, heightConstraint);
136137
CacheMeasureConstraints(widthConstraint, heightConstraint);

src/Core/src/Platform/iOS/ViewExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,5 +945,7 @@ internal static float GetDisplayDensity(this UIView? view) =>
945945
internal static bool ShowSoftInput(this UIView inputView) => inputView.BecomeFirstResponder();
946946

947947
internal static bool IsSoftInputShowing(this UIView inputView) => inputView.IsFirstResponder;
948+
949+
internal static bool IsFinalMeasureHandledBySuperView(this UIView? view) => view?.Superview is ICrossPlatformLayoutBacking { CrossPlatformLayout: not null };
948950
}
949951
}

src/Core/src/Platform/iOS/WrapperView.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@
99

1010
namespace Microsoft.Maui.Platform
1111
{
12-
public partial class WrapperView : UIView, IDisposable, IUIViewLifeCycleEvents
12+
public partial class WrapperView : UIView, IDisposable, IUIViewLifeCycleEvents, ICrossPlatformLayoutBacking
1313
{
1414
bool _fireSetNeedsLayoutOnParentWhenWindowAttached;
1515
WeakReference<ICrossPlatformLayout>? _crossPlatformLayoutReference;
1616

17-
internal ICrossPlatformLayout? CrossPlatformLayout
17+
ICrossPlatformLayout? ICrossPlatformLayoutBacking.CrossPlatformLayout
1818
{
1919
get => _crossPlatformLayoutReference != null && _crossPlatformLayoutReference.TryGetTarget(out var v) ? v : null;
2020
set => _crossPlatformLayoutReference = value == null ? null : new WeakReference<ICrossPlatformLayout>(value);
2121
}
22+
23+
internal ICrossPlatformLayout? CrossPlatformLayout
24+
{
25+
get => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout;
26+
set => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout = value;
27+
}
2228

2329
double _lastMeasureHeight = double.NaN;
2430
double _lastMeasureWidth = double.NaN;

0 commit comments

Comments
 (0)