[iOS 26] Fix for Shell ForegroundColor not applied to ToolbarItems#34085
[iOS 26] Fix for Shell ForegroundColor not applied to ToolbarItems#34085SyedAbdulAzeemSF4852 wants to merge 4 commits intodotnet:mainfrom
Conversation
… the reported issue ID. Also added the baseline snapshot for iOS.
|
Hey there @@SyedAbdulAzeemSF4852! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This pull request fixes an iOS 26-specific issue where Shell.ForegroundColor is not applied to toolbar items due to Apple's LiquidGlass redesign changes. In iOS 26+, UINavigationBar.TintColor no longer automatically propagates to bar button items, requiring explicit TintColor setting on each item.
Changes:
- Added
UpdateRightBarButtonItemTintColors()method to explicitly set TintColor on right bar button items for iOS 26+ and MacCatalyst 26+ - Updated property change handlers (
HandleShellPropertyChanged,OnPagePropertyChanged) to call the new method when ForegroundColor changes - Added UI test (Issue34083) with screenshot verification to validate the fix
Reviewed changes
Copilot reviewed 3 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ShellPageRendererTracker.cs | Added iOS 26+ workaround to explicitly set TintColor on right bar button items when Shell.ForegroundColor changes |
| Issue34083.cs (HostApp) | Test page demonstrating Shell with ForegroundColor and toolbar item |
| Issue34083.cs (Tests) | UI test with screenshot verification for toolbar item color |
| VerifyShellForegroundColorIsAppliedToToolbarItems.png (ios) | Baseline screenshot for iOS < 26 |
| VerifyShellForegroundColorIsAppliedToToolbarItems.png (ios-26) | Baseline screenshot for iOS 26+ |
Comments suppressed due to low confidence (1)
src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs:612
- The fix only applies TintColor to right bar button items (toolbar items), but the left bar button item (back button/flyout icon) may also be affected by the iOS 26 LiquidGlass change. The
UpdateLeftToolbarItemsmethod creates aUIBarButtonItemat line 575-576, but its TintColor is not explicitly set for iOS 26+.
Consider also updating the left bar button item's TintColor in UpdateLeftToolbarItems when iOS 26+ is detected, similar to how UpdateRightBarButtonItemTintColors handles right bar button items. The left bar button item is set in the callback at lines 575-576, so you would need to apply the TintColor there:
NavigationItem.LeftBarButtonItem =
new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (s, e) => LeftBarButtonItemHandler(ViewController, IsRootPage)) { Enabled = enabled };
// Add for iOS 26+
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
var foregroundColor = _context?.Shell?.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ??
_context?.Shell?.GetValue(Shell.ForegroundColorProperty);
if (foregroundColor is Graphics.Color shellForegroundColor)
{
NavigationItem.LeftBarButtonItem.TintColor = shellForegroundColor.ToPlatform();
}
} void UpdateLeftToolbarItems()
{
var shell = _context?.Shell;
var mauiContext = MauiContext;
if (shell is null || NavigationItem is null || mauiContext is null)
{
return;
}
var behavior = BackButtonBehavior;
var image = behavior.GetPropertyIfSet<ImageSource?>(BackButtonBehavior.IconOverrideProperty, null);
var enabled = behavior.GetPropertyIfSet(BackButtonBehavior.IsEnabledProperty, true);
var text = behavior.GetPropertyIfSet<string?>(BackButtonBehavior.TextOverrideProperty, null);
var command = behavior.GetPropertyIfSet<object?>(BackButtonBehavior.CommandProperty, null);
var backButtonVisible = behavior.GetPropertyIfSet<bool>(BackButtonBehavior.IsVisibleProperty, true);
if (String.IsNullOrWhiteSpace(text) && image == null)
{
//Add the FlyoutIcon only if the FlyoutBehavior is Flyout
if (_flyoutBehavior == FlyoutBehavior.Flyout)
{
image = shell.FlyoutIcon;
}
}
if (!IsRootPage)
{
NavigationItem.HidesBackButton = !backButtonVisible;
image = backButtonVisible ? image : null;
}
image.LoadImage(mauiContext, result =>
{
if (ViewController is null)
return;
UIImage? icon = null;
if (image is not null)
{
icon = result?.Value;
var foregroundColor = _context?.Shell.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ??
_context?.Shell.GetValue(Shell.ForegroundColorProperty);
if (foregroundColor is null)
{
icon = icon?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
}
var originalImageSize = icon?.Size ?? CGSize.Empty;
// The largest height you can use for navigation bar icons in iOS.
// Per Apple's Human Interface Guidelines, the navigation bar height is 44 points,
// so using the full height ensures maximum visual clarity and maintains consistency
// with iOS design standards. This allows icons to utilize the entire available
// vertical space within the navigation bar container.
var defaultIconHeight = 44f;
var buffer = 0.1;
// We only check height because the navigation bar constrains vertical space (44pt height),
// but allows horizontal flexibility. Width can vary based on icon design and content,
// while height must fit within the fixed navigation bar bounds to avoid clipping.
// if the image is bigger than the default available size, resize it
if (icon is not null && originalImageSize.Height - defaultIconHeight > buffer)
{
if (image is not FontImageSource fontImageSource || !fontImageSource.IsSet(FontImageSource.SizeProperty))
{
icon = icon.ResizeImageSource(originalImageSize.Width, defaultIconHeight, originalImageSize);
}
}
}
else if (String.IsNullOrWhiteSpace(text) && IsRootPage && _flyoutBehavior == FlyoutBehavior.Flyout)
{
icon = DrawHamburger();
}
if (icon != null)
{
NavigationItem.LeftBarButtonItem =
new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (s, e) => LeftBarButtonItemHandler(ViewController, IsRootPage)) { Enabled = enabled };
}
else
{
NavigationItem.LeftBarButtonItem = null;
UpdateBackButtonTitle();
}
if (NavigationItem.LeftBarButtonItem != null)
{
if (String.IsNullOrWhiteSpace(image?.AutomationId))
{
if (IsRootPage)
{
NavigationItem.LeftBarButtonItem.AccessibilityIdentifier = "OK";
NavigationItem.LeftBarButtonItem.AccessibilityLabel = "Menu";
}
else
NavigationItem.LeftBarButtonItem.AccessibilityIdentifier = "Back";
}
else
{
NavigationItem.LeftBarButtonItem.AccessibilityIdentifier = image.AutomationId;
}
if (image != null)
{
#pragma warning disable CS0618 // Type or member is obsolete
NavigationItem.LeftBarButtonItem.SetAccessibilityHint(image);
NavigationItem.LeftBarButtonItem.SetAccessibilityLabel(image);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
});
UpdateBackButtonTitle();
}
| if (!(OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (NavigationItem?.RightBarButtonItems is not { Length: > 0 } rightItems) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var foregroundColor = _context?.Shell?.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ?? | ||
| _context?.Shell?.GetValue(Shell.ForegroundColorProperty); | ||
|
|
||
| if (foregroundColor is Graphics.Color shellForegroundColor) | ||
| { | ||
| var platformColor = shellForegroundColor.ToPlatform(); | ||
| foreach (var item in rightItems) | ||
| { | ||
| item.TintColor = platformColor; |
There was a problem hiding this comment.
The version check logic is inverted. The condition !(OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) returns early when the version is LESS than 26, meaning the TintColor update only happens on iOS 26+. This is correct based on the PR description.
However, consider using a positive condition for better readability:
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
// Update TintColor for iOS 26+ only
if (NavigationItem?.RightBarButtonItems is not { Length: > 0 } rightItems)
{
return;
}
// ... rest of the logic
}This makes it immediately clear that the code executes FOR iOS 26+, not AGAINST it.
| if (!(OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))) | |
| { | |
| return; | |
| } | |
| if (NavigationItem?.RightBarButtonItems is not { Length: > 0 } rightItems) | |
| { | |
| return; | |
| } | |
| var foregroundColor = _context?.Shell?.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ?? | |
| _context?.Shell?.GetValue(Shell.ForegroundColorProperty); | |
| if (foregroundColor is Graphics.Color shellForegroundColor) | |
| { | |
| var platformColor = shellForegroundColor.ToPlatform(); | |
| foreach (var item in rightItems) | |
| { | |
| item.TintColor = platformColor; | |
| if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) | |
| { | |
| if (NavigationItem?.RightBarButtonItems is not { Length: > 0 } rightItems) | |
| { | |
| return; | |
| } | |
| var foregroundColor = _context?.Shell?.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ?? | |
| _context?.Shell?.GetValue(Shell.ForegroundColorProperty); | |
| if (foregroundColor is Graphics.Color shellForegroundColor) | |
| { | |
| var platformColor = shellForegroundColor.ToPlatform(); | |
| foreach (var item in rightItems) | |
| { | |
| item.TintColor = platformColor; | |
| } |
| @@ -0,0 +1,24 @@ | |||
| #if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS // Android Issue Link: https://github.com/dotnet/maui/issues/24676, Windows Issue Link: https://github.com/dotnet/maui/issues/34071 | |||
There was a problem hiding this comment.
The test includes a preprocessor directive TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS which disables the test on Android and Windows. The comment references two separate issues for Android and Windows failures.
However, this test should NOT fail on these platforms - it's testing iOS-specific behavior (the test page is marked PlatformAffected.iOS). The test should simply not run on Android/Windows.
Consider using a cleaner approach:
- Remove the
TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWSdirective entirely - The test framework should automatically skip tests for platforms not affected based on the
PlatformAffected.iOSattribute in the Issue attribute - If platform-specific test execution is needed, use
#if IOS || MACCATALYSTinstead
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Updated the issue tracker ID, file name, and class name to align with the reported issue ID. Also added the baseline snapshot for iOS. ·
|
| File:Line | Reviewer Says | Status |
|---|---|---|
ShellPageRendererTracker.cs:488 |
Suggest using positive condition instead of negated early return | |
Issue34083.cs:1 |
TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS — suggest removing, but this IS the standard pattern for iOS-only tests |
ℹ️ Not an issue - standard codebase pattern |
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34085 | Explicitly set TintColor on right bar button items for iOS 26+ in UpdateRightBarButtonItemTintColors() |
⏳ PENDING (Gate) | ShellPageRendererTracker.cs (+32) |
Original PR |
🚦 Gate — Test Verification
📝 Review Session — Updated the issue tracker ID, file name, and class name to align with the reported issue ID. Also added the baseline snapshot for iOS. · 6c05a8f
Result: ❌ FAILED
Platform: ios
Mode: Full Verification
Test Results
| Check | Expected | Actual | Result |
|---|---|---|---|
| Tests FAIL without fix | FAIL | PASS | ❌ |
| Tests PASS with fix | PASS | PASS | ✅ |
Analysis
The Gate verification failed because the test passes both with and without the fix. The reference snapshot files (ios-26/VerifyShellForegroundColorIsAppliedToToolbarItems.png and ios/VerifyShellForegroundColorIsAppliedToToolbarItems.png) appear to have been captured while the bug was still present (showing the default system tint color, not purple).
When the fix is reverted, the UI matches the reference snapshot (buggy state = default color), causing the test to pass. This means the test does not effectively catch the regression.
Conclusion
⛔ Gate FAILED - The test snapshots were captured showing the buggy state (toolbar items with default system tint color, not purple). The snapshots need to be re-taken after the fix is applied and verified working (showing purple toolbar items).
🔧 Fix — Analysis & Comparison
📝 Review Session — Updated the issue tracker ID, file name, and class name to align with the reported issue ID. Also added the baseline snapshot for iOS. · 6c05a8f
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34085 | Explicitly set TintColor on right bar button items for iOS 26+ in UpdateRightBarButtonItemTintColors() |
❌ SKIPPED (Gate failed) | ShellPageRendererTracker.cs (+32) |
Original PR |
Exhausted: N/A - try-fix was skipped because Gate failed (tests pass regardless of fix state)
Reason for skip: The reference snapshots in the PR were captured while the bug was present (showing default system tint, not purple). Since VerifyScreenshot() compares against those snapshots, the test passes both with and without the fix. Running try-fix would produce unreliable/misleading results.
Selected Fix: PR's fix — The code approach is directionally correct (pending fixes noted in the Report). The fix correctly targets the iOS 26+ behavior change with the right method calls.
📋 Report — Final Recommendation
📝 Review Session — Updated the issue tracker ID, file name, and class name to align with the reported issue ID. Also added the baseline snapshot for iOS. · 6c05a8f
⚠️ Final Recommendation: REQUEST CHANGES
Summary
PR #34085 addresses a real iOS 26 regression where Apple's LiquidGlass redesign broke UINavigationBar.TintColor automatic propagation to bar button items. The fix approach (explicitly setting TintColor per item for iOS 26+ and MacCatalyst 26+) is directionally correct. However, there are two issues that need to be addressed before merge:
- Critical: Test snapshots captured with bug present — Gate verification confirms tests pass both with and without the fix, meaning the reference snapshots show the buggy state (default tint, not purple). Tests provide false confidence.
- Functional: Missing TintColor reset when ForegroundColor is cleared at runtime — If a user sets then clears
Shell.ForegroundColor, the toolbar items will retain the previously-set TintColor instead of reverting to the system default.
Root Cause
On iOS 26, Apple's LiquidGlass redesign changed how UINavigationBar.TintColor propagates to bar button items — it is no longer automatically inherited by UIBarButtonItem instances. Previously, MAUI relied on UIKit to propagate the navigation bar's TintColor to individual UIBarButtonItem instances. Starting with iOS 26, this propagation must be done explicitly by the app.
Code Review Findings
🔴 Test Snapshots Captured in Buggy State (High)
Files:
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/VerifyShellForegroundColorIsAppliedToToolbarItems.pngsrc/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifyShellForegroundColorIsAppliedToToolbarItems.png
Problem: Gate verification confirmed tests pass both WITH and WITHOUT the fix applied. The reference snapshots show the toolbar items in the default system tint color (the buggy state), not in purple. As a result, VerifyScreenshot() passes whether or not the toolbar items are purple — the test doesn't verify the fix is working.
Fix required: Delete the existing snapshots and re-run the test suite on iOS 26 with the fix confirmed working (toolbar items must appear purple/in the configured ForegroundColor). The new snapshots captured from a passing state will serve as the correct baseline.
🔴 Missing TintColor Reset When ForegroundColor is Cleared (Medium)
File: src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs
Problem: UpdateRightBarButtonItemTintColors() only sets TintColor when foregroundColor is a Color. If the user removes/unsets ForegroundColor (sets to null), the TintColor on existing bar button items is never reset — the previous color will persist permanently.
// Current code - missing the null/reset case
if (foregroundColor is Graphics.Color shellForegroundColor)
{
var platformColor = shellForegroundColor.ToPlatform();
foreach (var item in rightItems)
{
item.TintColor = platformColor;
}
}
// Missing: else branch to reset to system defaultFix required:
if (foregroundColor is Graphics.Color shellForegroundColor)
{
var platformColor = shellForegroundColor.ToPlatform();
foreach (var item in rightItems)
{
item.TintColor = platformColor;
}
}
else
{
foreach (var item in rightItems)
{
item.TintColor = null; // Reset to system default
}
}🟡 Code Readability: Negative Condition (Low)
File: ShellPageRendererTracker.cs, line ~470
The negated early-return condition makes intent slightly less clear:
// Current
if (!(OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)))
{
return;
}Consider using a positive guard block for readability. Minor style concern only — functionally correct as-is.
✅ Looks Good
- iOS 26+ version guard is correct — early return on pre-26 ensures no behavior change for older iOS versions
- Property change hooks are comprehensive —
UpdateRightBarButtonItemTintColors()is called fromUpdateToolbarItems(),HandleShellPropertyChanged, andOnPagePropertyChanged - ForegroundColor fallback chain is correct — checks
CurrentPagefirst, then falls back to Shell level (consistent with existingUpdateLeftToolbarItems()pattern) - MacCatalyst 26+ included — correctly applies to both iOS 26+ and MacCatalyst 26+
TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWS— this IS the correct standard pattern for iOS-only tests in this codebase (10+ existing tests use it). Automated reviewer concern is incorrect.- PR title and description are accurate and include the required NOTE block.
Issues to Address
| Issue | Impact | Action Required |
|---|---|---|
| Test snapshots captured in buggy state | 🔴 High | Delete snapshots, re-run tests on iOS 26 with fix working to capture correct baseline |
| Missing TintColor reset for null ForegroundColor | 🟡 Medium | Add else branch resetting item.TintColor = null |
| Negative condition readability | 🟡 Low | Optional: refactor to positive guard block |
📋 Expand PR Finalization Review
Title: ✅ Good
Current: [iOS 26] Fix for Shell ForegroundColor not applied to ToolbarItems
Description: ✅ Good
Description needs updates. See details below.
✨ Suggested PR Description
[!NOTE]
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
- On iOS 26 and Mac Catalyst 26+, setting
Shell.ForegroundColor(either at the Shell level or Page level) no longer applies the specified color to ToolbarItems. - The toolbar items remain in the default system tint color instead of respecting the configured foreground color.
Root Cause
On iOS 26, Apple's LiquidGlass redesign changed how UINavigationBar.TintColor propagates to bar button items — it is no longer automatically inherited by UIBarButtonItem instances.
Description of Change
- Added
UpdateRightBarButtonItemTintColors()method inShellPageRendererTracker.csto explicitly setTintColorof right bar button items to the Shell's foreground color for iOS 26+ and Mac Catalyst 26+. This ensures toolbar items correctly reflect the intended color. - Updated property change handlers (
HandleShellPropertyChanged,OnPagePropertyChanged) to callUpdateRightBarButtonItemTintColors()when relevant properties change, guaranteeing color updates are applied on dynamic property changes. - Called
UpdateRightBarButtonItemTintColors()fromUpdateToolbarItems()to apply the color on initial render.
Issues Fixed
Fixes #34083
Validated the behaviour in the following platforms
- Windows
- Android
- iOS
- Mac
Output
| Before | After |
|---|---|
![]() |
![]() |
Code Review: ✅ Passed
PR #34085 Code Review
Code Review Findings
🟡 Medium Issue
Missing TintColor Reset When ForegroundColor Is Cleared
- File:
src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs - Method:
UpdateRightBarButtonItemTintColors() - Problem: The method only sets
TintColorwhen a non-nullShell.ForegroundColoris found. If a user setsShell.ForegroundColorand later clears it (sets to null/unset), the oldTintColorwill remain on the bar button items indefinitely. The items won't revert to the system-default tint color.
Current code:
if (foregroundColor is Graphics.Color shellForegroundColor)
{
var platformColor = shellForegroundColor.ToPlatform();
foreach (var item in rightItems)
{
item.TintColor = platformColor;
}
}
// No else: TintColor stays at old value if foregroundColor is nullRecommendation: Add an else branch to reset to null (system default):
if (foregroundColor is Graphics.Color shellForegroundColor)
{
var platformColor = shellForegroundColor.ToPlatform();
foreach (var item in rightItems)
{
item.TintColor = platformColor;
}
}
else
{
foreach (var item in rightItems)
{
item.TintColor = null; // Reset to system default
}
}🟡 Minor Issue
Title Omits Mac Catalyst Coverage
- Problem: The PR title
[iOS 26]does not mention Mac Catalyst 26+, even though the code explicitly includesOperatingSystem.IsMacCatalystVersionAtLeast(26)in the version guard. - Recommendation: Change title to
[iOS/Mac Catalyst 26] Fix Shell.ForegroundColor not applied to ToolbarItems
🔵 Trivial Issues
Missing trailing newlines in test files
Both new test files are missing a trailing newline (\ No newline at end of file):
src/Controls/tests/TestCases.HostApp/Issues/Issue34083.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34083.cs
These should end with a newline per standard code style.
✅ Looks Good
- Version guard is correct:
OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)— properly targets only the affected OS versions. - Color lookup precedence is correct:
CurrentPageforeground color checked first, then falls back toShelllevel — this mirrors how the existing Shell theming logic works. - All relevant call sites updated:
UpdateToolbarItems(),HandleShellPropertyChanged(), andOnPagePropertyChanged()all callUpdateRightBarButtonItemTintColors(), ensuring color is applied on initial render and on dynamic property changes. - Snapshot baseline provided: Two snapshot files added for
ios/andios-26/, ensuring the test has baseline images for both OS versions. - Test guards are correct:
#if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_WINDOWScorrectly limits the test to iOS and Mac Catalyst, since this is an iOS-specific behavior change. ThePlatformAffected.iOSattribute on the HostApp issue page is also consistent. - No unnecessary scope change: New method is private (no access modifier) — not exposing this workaround as overridable API is appropriate given its narrow iOS 26-specific nature.


Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
Root Cause
Description of Change
Issues Fixed
Fixes #34083
Validated the behaviour in the following platforms
Output