Skip to content

[iOS] Fix tab bar unselected colors not rendering on iOS 26+#34688

Open
jfversluis wants to merge 1 commit intomainfrom
Fix-iOS-26-bugs-ff88
Open

[iOS] Fix tab bar unselected colors not rendering on iOS 26+#34688
jfversluis wants to merge 1 commit intomainfrom
Fix-iOS-26-bugs-ff88

Conversation

@jfversluis
Copy link
Member

Description

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:

Root Cause

The iOS 26 liquid glass tab bar uses a dual-layer compositing architecture:

  • SelectedContentView — renders all tabs with the selected tint (visible layer)
  • ContentView — renders all tabs with unselected style (composited behind)
  • DestOutView — uses destOut CALayer compositing filter to cut out the selected tab

The compositing pipeline strips all TintColor/UnselectedItemTintColor from 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 AlwaysOriginal rendering mode via UIImage.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):

  • New ApplyPreColoredImagesForIOS26() method that creates pre-colored tab icon copies using AlwaysOriginal rendering
  • Caches original template images in a ConditionalWeakTable to avoid quality degradation
  • Sets per-item SetTitleTextAttributes for text color
  • On iOS 26+: skips Normal appearance state, sets direct properties + pre-colored images

SafeShellTabBarAppearanceTracker.cs (Shell):

  • iOS 26+ early-return path that skips the full appearance pipeline
  • Caches pending colors for re-application in UpdateLayout() (liquid glass resets properties during layout)

TabbedRenderer.cs (TabbedPage):

  • Caches effective colors and re-applies pre-colored images in ViewDidLayoutSubviews()

Pre-iOS 26 behavior is completely unchanged.

Why not AlwaysTemplate + UnselectedItemTintColor?

This was attempted in the previously closed PR #32153, but AlwaysTemplate relies on the tint pipeline — which is exactly what iOS 26 liquid glass ignores. AlwaysOriginal bakes color into pixel data, which survives the compositing.

Test Results

Category iOS 26.2 iOS 18.5
TabbedPage ✅ 12 passed, 0 failed ✅ 12 passed, 0 failed
Shell ✅ 202 passed, 0 failed ✅ 201 passed, 1 failed (pre-existing)

Issues Fixed

Fixes #32125
Fixes #34605

Copilot AI review requested due to automatic review settings March 26, 2026 20:55
@github-actions
Copy link
Contributor

github-actions bot commented Mar 26, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34688

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34688"

@github-actions
Copy link
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

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 UnselectedColor-only (without TitleColor), some pre-iOS 26 tests now skip entirely on iOS 26+ CI rather than asserting new-path behavior, and BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor is marked Skip on Mac Catalyst despite the fix applying there too.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34688 — Fix iOS 26+ tab bar unselected colors for TabbedPage and Shell
Test files evaluated: 4 device test files
Fix files: 3 (Shell tracker, TabbedRenderer, TabbedViewExtensions)


Overall Verdict

⚠️ Tests need improvement

The TabbedPage iOS 26+ scenarios are well covered, but the Shell fix is only tested with the TitleColor + UnselectedColor combination. There is no iOS 26+ property-based test for Shell with UnselectedColor alone. Several existing pixel-based tests now return early on iOS 26+ (appropriate, given the rendering changes), but this means a gap exists for validating those color paths on iOS 26+ CI.


1. Fix Coverage — ✅

