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
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,15 @@ protected virtual void Dispose(bool disposing)
}

if (NavigationItem?.TitleView is TitleViewContainer tvc)
{
// Explicitly null out the native TitleView to break the UIKit reference chain
// that prevents the page from being garbage collected when x:Name is used
// together with Shell.TitleView. The NameScope attached to the TitleView
// children holds a reference back to the page (via the registered x:Name),
// so clearing this native reference is necessary to allow GC.
NavigationItem.TitleView = null;
tvc.Disconnect();
}

_keyboardWillHideObserver?.Dispose();
_keyboardWillHideObserver = null;
Expand Down
90 changes: 90 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue34975.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 34975, "Title view memory leak when using Shell TitleView and x Name", PlatformAffected.iOS)]
public class Issue34975 : Shell
{
public Issue34975()
{
FlyoutBehavior = FlyoutBehavior.Flyout;
Routing.RegisterRoute("Issue34975_second", typeof(Issue34975SecondPage));

var navigateButton = new Button
{
Text = "Navigate to Second Page",
AutomationId = "NavigateButton",
};

var checkButton = new Button
{
Text = "Check Memory",
AutomationId = "CheckMemoryButton",
IsVisible = false,
};

var statusLabel = new Label
{
Text = "1. Tap Navigate, then Tap Check Memory",
FontSize = 14,
HorizontalOptions = LayoutOptions.Center,
AutomationId = "StatusLabel",
};

navigateButton.Clicked += async (s, e) =>
{
Issue34975SecondPage.Instances.Clear();

await Shell.Current.GoToAsync("Issue34975_second");

await Shell.Current.GoToAsync("..");

// A small delay lets that continuation run before we expose CheckMemoryButton.
await Task.Delay(500);

checkButton.IsVisible = true;
statusLabel.Text = "Now tap Check Memory";
};

checkButton.Clicked += async (s, e) =>
{
var instances = Issue34975SecondPage.Instances;
if (instances.Count == 0)
{
statusLabel.Text = "Navigate first";
return;
}

statusLabel.Text = "Checking...";
try
{
await GarbageCollectionHelper.WaitForGC(5000, instances.ToArray());
statusLabel.Text = "Test passed";
}
catch
{
statusLabel.Text = "Memory Leak Detected";
}
};

var mainPage = new ContentPage
{
Content = new VerticalStackLayout
{
Padding = new Thickness(20),
Spacing = 15,
VerticalOptions = LayoutOptions.Center,
Children =
{
statusLabel,
navigateButton,
checkButton,
}
}
};

Items.Add(new ShellContent
{
Content = mainPage,
Route = "Issue34975_main",
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue34975SecondPage"
x:Name="issue34975SecondPage">

<Shell.TitleView>
<Label Text="Test Title View"/>
</Shell.TitleView>

<VerticalStackLayout Padding="30,0" Spacing="25" VerticalOptions="Center">
<Label Text="Second Page — use the native back button to navigate back."
AutomationId="SecondPageLabel"
HorizontalOptions="Center"/>
</VerticalStackLayout>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Maui.Controls.Sample.Issues;

// x:Name="issue34975SecondPage" in the XAML is the key trigger for the memory leak.
// It causes the page to register itself in its own XAML NameScope, which combined with
// Shell.TitleView creates a retain cycle on iOS that prevents GC collection.
public partial class Issue34975SecondPage : ContentPage
{
public static List<WeakReference> Instances { get; } = [];

public Issue34975SecondPage()
{
InitializeComponent();
Instances.Add(new WeakReference(this));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue34975 : _IssuesUITest
{
public override string Issue => "Title view memory leak when using Shell TitleView and x Name";

public Issue34975(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.Shell)]
public void ShellTitleViewWithXNameShouldNotLeakMemory()
{
App.WaitForElement("NavigateButton");
App.Tap("NavigateButton");

App.WaitForElement("CheckMemoryButton");
App.Tap("CheckMemoryButton");

App.WaitForTextToBePresentInElement("StatusLabel", "Test passed", timeout: TimeSpan.FromSeconds(15));
Assert.That(App.FindElement("StatusLabel").GetText(), Does.Contain("Test passed"),
"Memory leak detected: SecondPage instances were not garbage collected.");
}
}
Loading