[iOS] Fix tab bar unselected colors not rendering on iOS 26+#34688
[iOS] Fix tab bar unselected colors not rendering on iOS 26+#34688jfversluis wants to merge 1 commit intomainfrom
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34688Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34688" |
🧪 PR Test EvaluationOverall Verdict: The core fix scenarios are covered by well-written device tests, but there are a few coverage gaps: the Shell path lacks an iOS 26+ test for
📊 Expand Full EvaluationPR Test Evaluation ReportPR: #34688 — Fix iOS 26+ tab bar unselected colors for TabbedPage and Shell Overall VerdictThe TabbedPage iOS 26+ scenarios are well covered, but the Shell fix is only tested with the 1. Fix Coverage — ✅The new tests directly exercise the fix's primary code paths:
Tests would fail if 2. Edge Cases & Gaps —
|
| Fix Code Path | Covered By Test |
|---|---|
TabbedRenderer.UpdateiOS15TabBarAppearance — iOS 26+ path sets _pendingUnselectedTintColor from barTextColor |
UnselectedItemTintColorSetFromBarTextColor |
TabbedRenderer.UpdateiOS15TabBarAppearance — iOS 26+ path: unselectedTabColor takes priority |
UnselectedItemTintColorSetFromUnselectedTabColor |
TabbedRenderer.ViewDidLayoutSubviews — dynamic re-application |
ChangingBarTextColorUpdatesUnselectedItemTintColor (indirectly) |
SafeShellTabBarAppearanceTracker.UpdateiOS15TabBarAppearance — iOS 26+ unselected path |
ShellTabBarUnselectedAndTitleColorWorkTogether |
TabbedViewExtensions.UpdateiOS15TabBarAppearance — iOS 26+ direct property setting |
All TabbedPage iOS 26 tests |
TabbedViewExtensions.ApplyPreColoredImagesForIOS26 — icon pre-coloring |
❌ Not covered |
Recommendations
-
Add Shell iOS 26+ unselected-only test: Add a test to
ShellTabBarTests.cs(or.iOS.cs) that calls onlyShell.SetTabBarUnselectedColor(noSetTabBarTitleColor) and verifiesValidateTabBarUnselectedTintColorPropertyon iOS 26+. This covers theif (unselectedColor is not null)branch in the Shell tracker's iOS 26 path. -
Resolve Mac Catalyst skip on
BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor: Track down why this fails on Mac Catalyst and either fix it or add a property-based Mac Catalyst variant alongside the pixel-based test. The fix's Mac Catalyst code path is otherwise unvalidated for this scenario. -
Minor dedup opportunity:
UnselectedItemTintColorSetFromBarTextColorandChangingBarTextColorUpdatesUnselectedItemTintColoroverlap on the Red→Blue case. Consider consolidating by keeping the combined test and removing the duplicate initial assertion fromChangingBarTextColorUpdatesUnselectedItemTintColor. Low priority.
Warning
⚠️ Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
dc.services.visualstudio.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "dc.services.visualstudio.com"See Network Configuration for more information.
Note
🔒 Integrity filtering filtered 1 item
Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.
- pr:[iOS] Fix tab bar unselected colors not rendering on iOS 26+ #34688 (
pull_request_read: Resource 'pr:[iOS] Fix tab bar unselected colors not rendering on iOS 26+ #34688' has lower integrity than agent requires. Agent would need to drop integrity tags [unapproved:all approved:all] to trust this resource.)
🧪 Test evaluation by Evaluate PR Tests
There was a problem hiding this comment.
Pull request overview
Fixes iOS 26+ “liquid glass” tab bar rendering regressions where unselected tab text/icon colors (TabbedPage + Shell) are ignored by the system tint pipeline, by switching to per-item title attributes and pre-colored (AlwaysOriginal) images as a workaround.
Changes:
- Add iOS 26+ code paths to apply unselected/selected colors via direct
UITabBarproperties plus pre-colored tab images. - Re-apply colors during layout for iOS 26+ because UIKit may reset values during layout passes.
- Add/adjust device tests to validate iOS 26+ behavior via property-based assertions (since pixel-based verification is unreliable).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Core/src/Platform/iOS/TabbedViewExtensions.cs | Adds iOS 26+ pre-colored image workaround + adjusts tab icon resizing behavior. |
| src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs | Caches/reapplies effective colors on iOS 26+ during layout. |
| src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs | Adds iOS 26+ early-return appearance path and re-applies colors during layout. |
| src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.iOS.cs | Adds iOS 26+ regression tests verifying UnselectedItemTintColor behavior. |
| src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs | Updates/clarifies existing tests and adds a non-iOS26 regression scenario test. |
| src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.iOS.cs | Improves tab bar lookup and adds helper to validate UnselectedItemTintColor. |
| src/Controls/tests/DeviceTests/Elements/Shell/ShellTabBarTests.cs | Adds iOS 26+ regression test using property-based validation fallback. |
| /// Must be called on every layout pass because UIKit may reset these during layout. | ||
| /// See: https://github.com/dotnet/maui/issues/32125, https://github.com/dotnet/maui/issues/34605 | ||
| /// </summary> | ||
| [System.Runtime.Versioning.SupportedOSPlatform("ios26.0")] |
There was a problem hiding this comment.
ApplyPreColoredImagesForIOS26 is invoked for both iOS and MacCatalyst (callers guard on IsMacCatalystVersionAtLeast(26)), but the method is only annotated with [SupportedOSPlatform("ios26.0")]. In nullable-enabled projects this can trigger platform compatibility diagnostics for MacCatalyst builds. Consider adding a matching [SupportedOSPlatform("maccatalyst26.0")] (or adjusting naming/annotations) so the analyzer matches actual supported usage.
| [System.Runtime.Versioning.SupportedOSPlatform("ios26.0")] | |
| [System.Runtime.Versioning.SupportedOSPlatform("ios26.0")] | |
| [System.Runtime.Versioning.SupportedOSPlatform("maccatalyst26.0")] |
| if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) | ||
| { | ||
| return resizedImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); | ||
| } | ||
|
|
There was a problem hiding this comment.
AutoResizeTabBarImage now forces AlwaysTemplate on iOS/MacCatalyst 26+. ResizeImageSource already preserves the input image rendering mode, so this change will override callers which intentionally provide AlwaysOriginal tab icons (e.g., multi-color icons) and can change app appearance. If the intent is only to ensure a template mask for the iOS 26 workaround, consider keeping the original rendering mode here and doing any template conversion only inside the iOS 26 tinting path.
| if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)) | |
| { | |
| return resizedImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate); | |
| } |
| item.Image = template.ApplyTintColor(unselectedColor) | ||
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); | ||
|
|
||
| if (selectedColor is not null) | ||
| { | ||
| item.SelectedImage = template.ApplyTintColor(selectedColor) | ||
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); |
There was a problem hiding this comment.
ApplyPreColoredImagesForIOS26 creates new tinted UIImage instances for each tab item every time it's called, and callers invoke it on every layout pass (e.g., ViewDidLayoutSubviews / UpdateLayout). This can cause avoidable allocations and GC pressure during frequent layouts. Consider caching the tinted images per UITabBarItem+color (or only regenerating when the effective colors change) and simply re-assigning cached instances when UIKit resets the images.
| item.Image = template.ApplyTintColor(unselectedColor) | |
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); | |
| if (selectedColor is not null) | |
| { | |
| item.SelectedImage = template.ApplyTintColor(selectedColor) | |
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); | |
| // Only (re)apply tint when the current image is still the original | |
| // template. This avoids allocating new tinted UIImage instances on | |
| // every layout pass when nothing has changed. | |
| if (ReferenceEquals(img, template)) | |
| { | |
| item.Image = template.ApplyTintColor(unselectedColor) | |
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); | |
| if (selectedColor is not null) | |
| { | |
| item.SelectedImage = template.ApplyTintColor(selectedColor) | |
| ?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal); | |
| } |
| // Selected color via TintColor (works on iOS 26) | ||
| var selectedColor = foregroundColor ?? titleColor; | ||
| if (selectedColor is not null) | ||
| { | ||
| _pendingSelectedTintColor = selectedColor.ToPlatform(); | ||
| tabBar.TintColor = _pendingSelectedTintColor; | ||
| } | ||
|
|
||
| // 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); | ||
| } | ||
|
|
||
| return; |
There was a problem hiding this comment.
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.
| // TabbedRenderer IS a UITabBarController — get TabBar directly from it | ||
| if (tabbedPage.Handler?.PlatformView is UITabBarController tbc) |
There was a problem hiding this comment.
In GetTabBar, the fast-path tabbedPage.Handler?.PlatformView is UITabBarController will never succeed for TabbedRenderer: IElementHandler.PlatformView is explicitly implemented to return NativeView (a UIView), not the UITabBarController itself. This makes the comment misleading and forces the fallback path every time. Consider using (tabbedPage.Handler as IPlatformViewHandler)?.ViewController (or the handler passed into CreateHandlerAndAddToWindow) to get the UITabBarController directly.
| // TabbedRenderer IS a UITabBarController — get TabBar directly from it | |
| if (tabbedPage.Handler?.PlatformView is UITabBarController tbc) | |
| // Try to get UITabBarController directly from the handler's ViewController | |
| var platformHandler = tabbedPage.Handler as IPlatformViewHandler; | |
| if (platformHandler?.ViewController is UITabBarController tbc) |
|
/azp run maui-pr-uitests |
On iOS 26+, Apple's liquid glass tab bar compositing pipeline ignores UITabBarAppearance Normal state (TitleTextAttributes, IconColor) AND UITabBar.UnselectedItemTintColor for visual rendering, even though the properties are stored correctly. This caused TabbedPage.BarTextColor and Shell.TabBarUnselectedColor to have no visual effect on unselected tabs. Fix: Bypass the tint pipeline entirely on iOS 26+ by using pre-colored images with AlwaysOriginal rendering mode (via UIImage.ApplyTintColor), which bakes the color into image pixel data. This is the same approach used for the iOS 26 back button color fix (PR #34326). Also set per-item SetTitleTextAttributes for text color. Cache original template images in a ConditionalWeakTable to avoid quality degradation from repeated AlwaysOriginal→Template round-trips. For Shell: Early-return in SafeShellTabBarAppearanceTracker on iOS 26+, skipping the full appearance pipeline. Cache pending colors for re-application in UpdateLayout since liquid glass resets properties. For TabbedPage: Cache effective colors in TabbedRenderer and re-apply in ViewDidLayoutSubviews. Pre-iOS 26 behavior is unchanged. Fixes #32125 Fixes #34605 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
86a1043 to
7207ec9
Compare
🧪 PR Test EvaluationOverall Verdict: Tests provide solid coverage of the main iOS 26+ property-setting paths (
📊 Expand Full EvaluationPR Test Evaluation ReportPR: #34688 — Fix iOS 26+ tab bar unselected colors for TabbedPage and Shell Overall VerdictThe property-based device tests do a good job verifying that 1. Fix Coverage —
|
| Test | Assertion Quality |
|---|---|
UnselectedItemTintColorSetFromBarTextColor |
✅ Specific — checks exact color with tolerance |
UnselectedItemTintColorSetFromUnselectedTabColor |
✅ Specific — checks exact color with tolerance |
ChangingBarTextColorUpdatesUnselectedItemTintColor (null clear) |
BarTextColor should restore a default; the test doesn't assert the expected default value. |
ShellTabBarUnselectedAndTitleColorWorkTogether (iOS 26+ path) |
UnselectedItemTintColor, not TintColor (selected color) |
9. Fix-Test Alignment — ✅
The test files correctly target the same controls modified by the fix (TabbedPage and Shell). Tests are in the matching DeviceTests/Elements/TabbedPage/ and DeviceTests/Elements/Shell/ directories. The new iOS-specific tests verify the iOS 26+ code paths introduced in TabbedViewExtensions.cs, TabbedRenderer.cs, and SafeShellTabBarAppearanceTracker.cs.
Recommendations
-
Add a test for
ApplyPreColoredImagesForIOS26— After setting a color and triggering layout, verify that at least one tab item has its image inUIImageRenderingMode.AlwaysOriginal. This is the most important untested code path. Can be done inTabbedPageTests.iOS.cswithin an existingCreateHandlerAndAddToWindowblock. -
Strengthen the null-clear assertion — In
ChangingBarTextColorUpdatesUnselectedItemTintColor, aftertabbedPage.BarTextColor = null, assert the expected value (e.g.,nullor the system default tint), not just that it's not the previous colors. -
Add Shell selected-color assertion — In
ShellTabBarUnselectedAndTitleColorWorkTogetherfor the iOS 26+ branch, also verifytabBar.TintColorequalstitleColor(the selected tab color), since the fix sets it viatabBar.TintColor = _pendingSelectedTintColor. -
(Optional) Resolve the Mac Catalyst TabbedPage skip — The
Skip = "Fails on Mac Catalyst, fixme"note onBarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColorsuggests a gap in Mac Catalyst coverage. Consider investigating and addressing in a follow-up.
Warning
⚠️ Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
dc.services.visualstudio.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "dc.services.visualstudio.com"See Network Configuration for more information.
Note
🔒 Integrity filtering filtered 1 item
Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.
- pr:[iOS] Fix tab bar unselected colors not rendering on iOS 26+ #34688 (
pull_request_read: Resource 'pr:[iOS] Fix tab bar unselected colors not rendering on iOS 26+ #34688' has lower integrity than agent requires. Agent would need to drop integrity tags [unapproved:all approved:all] to trust this resource.)
🧪 Test evaluation by Evaluate PR Tests
Description
On iOS 26+, Apple's liquid glass tab bar compositing pipeline ignores
UITabBarAppearanceNormal state (TitleTextAttributes,IconColor) ANDUITabBar.UnselectedItemTintColorfor visual rendering, even though the properties are stored correctly. This caused:TabbedPage.BarTextColorto have no visual effect on unselected tabs ([iOS] TabbedPage BarTextColor not applied to unselected tabs #34605)Shell.TabBarUnselectedColorto have no visual effect ([iOS26] TabBarUnselectedColor not working on ios #32125)Root Cause
The iOS 26 liquid glass tab bar uses a dual-layer compositing architecture:
destOutCALayer compositing filter to cut out the selected tabThe compositing pipeline strips all
TintColor/UnselectedItemTintColorfrom the rendering path, regardless of whether they're set via appearance or direct properties.Fix
Bypass the tint pipeline entirely on iOS 26+ by using pre-colored images with
AlwaysOriginalrendering mode viaUIImage.ApplyTintColor(), which bakes the color into image pixel data. This is the same proven approach used for the iOS 26 back button color fix (PR #34326).What changed:
TabbedViewExtensions.cs(shared):ApplyPreColoredImagesForIOS26()method that creates pre-colored tab icon copies usingAlwaysOriginalrenderingConditionalWeakTableto avoid quality degradationSetTitleTextAttributesfor text colorSafeShellTabBarAppearanceTracker.cs(Shell):UpdateLayout()(liquid glass resets properties during layout)TabbedRenderer.cs(TabbedPage):ViewDidLayoutSubviews()Pre-iOS 26 behavior is completely unchanged.
Why not
AlwaysTemplate+UnselectedItemTintColor?This was attempted in the previously closed PR #32153, but
AlwaysTemplaterelies on the tint pipeline — which is exactly what iOS 26 liquid glass ignores.AlwaysOriginalbakes color into pixel data, which survives the compositing.Test Results
Issues Fixed
Fixes #32125
Fixes #34605