The new tests directly exercise the fix's primary code paths:

  • UnselectedItemTintColorSetFromBarTextColor — verifies BarTextColorUnselectedItemTintColor on iOS 26+ for TabbedPage (issue [iOS] TabbedPage BarTextColor not applied to unselected tabs #34605)
  • UnselectedItemTintColorSetFromUnselectedTabColor — verifies UnselectedTabColor takes priority over BarTextColor on iOS 26+ (issue [iOS26] TabBarUnselectedColor not working on ios #32125)
  • ChangingBarTextColorUpdatesUnselectedItemTintColor — verifies dynamic color changes and clearing
  • ShellTabBarUnselectedAndTitleColorWorkTogether — uses ValidateTabBarUnselectedTintColorProperty to verify the Shell fix on iOS 26+

Tests would fail if _pendingUnselectedTintColor caching or tabBar.UnselectedItemTintColor assignment were removed from the fix.


2. Edge Cases & Gaps — ⚠️

Covered:

  • BarTextColorUnselectedItemTintColor on iOS 26+ (TabbedPage)
  • UnselectedTabColor overrides BarTextColor priority on iOS 26+ (TabbedPage)
  • Dynamic color change (Red → Blue) on iOS 26+ (TabbedPage)
  • Clearing BarTextColor (set to null) on iOS 26+ (TabbedPage)
  • Shell: TitleColor + UnselectedColor combined on iOS 26+

Missing:

  • Shell UnselectedColor-only on iOS 26+: ShellTabBarUnselectedColorInitializesCorrectly skips on iOS 26+. There is no property-based test verifying that setting only Shell.SetTabBarUnselectedColor (without SetTabBarTitleColor) works on iOS 26+. The fix has this code path (if (unselectedColor is not null) in UpdateiOS15TabBarAppearance) but it goes untested for iOS 26+.
  • Layout re-application mechanism: The ViewDidLayoutSubviews re-application loop (key to the "liquid glass resets state" bug) is not verified. A test that triggers a layout pass and asserts colors survive would validate this mechanism directly.
  • ApplyPreColoredImagesForIOS26 (icon coloring): The pre-colored image path is acknowledged to be untestable via pixel comparison on iOS 26+, but no property assertion (e.g., checking item.Image.RenderingMode == AlwaysOriginal) exists to verify the images were at least set.

3. Test Type Appropriateness — ✅

Current: Device Tests
Recommendation: Appropriate — no lighter type would work here

The fix targets UITabBar.UnselectedItemTintColor, UITabBarController, and per-item image rendering. These require a running iOS platform with actual native objects. Unit tests cannot mock UITabBar or OperatingSystem.IsIOSVersionAtLeast() effectively. Device tests are the correct level.


4. Convention Compliance — ✅

Automated script reports 0 convention issues.

  • [Fact] / [Theory] attributes used correctly (xUnit)
  • [Category(TestCategory.Shell)] / [Category(TestCategory.TabbedPage)] applied at class level
  • Tests use async Task correctly
  • Partial class pattern (ShellTests, TabbedPageTests) with platform-specific .iOS.cs files follows existing conventions

One observation: BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor and two other TabbedPage tests carry #if MACCATALYST, Skip = "Fails on Mac Catalyst, fixme" inline, which is a minor violation of the "no inline #if" guideline. These are pre-existing patterns in this file and the skips are noted, not new violations.


5. Flakiness Risk — ✅ Low

  • All iOS 26+ tests guard with if (!OperatingSystem.IsIOSVersionAtLeast(26) && !OperatingSystem.IsMacCatalystVersionAtLeast(26)) return; — they simply skip on non-target platforms
  • Property-based assertions (tabBar.UnselectedItemTintColor) are synchronous and stable
  • ColorComparison.ARGBEquivalent(..., 0.1) tolerance avoids brittle exact comparison
  • No Task.Delay or Thread.Sleep usage in new tests
  • No VerifyScreenshot usage (correctly avoided given iOS 26+ pixel comparison issues)

6. Duplicate Coverage — ⚠️ Potential overlap

UnselectedItemTintColorSetFromBarTextColor already sets Red and then verifies Red→Blue in the same test body. ChangingBarTextColorUpdatesUnselectedItemTintColor independently tests Red→Green→null. There is meaningful overlap but the second test adds the null-clearing scenario which is distinct. The overlap is minor and both tests are readable on their own.


7. Platform Scope — ⚠️

  • Fix code applies to both iOS and MacCatalyst (all IsIOSVersionAtLeast(26) checks are paired with IsMacCatalystVersionAtLeast(26))
  • The three new TabbedPageTests.iOS.cs tests correctly include both IsIOSVersionAtLeast(26) and IsMacCatalystVersionAtLeast(26) in their guards — they will run on Mac Catalyst 26+ CI
  • ShellTabBarUnselectedAndTitleColorWorkTogether also has no Mac Catalyst skip ✅
  • Concern: BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor (TabbedPage) is Skip = "Fails on Mac Catalyst, fixme" — Mac Catalyst coverage for the BarTextColor → all-tabs scenario is missing

8. Assertion Quality — ✅

Assert.NotNull(tabBar.UnselectedItemTintColor);
Assert.True(
    ColorComparison.ARGBEquivalent(tabBar.UnselectedItemTintColor, Colors.Red.ToPlatform(), 0.1),
    $"Expected UnselectedItemTintColor to be Red but got {tabBar.UnselectedItemTintColor}");
  • Two-layer assertion: null check then color comparison
  • Tolerance 0.1 avoids brittle exact color matching while still being specific
  • Failure messages include the actual value for debugging
  • No magic numbers or Assert.True(true) patterns

9. Fix-Test Alignment — ✅

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

  1. Add Shell iOS 26+ unselected-only test: Add a test to ShellTabBarTests.cs (or .iOS.cs) that calls only Shell.SetTabBarUnselectedColor (no SetTabBarTitleColor) and verifies ValidateTabBarUnselectedTintColorProperty on iOS 26+. This covers the if (unselectedColor is not null) branch in the Shell tracker's iOS 26 path.

  2. 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.

  3. Minor dedup opportunity: UnselectedItemTintColorSetFromBarTextColor and ChangingBarTextColorUpdatesUnselectedItemTintColor overlap on the Red→Blue case. Consider consolidating by keeping the combined test and removing the duplicate initial assertion from ChangingBarTextColorUpdatesUnselectedItemTintColor. 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.

🧪 Test evaluation by Evaluate PR Tests

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 UITabBar properties 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")]
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.

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.

Suggested change
[System.Runtime.Versioning.SupportedOSPlatform("ios26.0")]
[System.Runtime.Versioning.SupportedOSPlatform("ios26.0")]
[System.Runtime.Versioning.SupportedOSPlatform("maccatalyst26.0")]

Copilot uses AI. Check for mistakes.
Comment on lines +253 to +257
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
return resizedImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
}

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.

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.

Suggested change
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
return resizedImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
}

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +193
item.Image = template.ApplyTintColor(unselectedColor)
?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);

