Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ public virtual void UpdateFlowDirection()
{
foreach (var child in ItemsView.LogicalChildrenInternal)
{
// Skip the empty view element — its flow direction is handled
// separately in AlignEmptyView to avoid double application
if (child == _emptyViewFormsElement)
{
continue;
}

if (child is VisualElement ve && ve.Handler?.PlatformView is UIView view)
{
view.UpdateFlowDirection(ve);
Expand Down Expand Up @@ -775,37 +782,30 @@ void AlignEmptyView()
return;
}

bool isRtl;

if (OperatingSystem.IsIOSVersionAtLeast(10) || OperatingSystem.IsTvOSVersionAtLeast(10))
isRtl = CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft;
else
isRtl = CollectionView.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft;

if (isRtl)
if (_emptyViewFormsElement is not null)
{
if (_emptyUIView.Transform.A == -1)
// The empty view's FlowDirection is handled here instead of in UpdateFlowDirection()
// to ensure proper alignment independent of the CollectionView's layout flip behavior.
if (_emptyViewFormsElement.Handler?.PlatformView is UIView emptyView)
{
return;
emptyView.UpdateFlowDirection(_emptyViewFormsElement);
}

FlipEmptyView();
}
else
else if (_emptyUIView is UILabel label)
{
if (_emptyUIView.Transform.A == -1)
// For UILabel, set the text alignment to center to ensure consistent behavior with Windows and Android
label.TextAlignment = UITextAlignment.Center;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unconditionally setting label.TextAlignment = UITextAlignment.Center is a behavioral change for users who previously relied on the implicit (Natural / Left) alignment of an internal UILabel-rendered string EmptyView. The PR description mentions this was done "to provide a better user experience" and to align with Windows/Android, but it is technically a regression for any user comparing pixel-exact baselines. Consider only forcing center when no alignment was previously set, or document this in release notes.

label.SemanticContentAttribute = ItemsView.FlowDirection switch
{
FlipEmptyView();
}
FlowDirection.RightToLeft => UISemanticContentAttribute.ForceRightToLeft,
FlowDirection.LeftToRight => UISemanticContentAttribute.ForceLeftToRight,
_ => CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft
? UISemanticContentAttribute.ForceRightToLeft
: UISemanticContentAttribute.ForceLeftToRight
};
}
}

void FlipEmptyView()
{
// Flip the empty view 180 degrees around the X axis
_emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1);
}

void ShowEmptyView()
{
if (_emptyViewDisplayed || _emptyUIView == null)
Expand Down
66 changes: 42 additions & 24 deletions src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ public virtual void UpdateFlowDirection()
itemsView.UpdateFlowDirection(ItemsView);
foreach (var child in ItemsView.LogicalChildrenInternal)
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
{
// Skip the empty view element — its flow direction is handled
// separately in AlignEmptyView to avoid double application
if (child == _emptyViewFormsElement)
{
continue;
}

if (child is VisualElement ve && ve.Handler?.PlatformView is UIView view)
{
view.UpdateFlowDirection(ve);
Expand All @@ -320,7 +327,7 @@ public virtual void UpdateFlowDirection()
cell.Label.UpdateFlowDirection(ItemsView);
}
}

CollectionView.UpdateFlowDirection(ItemsView);
}

Expand Down Expand Up @@ -529,37 +536,30 @@ void AlignEmptyView()
return;
}
Comment thread
Dhivya-SF4094 marked this conversation as resolved.

bool isRtl;

if (OperatingSystem.IsIOSVersionAtLeast(10) || OperatingSystem.IsTvOSVersionAtLeast(10))
isRtl = CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft;
else
isRtl = CollectionView.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft;

