Skip to content
Open
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
28 changes: 9 additions & 19 deletions src/Controls/samples/Controls.Sample.Sandbox/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
namespace Maui.Controls.Sample;
namespace Maui.Controls.Sample;

public partial class App : Application
{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState? activationState)
{
// To test shell scenarios, change this to true
bool useShell = false;
public App()
{
InitializeComponent();
}

if (!useShell)
{
return new Window(new NavigationPage(new MainPage()));
}
else
{
return new Window(new SandboxShell());
}
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new SandboxShell());
}
}
7 changes: 7 additions & 0 deletions src/Controls/samples/Controls.Sample.Sandbox/MainPage2.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?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.MainPage2"
Title="Settings">
<Label Text="Settings Page" HorizontalOptions="Center" VerticalOptions="Center" />
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Maui.Controls.Sample;

public partial class MainPage2 : ContentPage
{
public MainPage2()
{
InitializeComponent();
}
}
32 changes: 11 additions & 21 deletions src/Controls/samples/Controls.Sample.Sandbox/SandboxShell.xaml
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
<Shell
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.SandboxShell"
xmlns:local="clr-namespace:Maui.Controls.Sample"
x:Name="shell">
<TabBar Shell.TabBarForegroundColor="Green"
Shell.TabBarUnselectedColor="Red">
<Tab Title="MainPage1" Icon="groceries.png">
<ShellContent Icon="groceries.png"
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Tab>
<Tab Title="MainPage2" Icon="dotnet_bot.png">
<ShellContent Icon="dotnet_bot.png"
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage2" />
</Tab>
<?xml version="1.0" encoding="utf-8" ?>
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Maui.Controls.Sample"
x:Class="Maui.Controls.Sample.SandboxShell"
Shell.TabBarForegroundColor="Green"
Shell.TabBarUnselectedColor="Red">
<TabBar>
<ShellContent Title="Home" ContentTemplate="{DataTemplate local:MainPage}" Icon="{OnPlatform iOS='house.fill', Default='house.png'}" />
<ShellContent Title="Settings" ContentTemplate="{DataTemplate local:MainPage2}" Icon="{OnPlatform iOS='gear', Default='gear.png'}" />
</TabBar>
</Shell>
</Shell>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.ComponentModel;
using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit;

Expand All @@ -13,6 +14,11 @@ public class SafeShellTabBarAppearanceTracker : IShellTabBarAppearanceTracker
UIColor _defaultTint;
UIColor _defaultUnselectedTint;
UITabBarAppearance _tabBarAppearance;

// iOS 26+ stores pending colors to re-apply during layout
UIColor _pendingUnselectedTintColor;
UIColor _pendingSelectedTintColor;

public virtual void ResetAppearance(UITabBarController controller)
{
if (_defaultTint == null)
Expand All @@ -22,6 +28,8 @@ public virtual void ResetAppearance(UITabBarController controller)
tabBar.BarTintColor = _defaultBarTint;
tabBar.TintColor = _defaultTint;
tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
_pendingUnselectedTintColor = null;
_pendingSelectedTintColor = null;
}

public virtual void SetAppearance(UITabBarController controller, ShellAppearance appearance)
Expand Down Expand Up @@ -50,6 +58,19 @@ public virtual void SetAppearance(UITabBarController controller, ShellAppearance

public virtual void UpdateLayout(UITabBarController controller)
{
// iOS 26+: Re-apply colors on every layout pass.
// The liquid glass tab bar resets subview properties during layout.
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
var tabBar = controller.TabBar;
if (_pendingSelectedTintColor is not null)
tabBar.TintColor = _pendingSelectedTintColor;
if (_pendingUnselectedTintColor is not null)
{
tabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
tabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
}
}
}