if (selectedColor is not null)
{
item.SelectedImage = template.ApplyTintColor(selectedColor)
?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
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.

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.

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +136
// 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;
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.
Comment on lines +20 to +21
// TabbedRenderer IS a UITabBarController — get TabBar directly from it
if (tabbedPage.Handler?.PlatformView is UITabBarController tbc)
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 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.

Suggested change
// 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)

Copilot uses AI. Check for mistakes.
@kubaflo
Copy link
Contributor

kubaflo commented Mar 26, 2026

/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>
@jfversluis jfversluis force-pushed the Fix-iOS-26-bugs-ff88 branch from 86a1043 to 7207ec9 Compare March 26, 2026 21:14
@github-actions
Copy link
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Tests need improvement

Tests provide solid coverage of the main iOS 26+ property-setting paths (UnselectedItemTintColor), but the icon pre-coloring mechanism (ApplyPreColoredImagesForIOS26) — a key part of the fix — has no test coverage, and one test's null-clearing assertion is weak.

👍 / 👎 — Was this evaluation helpful? React to let us know!

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #34688 — Fix iOS 26+ tab bar unselected colors for TabbedPage and Shell
Test files evaluated: 4 (device tests)
Fix files: 3


Overall Verdict

⚠️ Tests need improvement

The property-based device tests do a good job verifying that UnselectedItemTintColor is set correctly on iOS 26+, but the icon pre-coloring code path (ApplyPreColoredImagesForIOS26, including its ConditionalWeakTable cache) is entirely untested, and the null-clearing assertion in one test is too weak to catch regressions.


1. Fix Coverage — ⚠️

The tests directly verify that UnselectedItemTintColor is set on the UITabBar after the fix is applied — which is the main observable side effect. However, the second key mechanism — ApplyPreColoredImagesForIOS26 — bakes color into tab icon images using AlwaysOriginal rendering mode. No test verifies:

  • That tab item images are replaced with pre-colored versions
  • That the ConditionalWeakTable cache (s_originalTemplateImages) is populated and prevents quality degradation from repeated round-trips

These code paths in TabbedViewExtensions.cs are non-trivial (~40 lines) and have no test coverage.

2. Edge Cases & Gaps — ⚠️

Covered:

  • BarTextColor set → UnselectedItemTintColor updated (TabbedPage, iOS 26+)
  • UnselectedTabColor takes priority over BarTextColor (TabbedPage, iOS 26+)
  • Changing BarTextColor updates the tint in real time (TabbedPage, iOS 26+)
  • Shell: TabBarUnselectedColor + TabBarTitleColor together (iOS 26+ property-based; older iOS pixel-based)
  • Pre-iOS 26 behavior preserved (existing tests still run on pre-26, guarded with version checks)
  • BarTextColor = null clears the custom tint (partially — see assertion quality)

Missing:

  • Tab icon pre-coloringApplyPreColoredImagesForIOS26 modifies UITabBarItem.Image on every layout. No test checks that items have pre-colored images after the fix. A device test could inspect item.Image.RenderingMode == UIImageRenderingMode.AlwaysOriginal after setting a color.
  • Image cache correctness — The s_originalTemplateImages ConditionalWeakTable prevents quality degradation from repeated tinting. No test verifies the cache is populated, or that repeatedly calling the method doesn't degrade image quality.
  • Shell: selected tab color — The Shell iOS 26+ branch (ShellTabBarUnselectedAndTitleColorWorkTogether) only validates UnselectedItemTintColor. It doesn't verify that TintColor (selected tab color) was set when TabBarTitleColor is specified.
  • BarTextColor clear → expected value — When BarTextColor is set to null, what should UnselectedItemTintColor be? The current test only asserts it is NOT the previous colors, not what it should actually equal.

3. Test Type Appropriateness — ✅

Current: Device Tests (iOS)
Recommendation: Same. The fix involves native UITabBar, UITabBarItem, and UIImage APIs that require an actual iOS or MacCatalyst runtime context. Device tests are the correct choice here. There is no logic that could be extracted into unit tests.

4. Convention Compliance — ✅

The automated script found 0 convention issues. Tests use [Fact] (xUnit), include OS version guards (OperatingSystem.IsIOSVersionAtLeast(26)), and are placed in the correct device test files. No anti-patterns detected.

5. Flakiness Risk — ✅ Low

Tests use property-based assertions (tabBar.UnselectedItemTintColor) rather than pixel/screenshot comparisons, which is the correct approach for iOS 26+ where pixel verification is unreliable. No Task.Delay, Thread.Sleep, or animation timing issues. No external URLs. Risk is low.

6. Duplicate Coverage — ✅ No duplicates

Existing Shell and TabbedPage tests that used pixel-based color verification now correctly early-return on iOS 26+ with a comment pointing to the new property-based tests. The new tests fill the gap without overlapping.

7. Platform Scope — ⚠️

The fix explicitly handles both iOS and MacCatalyst (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26)), and the test guards reflect both. However:

  • TabbedPageTests.cs adds BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor with Skip = "Fails on Mac Catalyst, fixme" on the MacCatalyst build. This acknowledges a known failure that is unresolved by this PR. It's a pre-existing issue, but it means the MacCatalyst fix path for TabbedPage is not fully validated.
  • Shell tests do cover MacCatalyst through the #if IOS || MACCATALYST block, which is correct.