if (isRtl)
if (_emptyViewFormsElement is not null)
{
if (_emptyUIView.Transform.A == -1)
// The empty view's FlowDirection is handled here instead of in UpdateFlowDirection()
// to ensure proper alignment independent of the CollectionView's layout flip behavior.
if (_emptyViewFormsElement.Handler?.PlatformView is UIView emptyView)
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
{
return;
emptyView.UpdateFlowDirection(_emptyViewFormsElement);
}

FlipEmptyView();
}
else
else if (_emptyUIView is UILabel label)
{
if (_emptyUIView.Transform.A == -1)
// For UILabel, set the text alignment to center to ensure consistent behavior with Windows and Android
label.TextAlignment = UITextAlignment.Center;
label.SemanticContentAttribute = ItemsView.FlowDirection switch
{
FlipEmptyView();
}
FlowDirection.RightToLeft => UISemanticContentAttribute.ForceRightToLeft,
FlowDirection.LeftToRight => UISemanticContentAttribute.ForceLeftToRight,
_ => CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft
? UISemanticContentAttribute.ForceRightToLeft
: UISemanticContentAttribute.ForceLeftToRight
};
}
}

void FlipEmptyView()
{
// Flip the empty view 180 degrees around the X axis
_emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1);
}

void ShowEmptyView()
{
if (_emptyViewDisplayed || _emptyUIView == null)
Expand All @@ -568,7 +568,25 @@ void ShowEmptyView()
}

_emptyUIView.Tag = EmptyTag;
CollectionView.AddSubview(_emptyUIView);

// Add the empty view to the CollectionView's superview instead of the CollectionView itself.
// The compositional layout's flipsHorizontallyInOppositeLayoutDirection (default true) causes
// the CollectionView to flip its content coordinate system when SemanticContentAttribute is
// ForceRightToLeft. Layout-managed views (cells, supplementary views) are compensated by the
// layout, but direct subviews are NOT — resulting in mirror-flipped rendering.
// Adding to the superview avoids this flip zone entirely.
var targetView = CollectionView.Superview;
if (targetView is not null)
{
targetView.InsertSubviewAbove(_emptyUIView, CollectionView);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding _emptyUIView to CollectionView.Superview introduces lifetime/parenting risks: (1) if the CollectionView is later moved to a different superview (e.g. layout reparenting), the empty view stays on the old superview and may continue to render or leak. (2) When the controller is torn down, HideEmptyView's RemoveFromSuperview removes it correctly, but TearDownEmptyView should ensure no stale reference remains. (3) Z-ordering: InsertSubviewAbove(_emptyUIView, CollectionView) places it above the CollectionView, but other siblings (e.g. RefreshControl wrapper or container chrome) added later by parent code could be placed below it, creating visual hit-test surprises.

}
else
{
// TODO: DetermineEmptyViewFrame() returns superview-coordinate-space values (CollectionView.Frame.X/Y),
// which are incorrect when the empty view is a child of CollectionView. This fallback is unlikely
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
// to execute in practice since Superview is expected to be non-null by the time ShowEmptyView() is called.
CollectionView.AddSubview(_emptyUIView);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The author's TODO acknowledges that DetermineEmptyViewFrame() returns superview-coordinate values that are wrong if _emptyUIView is added as a child of CollectionView. If Superview is genuinely never null at this point, prefer making this an Debug.Assert(targetView is not null) and removing the broken fallback — silently rendering at wrong coordinates is worse than failing fast in debug. If the fallback is truly needed, fix DetermineEmptyViewFrame() to use local coordinates.

}

if (((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) == -1)
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue32404.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 32404, "[Android, iOS, MacOS] FlowDirection not working on EmptyView in CollectionView", PlatformAffected.iOS | PlatformAffected.macOS)]
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
public class Issue32404 : ContentPage
{
Label flowDirectionLabel;
CollectionView emptyViewStringCollectionView;
CollectionView emptyViewViewCollectionView;
CollectionView emptyViewTemplateCollectionView;

public Issue32404()
{
var grid = new Grid
{
Padding = new Thickness(10),
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Star },
new RowDefinition { Height = GridLength.Star },
new RowDefinition { Height = GridLength.Star }
}
};

var toggleButton = new Button
{
Text = "Toggle FlowDirection",
HorizontalOptions = LayoutOptions.Center,
Margin = new Thickness(0, 10),
AutomationId = "Issue32404ToggleButton"
};

toggleButton.Clicked += OnToggleFlowDirectionClicked;
grid.Add(toggleButton);
Grid.SetRow(toggleButton, 0);