#region IDisposable Support
Expand All @@ -76,6 +97,55 @@ void UpdateiOS15TabBarAppearance(UITabBarController controller, ShellAppearance
var unselectedColor = appearanceElement.EffectiveTabBarUnselectedColor;
var titleColor = appearanceElement.EffectiveTabBarTitleColor;

// iOS 26+: The UITabBarAppearance Normal state (TitleTextAttributes, IconColor) is
// ignored by the liquid glass tab bar. Skip the full appearance pipeline and use
// direct UITabBar properties plus subview coloring instead.
// See: https://github.com/dotnet/maui/issues/32125, https://github.com/dotnet/maui/issues/34605
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
var tabBar = controller.TabBar;

// Background color via appearance (this still works on iOS 26)
if (_tabBarAppearance == null)
{
_tabBarAppearance = new UITabBarAppearance();
_tabBarAppearance.ConfigureWithDefaultBackground();
}
if (backgroundColor is not null)
_tabBarAppearance.BackgroundColor = backgroundColor.ToPlatform();

tabBar.StandardAppearance = _tabBarAppearance;
tabBar.ScrollEdgeAppearance = _tabBarAppearance;

// Selected color via TintColor (works on iOS 26)
var selectedColor = foregroundColor ?? titleColor;
if (selectedColor is not null)
{
_pendingSelectedTintColor = selectedColor.ToPlatform();
tabBar.TintColor = _pendingSelectedTintColor;
}
else
{
_pendingSelectedTintColor = null;
tabBar.TintColor = _defaultTint;
}

// Unselected color: set property + pre-colored images for visual rendering
if (unselectedColor is not null)
{
_pendingUnselectedTintColor = unselectedColor.ToPlatform();
tabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
tabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
}
else
{
_pendingUnselectedTintColor = null;
tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
}

return;
Comment on lines +120 to +146
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

In the iOS 26+ early-return path, _pendingSelectedTintColor/_pendingUnselectedTintColor are only set when the corresponding selectedColor/unselectedColor is non-null. If an app later removes these colors (e.g., dynamic resources revert to default), the pending fields and UITabBar properties will keep reapplying the old values in UpdateLayout(), effectively making the colors “sticky”. Consider explicitly clearing the pending fields and restoring tabBar.TintColor / tabBar.UnselectedItemTintColor to the defaults when the effective colors are null/default.

Copilot uses AI. Check for mistakes.
}

controller.TabBar
.UpdateiOS15TabBarAppearance(
ref _tabBarAppearance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using UIKit;
using static Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page;
using PageUIStatusBarAnimation = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIStatusBarAnimation;
Expand All @@ -29,6 +30,10 @@ public class TabbedRenderer : UITabBarController, IPlatformViewHandler
UITabBarAppearance _tabBarAppearance;
WeakReference<VisualElement> _element;

// iOS 26+: cached unselected tint color for re-application during layout
UIColor _pendingUnselectedTintColor;
UIColor _pendingSelectedTintColor;

Brush _currentBarBackground;

IMauiContext MauiContext => _mauiContext;
Expand Down Expand Up @@ -136,6 +141,15 @@ public override void ViewDidLayoutSubviews()
// in narrow viewports (< 667 points) before Element is set. Guard against this.
if (Element is IView view)
view.Arrange(View.Bounds.ToRectangle());

// iOS 26+: Re-apply unselected item colors on every layout pass.
// The liquid glass tab bar resets subview tint colors during layout.
if ((OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
&& _pendingUnselectedTintColor is not null && TabBar is not null)
{
TabBar.UnselectedItemTintColor = _pendingUnselectedTintColor;
TabBar.ApplyPreColoredImagesForIOS26(_pendingUnselectedTintColor, _pendingSelectedTintColor);
}
}

protected override void Dispose(bool disposing)
Expand Down Expand Up @@ -190,7 +204,7 @@ void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e)
UpdateTabBarItem(page);
}
}

public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection)
{
if (previousTraitCollection.VerticalSizeClass == TraitCollection.VerticalSizeClass)
Expand Down Expand Up @@ -610,15 +624,32 @@ void UpdateiOS15TabBarAppearance()
{
if (Tabbed is not TabbedPage tabbed)
return;

var unselectedTabColor = tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null;
var barTextColor = tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null;

TabBar.UpdateiOS15TabBarAppearance(
ref _tabBarAppearance,
_defaultBarColor,
_defaultBarTextColor,
tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null,
tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null,
unselectedTabColor,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null,
tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null,
tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null);
barTextColor,
barTextColor);

// Cache the effective colors for iOS 26 layout re-application
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
_pendingUnselectedTintColor = unselectedTabColor?.ToPlatform()
?? barTextColor?.ToPlatform()
?? _defaultBarTextColor;

var selectedTabColor2 = tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null;
_pendingSelectedTintColor = selectedTabColor2?.ToPlatform()
?? barTextColor?.ToPlatform()
?? _defaultBarTextColor;
}
}

#region IPlatformViewHandler
Expand Down
45 changes: 37 additions & 8 deletions src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ public partial class ShellTests
public async Task ForegroundColorSetsIconAndTitleColorSetsTitle()
{
#if IOS || MACCATALYST
// Skip on iOS 26+ due to UITabBar internal API changes
// See: https://github.com/dotnet/maui/issues/33004
// Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
Expand Down Expand Up @@ -54,8 +53,7 @@ await RunShellTabBarTests(shell =>
public async Task ShellTabBarTitleColorInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
// Skip on iOS 26+ due to UITabBar internal API changes
// See: https://github.com/dotnet/maui/issues/33004
// Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
Expand All @@ -78,8 +76,7 @@ await RunShellTabBarTests(shell => Shell.SetTabBarTitleColor(shell, expectedColo
public async Task ShellTabBarForegroundInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
// Skip on iOS 26+ due to UITabBar internal API changes
// See: https://github.com/dotnet/maui/issues/33004
// Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
Expand All @@ -100,8 +97,7 @@ await RunShellTabBarTests(shell => Shell.SetTabBarForegroundColor(shell, expecte
public async Task ShellTabBarUnselectedColorInitializesCorrectly(string colorHex)
{
#if IOS || MACCATALYST
// Skip on iOS 26+ due to UITabBar internal API changes
// See: https://github.com/dotnet/maui/issues/33004
// Pixel-based tab bar color verification doesn't work on iOS 26+ due to UITabBar rendering changes
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
return;
#endif
Expand All @@ -116,6 +112,39 @@ await RunShellTabBarTests(shell => Shell.SetTabBarUnselectedColor(shell, expecte
});
}

// Regression test for https://github.com/dotnet/maui/issues/32125
// On iOS 26+, Shell.TabBarUnselectedColor was not applied to unselected tabs
[Fact(DisplayName = "Shell TabBar UnselectedColor and TitleColor work together")]
public async Task ShellTabBarUnselectedAndTitleColorWorkTogether()
{
var titleColor = Color.FromArgb("#FFFF0000");
var unselectedColor = Color.FromArgb("#FF808080");
await RunShellTabBarTests(shell =>
{
Shell.SetTabBarTitleColor(shell, titleColor);
Shell.SetTabBarUnselectedColor(shell, unselectedColor);
},
async (shell) =>
{
#if IOS || MACCATALYST
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
// On iOS 26+, verify the UnselectedItemTintColor property directly
// because pixel-based verification doesn't work with the new tab bar rendering
await ValidateTabBarUnselectedTintColorProperty(shell.CurrentSection, unselectedColor);
}
else
#endif
{
// Selected tab should use title color
await ValidateTabBarTextColor(shell.CurrentSection, titleColor, true);
// Unselected tab should use unselected color
await ValidateTabBarTextColor(shell.Items[0].Items[1], unselectedColor, true);
await ValidateTabBarIconColor(shell.Items[0].Items[1], unselectedColor, true);
}
});
}

async Task RunShellTabBarTests(Action<Shell> setup, Func<Shell, Task> runTest)
{
SetupBuilder();
Expand Down
Loading
Loading