8. Assertion Quality — ⚠️

Test Assertion Quality
UnselectedItemTintColorSetFromBarTextColor ✅ Specific — checks exact color with tolerance
UnselectedItemTintColorSetFromUnselectedTabColor ✅ Specific — checks exact color with tolerance
ChangingBarTextColorUpdatesUnselectedItemTintColor (null clear) ⚠️ Weak — asserts NOT Red/Green, not what the value SHOULD be. A null BarTextColor should restore a default; the test doesn't assert the expected default value.
ShellTabBarUnselectedAndTitleColorWorkTogether (iOS 26+ path) ⚠️ Incomplete — only checks 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

  1. Add a test for ApplyPreColoredImagesForIOS26 — After setting a color and triggering layout, verify that at least one tab item has its image in UIImageRenderingMode.AlwaysOriginal. This is the most important untested code path. Can be done in TabbedPageTests.iOS.cs within an existing CreateHandlerAndAddToWindow block.

  2. Strengthen the null-clear assertion — In ChangingBarTextColorUpdatesUnselectedItemTintColor, after tabbedPage.BarTextColor = null, assert the expected value (e.g., null or the system default tint), not just that it's not the previous colors.

  3. Add Shell selected-color assertion — In ShellTabBarUnselectedAndTitleColorWorkTogether for the iOS 26+ branch, also verify tabBar.TintColor equals titleColor (the selected tab color), since the fix sets it via tabBar.TintColor = _pendingSelectedTintColor.

  4. (Optional) Resolve the Mac Catalyst TabbedPage skip — The Skip = "Fails on Mac Catalyst, fixme" note on BarTextColorAppliesToUnselectedTabsWithoutExplicitUnselectedColor suggests 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.

🧪 Test evaluation by Evaluate PR Tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] TabbedPage BarTextColor not applied to unselected tabs [iOS26] TabBarUnselectedColor not working on ios

3 participants