flowDirectionLabel = new Label
{
Text = "Current FlowDirection: LeftToRight",
HorizontalOptions = LayoutOptions.Center,
FontAttributes = FontAttributes.Bold
};
grid.Add(flowDirectionLabel);
Grid.SetRow(flowDirectionLabel, 1);

// String EmptyView
emptyViewStringCollectionView = new CollectionView
{
BackgroundColor = Colors.LightGray,
FlowDirection = FlowDirection.LeftToRight,
EmptyView = "EmptyView Text (String)",
AutomationId = "CollectionView1"
};

// View EmptyView
emptyViewViewCollectionView = new CollectionView
{
BackgroundColor = Colors.LightBlue,
FlowDirection = FlowDirection.LeftToRight,
AutomationId = "CollectionView2"
};

var emptyViewGrid = new Grid();
var emptyViewLabel = new Label
{
Text = "EmptyView (Grid View)",
};
emptyViewGrid.Add(emptyViewLabel);
emptyViewViewCollectionView.EmptyView = emptyViewGrid;

// DataTemplate EmptyView
emptyViewTemplateCollectionView = new CollectionView
{
BackgroundColor = Colors.LightGreen,
FlowDirection = FlowDirection.LeftToRight,
AutomationId = "CollectionView3"
};

emptyViewTemplateCollectionView.EmptyViewTemplate = new DataTemplate(() =>
{
var stackLayout = new VerticalStackLayout();
var templateLabel = new Label
{
Text = "EmptyView Template",
};

stackLayout.Add(templateLabel);
return stackLayout;
});

grid.Add(emptyViewStringCollectionView);
Grid.SetRow(emptyViewStringCollectionView, 2);
grid.Add(emptyViewViewCollectionView);
Grid.SetRow(emptyViewViewCollectionView, 3);
grid.Add(emptyViewTemplateCollectionView);
Grid.SetRow(emptyViewTemplateCollectionView, 4);
// Set Grid as Content
Content = grid;
}

void OnToggleFlowDirectionClicked(object sender, EventArgs e)
{
// Toggle between LeftToRight and RightToLeft
if (emptyViewStringCollectionView.FlowDirection == FlowDirection.LeftToRight)
{
emptyViewStringCollectionView.FlowDirection = FlowDirection.RightToLeft;
emptyViewViewCollectionView.FlowDirection = FlowDirection.RightToLeft;
emptyViewTemplateCollectionView.FlowDirection = FlowDirection.RightToLeft;
flowDirectionLabel.Text = "Current FlowDirection: RightToLeft";
}
else
{
emptyViewStringCollectionView.FlowDirection = FlowDirection.LeftToRight;
emptyViewViewCollectionView.FlowDirection = FlowDirection.LeftToRight;
emptyViewTemplateCollectionView.FlowDirection = FlowDirection.LeftToRight;
flowDirectionLabel.Text = "Current FlowDirection: LeftToRight";
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File missing trailing newline (No newline at end of file). Minor — consistent with existing TestCases.HostApp files that should end with newline per .editorconfig.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#if TEST_FAILS_ON_WINDOWS // https://github.com/dotnet/maui/issues/18551
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No maccatalyst/ snapshot baseline was added. The PR includes baselines for android/, ios/, ios-26/, and mac/ only. On Catalyst test runs the framework falls back to the mac/ baseline (or fails to find any baseline), and the rendering does not match — this is exactly what the PR Gate caught (16.74% diff on FlowDirectionShouldWorkOnEmptyView_RightToLeft.png). Either add maccatalyst/ baselines or document that this test is expected to be excluded on MacCatalyst.

using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue32404 : _IssuesUITest
{
public Issue32404(TestDevice testDevice) : base(testDevice)
{
}
public override string Issue => "[Android, iOS, MacOS] FlowDirection not working on EmptyView in CollectionView";

[Test]
[Category(UITestCategories.CollectionView)]
public void FlowDirectionShouldWorkOnEmptyView()
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
{
App.WaitForElement("Issue32404ToggleButton");
App.Tap("Issue32404ToggleButton");
VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_RightToLeft");
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
App.Tap("Issue32404ToggleButton");
VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_LeftToRight");
Comment thread
Dhivya-SF4094 marked this conversation as resolved.
}
}
#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading