Skip to content

[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android - #37137

Open
Dhivya-SF4094 wants to merge 3 commits into
dotnet:net11.0from
Dhivya-SF4094:fix-shellGradientColor
Open

[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android#37137
Dhivya-SF4094 wants to merge 3 commits into
dotnet:net11.0from
Dhivya-SF4094:fix-shellGradientColor

Conversation

@Dhivya-SF4094

@Dhivya-SF4094 Dhivya-SF4094 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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!

Root Cause

  • Shell.Background supports Brush types such as SolidColorBrush, LinearGradientBrush, and RadialGradientBrush. However, the Shell tab-bar appearance trackers relied exclusively on EffectiveTabBarBackgroundColor, which exposes only a single Color value.

  • As a result, any brush assigned through Shell.Background was ignored when rendering the bottom tab bar. Platform-specific implementations therefore fell back to solid-color rendering:

  • On iOS, the shared tab-bar appearance helper accepted only a Color, preventing gradient information from being propagated to the native layer.

  • On Android, the tab bar always created a MaterialShapeDrawable or a color-transition drawable based on a single color value.

Description of Change

The background selection logic was updated to support both colors and brushes using the following precedence order:

  1. Shell.TabBarBackgroundColor
  2. Shell.Background
  3. Shell.BackgroundColor
  4. Platform default value

iOS

  • Updated the shared UpdateiOS15TabBarAppearance implementation to accept a Paint? instance instead of only a Color.
  • Converted Shell brushes into their corresponding Paint representations (SolidPaint, LinearGradientPaint, and RadialGradientPaint).
  • Added native gradient rendering support for the tab-bar background layer.
  • Disabled the default tab-bar blur effect when gradients are applied to preserve the intended appearance.
  • Updated existing TabbedPage callers to convert BarBackgroundColor values by using .AsPaint().

Android

  • Introduced a brush-based SetBackground(BottomNavigationView, Brush) implementation.
  • Routed gradient brushes through the existing GradientStrokeDrawable rendering path.
  • Preserved the existing behavior for solid-color brushes by continuing to use color-transition drawables.
  • Kept the existing SetBackgroundColor(..., Color) method for backward compatibility, marked it as obsolete, and forwarded calls to the new brush-based implementation.
  • These changes allow Shell tab bars to render gradient backgrounds consistently across iOS and Android while preserving existing solid-color behavior and subclass compatibility.

Issues Fixed:

Fixes #37138

Validated the behaviour in the following platforms

  • Android
  • Windows
  • iOS
  • Mac

Screenshots

Before After
iOS
Android

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🚀 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 -- 37137

Or

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

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Aug 6, 2026
@github-actions github-actions Bot added area-controls-shell Shell Navigation, Routes, Tabs, Flyout platform/android platform/ios platform/macos macOS / Mac Catalyst labels Aug 6, 2026
@Dhivya-SF4094 Dhivya-SF4094 changed the title [WIP] [.NET 11] 11Fix shell.Gradient color not working [WIP] [.NET 11] Fix shell.Gradient color not working Aug 6, 2026
@Dhivya-SF4094 Dhivya-SF4094 changed the title [WIP] [.NET 11] Fix shell.Gradient color not working [WIP] [.NET 11] Fix Shell.Gradient color not working Aug 6, 2026
@NirmalKumarYuvaraj NirmalKumarYuvaraj linked an issue Aug 6, 2026 that may be closed by this pull request
@Dhivya-SF4094 Dhivya-SF4094 changed the title [WIP] [.NET 11] Fix Shell.Gradient color not working [.NET 11] Fix Shell.Gradient color not working Aug 10, 2026
@Dhivya-SF4094 Dhivya-SF4094 changed the title [.NET 11] Fix Shell.Gradient color not working [.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android Aug 10, 2026
@Dhivya-SF4094
Dhivya-SF4094 force-pushed the fix-shellGradientColor branch from d8f4466 to 3d62979 Compare August 10, 2026 10:12
@sheiksyedm
sheiksyedm marked this pull request as ready for review August 10, 2026 10:26
Copilot AI lite review requested due to automatic review settings August 10, 2026 10:26
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates Shell/TabbedPage tab-bar appearance handling so that gradient (and other non-solid) backgrounds can be propagated to the native bottom tab bar on iOS and Android, aligning tab bar rendering with Shell.Background brush support.

Changes:

  • iOS: UpdateiOS15TabBarAppearance now accepts a Paint? and applies gradient backgrounds via UpdateBackground(...).
  • Android: Shell bottom nav appearance tracker now supports brush-based backgrounds (including gradients) and retains an obsolete color-based entry point.
  • Tests: The Issue10445 HostApp page is updated to ensure a bottom tab bar is present (so the gradient scenario can be validated via existing screenshot test).

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Core/src/Platform/iOS/TabbedViewExtensions.cs Pass paint/gradient info through iOS 15+ tab bar appearance and apply native background updates.
src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs Select tab bar background using TabBarBackgroundColor → Background brush → BackgroundColor precedence (iOS compatibility path).
src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellBottomNavViewAppearanceTracker.cs Add brush-based tab bar background handling (including gradients) and keep an obsolete color-based hook.
src/Controls/src/Core/Shell/ShellAppearance.cs Adjust tab bar background “effective” color behavior to no longer auto-fallback to BackgroundColor.
src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs Convert TabbedPage bar background color to Paint when calling iOS 15+ appearance helper.
src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs Convert compatibility TabbedPage bar background color to Paint when calling iOS 15+ appearance helper.
src/Controls/tests/TestCases.HostApp/Issues/Issue10445.cs Ensure the test Shell shows a bottom tab bar (multiple tabs) so gradient background can be validated visually.
src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt Record the newly introduced SetBackground(BottomNavigationView, Brush) protected virtual API in PublicAPI.

Comment on lines +55 to +65
if (barBackground is GradientPaint)
{
_tabBarAppearance.BackgroundEffect = null;
_tabBarAppearance.BackgroundColor = UIColor.Clear;
tabBar.BackgroundColor = UIColor.Clear;
tabBar.UpdateBackground(barBackground);
}
else
{
tabBar.RemoveBackgroundLayer();
}
Comment on lines 81 to +82
Color IShellAppearanceElement.EffectiveTabBarBackgroundColor =>
TabBarBackgroundColor ?? BackgroundColor;
TabBarBackgroundColor;
bottomView.ItemIconTintList = _itemIconTint;

SetBackgroundColor(bottomView, backgroundColor);
SetBackground(bottomView, background);
Comment on lines +43 to +44
AddBottomTab(page, "Home");
AddBottomTab(new ContentPage
bottomView.ItemIconTintList = GetDefaultTabColorList(_shellContext.AndroidContext);
bottomView.ItemTextColor = GetDefaultTabColorList(_shellContext.AndroidContext);
SetBackgroundColor(bottomView, null);
SetBackground(bottomView, null);
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 10, 2026
rmarinho added a commit that referenced this pull request Aug 10, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 11 findings

See inline comments for details.

tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null,
tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor.AsPaint() : null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Logic and CorrectnessColor.AsPaint() is an instance method on Color (src/Graphics/src/Graphics/Color.cs:166), not a null-tolerant extension method, so tabbed.BarBackgroundColor.AsPaint() throws NullReferenceException whenever BarBackgroundColor is null while IsSet(BarBackgroundColorProperty) is true.

IsSet is true for any explicit assignment, including an explicit null. Concrete repro: tabbed.BarBackgroundColor = Colors.Red; tabbed.BarBackgroundColor = null; (or BarBackgroundColor="{Binding BarColor}" / {AppThemeBinding Light=..., Dark={x:Null}} resolving to null) → crash on the next appearance update. Before this change the same input produced barBackgroundColor == null and fell back to defaultBarColor.

Suggest tabbed.BarBackgroundColor?.AsPaint() (and let the existing null => defaultBarColor switch arm handle it).

IsSet(SelectedTabColorProperty) ? SelectedTabColor : null,
IsSet(UnselectedTabColorProperty) ? UnselectedTabColor : null,
IsSet(BarBackgroundColorProperty) ? BarBackgroundColor : null,
IsSet(BarBackgroundColorProperty) ? BarBackgroundColor.AsPaint() : null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Logic and Correctness — Same NullReferenceException as TabbedRenderer.cs:645. Color.AsPaint() is an instance method (src/Graphics/src/Graphics/Color.cs:166); IsSet(BarBackgroundColorProperty) is true for an explicitly-assigned null, so BarBackgroundColor.AsPaint() will NRE for BarBackgroundColor = null after a previous non-null assignment, or for a binding/AppThemeBinding that resolves to null.

Use BarBackgroundColor?.AsPaint().


Color IShellAppearanceElement.EffectiveTabBarBackgroundColor =>
TabBarBackgroundColor ?? BackgroundColor;
TabBarBackgroundColor;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Cross-Platform Behavioral Consistency / Regression Prevention — Dropping the ?? BackgroundColor fallback changes the semantics of the IShellAppearanceElement.EffectiveTabBarBackgroundColor interface contract for every consumer, but only two of the five call sites were compensated in this PR.

Uncompensated consumers that lose the Shell.BackgroundColor → tab bar fallback:

  • src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs:763UpdateTopNavAreaBackground(null); Windows top-nav area reverts to the platform default when only Shell.BackgroundColor is set.
  • src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Tizen.cs:106.
  • src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs:149 — now always writes DefaultBottomNavigationViewBackgroundColor into the existing ColorDrawable.
  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs:114 (UpdateTabBarAppearance, the pre-iOS-15 path) still reads EffectiveTabBarBackgroundColor directly and was not updated alongside the iOS-15 path at line 74.

Repro: a Shell with only Shell.BackgroundColor="Red" (no TabBarBackgroundColor, no Shell.Background) → red tab bar before, default tab bar after, on Windows/Tizen.

Prefer leaving EffectiveTabBarBackgroundColor untouched and adding a new EffectiveTabBarBackground (Brush) member that encodes the full precedence, so no existing consumer changes behavior.

{
IShellAppearanceElement controller = appearance;
var backgroundColor = controller.EffectiveTabBarBackgroundColor;
var shellAppearance = appearance as ShellAppearance;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Architectural Layer Placement — The precedence chain (TabBarBackgroundColorShell.BackgroundShell.BackgroundColor → default) is reconstructed by hand here and byte-for-byte duplicated in SafeShellTabBarAppearanceTracker.cs:74-80. Because it was removed from ShellAppearance (the shared layer) and re-added per platform, Windows/Tizen/ShellItemRenderer silently diverge (see the ShellAppearance.cs:82 comment). This resolution belongs on ShellAppearance as an internal EffectiveTabBarBackground brush so all platforms share one definition.

Also, appearance as ShellAppearance narrows an IShellAppearanceElement parameter to a concrete type: any other IShellAppearanceElement implementation now yields shellAppearance == null, so both the Background and BackgroundColor fallbacks silently disappear for it (previously the interface member supplied the BackgroundColor fallback for everyone).

{
if (brush is GradientBrush)
{
bottomView.UpdateBackground(brush);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Performance-Critical Path / Native Platform Defaults Preservation — The gradient branch returns immediately, so unlike the solid path it has no "same value → early return" short-circuit. SetAppearance runs on every Shell appearance change (including tab/section switches), so a static gradient re-runs UpdateBackground and rebuilds the drawable on every tab switch. Cache the last applied brush and skip when unchanged, mirroring the lastColor == newColor guards below.

It also bypasses the MaterialShapeDrawable + InitializeElevationOverlay(bottomView.Context) path used for solid colors, so a gradient tab bar loses the Material elevation overlay in dark theme. If that is intentional, a short comment would prevent a future "fix" from re-adding it.


if (barBackground is GradientPaint)
{
_tabBarAppearance.BackgroundEffect = null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Native Platform Defaults Preservation — The gradient branch mutates _tabBarAppearance.BackgroundEffect = null, _tabBarAppearance.BackgroundColor = UIColor.Clear and tabBar.BackgroundColor = UIColor.Clear, but none of these is ever restored. ConfigureWithDefaultBackground() only runs once, when _tabBarAppearance is first allocated (line 42), and the else branch (line 64) only calls RemoveBackgroundLayer().

Repro: set a gradient Shell.Background / BarBackground, then clear it (or navigate to a page whose appearance has no background). barBackground becomes nulleffectiveBarColor = defaultBarColor; if defaultBarColor is null (the Shell tracker passes null for defaultBarColorSafeShellTabBarAppearanceTracker.cs:87), the if (effectiveBarColor != null) block is skipped and the tab bar is left permanently transparent with the default blur effect destroyed.

The else branch should also restore the default background (_tabBarAppearance.ConfigureWithDefaultBackground() or a captured default) alongside RemoveBackgroundLayer().

}

var effectiveBarColor = (barBackgroundColor == null) ? defaultBarColor : barBackgroundColor.ToPlatform();
var effectiveBarColor = barBackground switch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Logic and Correctness — The switch loses the previous "no explicit background → use the platform default" behavior for two inputs:

  • SolidPaint with a null ColorsolidPaint.Color?.ToPlatform() yields null, and defaultBarColor is never applied. Previously barBackgroundColor == null mapped straight to defaultBarColor. This is reachable today via BarBackgroundColor.AsPaint() at TabbedRenderer.cs:645 / TabbedPage.iOS.cs:849 (see the NRE comments there — once null-guarded, this arm becomes the live path).
  • Any non-solid, non-gradient Paint (e.g. ImagePaint) → _ => null, and neither the gradient branch nor the color branch runs, so the tab bar keeps whatever appearance it had.

Consider SolidPaint { Color: null } => defaultBarColor and an explicit fallback for unsupported paint types.

};

AddContentPage(page, "Home");
AddBottomTab(page, "Home");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Regression Prevention and Test Coverage — This host page is shared by every UI-test leg, and adding a second bottom tab changes the rendering on all of them, but only 2 of the 5 committed baselines for ShellBackgroundSupportsGradientBrush were regenerated:

Updated: TestCases.iOS.Tests/snapshots/ios/, TestCases.iOS.Tests/snapshots/ios-26/.
Not updated (will fail): TestCases.Android.Tests/snapshots/android/, TestCases.Mac.Tests/snapshots/mac/, TestCases.WinUI.Tests/snapshots/windows/.

The try-fix run already observed an 8.58% Android screenshot delta from exactly this host-page change (try-fix/content.md, candidate 1). Regenerate the Android, Mac, and Windows baselines, or limit the new host layout to platforms with updated baselines.


AddContentPage(page, "Home");
AddBottomTab(page, "Home");
AddBottomTab(new ContentPage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Regression Prevention and Test Coverage — Gate reported no new tests, and the only coverage added is a visual tweak to an existing screenshot test. The behaviors this PR actually changes are untested:

  1. Precedence: Shell.TabBarBackgroundColor must still win over a gradient Shell.Background (the new nested conditional in ShellBottomNavViewAppearanceTracker.cs:56-61 and SafeShellTabBarAppearanceTracker.cs:74-80).
  2. The removed EffectiveTabBarBackgroundColorBackgroundColor fallback: a Shell with only Shell.BackgroundColor set must still tint the tab bar (a device/unit test here would have caught the Windows/Tizen/ShellItemRenderer regression).
  3. Gradient → solid/cleared transition on both Android and iOS (see the TabbedViewExtensions.cs:57 comment).

A ShellAppearance-level unit test in Controls.Core.UnitTests covering the precedence matrix would be cheap and would pin the contract that the platform trackers now duplicate.

virtual Microsoft.Maui.Controls.Handlers.ShellItemHandler.OnSectionChanged(Microsoft.Maui.Controls.ShellSection! shellSection, bool animate) -> void
virtual Microsoft.Maui.Controls.Handlers.ShellItemHandler.OnTabReselected(Microsoft.Maui.Controls.ShellSection! shellSection) -> void
virtual Microsoft.Maui.Controls.Handlers.ShellSectionHandler.OnCreateNavigationAnimation(Android.Content.Context! context, bool isPopping, bool enter) -> Android.Views.Animations.Animation?
~virtual Microsoft.Maui.Controls.Platform.Compatibility.ShellBottomNavViewAppearanceTracker.SetBackground(Google.Android.Material.BottomNavigation.BottomNavigationView bottomView, Microsoft.Maui.Controls.Brush brush) -> void

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Public API Surface Design — This adds a permanent protected virtual API to a public unsealed compatibility type on a servicing branch (net11.0) in order to fix a rendering bug, while simultaneously obsoleting its shipped predecessor (PublicAPI.Shipped.txt:4369). Protected virtuals on ShellBottomNavViewAppearanceTracker cannot be removed later and become part of the compatibility-renderer contract.

If the gradient handling can be done inside the existing SetBackgroundColor dispatch path (see the ShellBottomNavViewAppearanceTracker.cs:148 comment), this API addition — and the obsoletion warning it forces on every existing subclass — can be avoided entirely.

MauiBot

This comment was marked as outdated.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) and removed s/agent-review-in-progress AI review is currently running for this PR labels Aug 10, 2026
@kubaflo

This comment has been minimized.

@kubaflo

kubaflo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 10 findings

See inline comments for details.

IsSet(SelectedTabColorProperty) ? SelectedTabColor : null,
IsSet(UnselectedTabColorProperty) ? UnselectedTabColor : null,
IsSet(BarBackgroundColorProperty) ? BarBackgroundColor : null,
IsSet(BarBackgroundColorProperty) ? BarBackgroundColor.AsPaint() : null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Logic and CorrectnessColor.AsPaint() is an instance method on Microsoft.Maui.Graphics.Color (Color.cs:166), not a null-safe extension. IsSet(BarBackgroundColorProperty) is true whenever the property has been assigned, including an explicit null (tabbedPage.BarBackgroundColor = null, or a binding that resolves to null after previously resolving to a color). In that case BarBackgroundColor.AsPaint() throws NullReferenceException and the tab bar update crashes. Before this change the same expression passed null through safely and UpdateiOS15TabBarAppearance fell back to defaultBarColor, restoring the platform default. Use BarBackgroundColor?.AsPaint() (and see the related stale-color issue in TabbedViewExtensions.cs:48).

tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null,
tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null,
tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor.AsPaint() : null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Logic and Correctness — Same NRE as TabbedPage.iOS.cs:849: Color.AsPaint() is an instance method, so tabbed.BarBackgroundColor.AsPaint() throws when BarBackgroundColorProperty is set to an explicit null (clearing the bar color, or a binding resolving to null). The previous code passed null and correctly restored _defaultBarColor. Use ?.AsPaint().


Color IShellAppearanceElement.EffectiveTabBarBackgroundColor =>
TabBarBackgroundColor ?? BackgroundColor;
TabBarBackgroundColor;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Regression Prevention / Cross-Platform Consistency — Dropping the ?? BackgroundColor fallback here changes behavior for every consumer of IShellAppearanceElement.EffectiveTabBarBackgroundColor, but the new TabBarBackgroundColor > Background > BackgroundColor precedence chain was only re-implemented in two of them (Android ShellBottomNavViewAppearanceTracker.SetAppearance and the iOS 15+ path of SafeShellTabBarAppearanceTracker). The remaining consumers now silently lose the Shell.BackgroundColor fallback with nothing replacing it:

  • Handlers/Shell/ShellItemHandler.Windows.cs:763Shell.BackgroundColor no longer tints the top nav/tab area on Windows.
  • Handlers/Shell/ShellItemHandler.Tizen.cs:106.
  • Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs:114 (UpdateTabBarAppearance, the pre-iOS15 path) and :30.
  • Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs:149, which now paints the bottom view with DefaultBottomNavigationViewBackgroundColor when only Shell.BackgroundColor is set.
    Prefer exposing the resolved value on ShellAppearance itself (e.g. an internal EffectiveTabBarBackground brush implementing the full precedence) so all platforms share one implementation, instead of duplicating the ternary chain per-platform and regressing the untouched platforms.

IShellAppearanceElement appearanceElement = appearance;

var backgroundColor = appearanceElement.EffectiveTabBarBackgroundColor;
var background = appearanceElement.EffectiveTabBarBackgroundColor is not null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Architectural Layer Placement — This 4-level precedence ternary is a verbatim copy of the one added in ShellBottomNavViewAppearanceTracker.SetAppearance (Android, line 56). Because the precedence now lives in the platform trackers rather than in ShellAppearance, the pre-iOS15 branch below (UpdateTabBarAppearance, line 114) and the Windows/Tizen handlers keep using the raw EffectiveTabBarBackgroundColor and get a different answer for the same Shell state. Resolve the precedence once in ShellAppearance (see comment on ShellAppearance.cs) and have every tracker consume it.

}

var effectiveBarColor = (barBackgroundColor == null) ? defaultBarColor : barBackgroundColor.ToPlatform();
var effectiveBarColor = barBackground switch

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Logic and Correctness — The _ => null arm (and SolidPaint solidPaint => solidPaint.Color?.ToPlatform() when Color is null) makes effectiveBarColor null, which the if (effectiveBarColor != null) guard at line 68 treats as "do nothing" — not as "restore defaultBarColor". Two reachable regressions:

  1. Shell.Background = new ImageBrush{...} converts to ImageSourcePaint, hits _ => null, so no background is applied at all AND _tabBarAppearance.BackgroundColor keeps the previously applied color (the appearance object is cached in the ref field across calls). Android's UpdateBackground does render image paints, so the platforms diverge.
  2. Clearing a previously-set bar color (SolidColorBrush with a null Color, or BarBackgroundColor = null while IsSet is true) leaves the stale color on the tab bar instead of reverting to the platform default.
    Make the fallback explicit: SolidPaint sp => sp.Color?.ToPlatform() ?? defaultBarColor, and _ => defaultBarColor for non-gradient/unsupported paints (or handle them via tabBar.UpdateBackground).

_ => null,
};

if (barBackground is GradientPaint)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Handler Mapper and Property Patterns / Cross-Platform Consistency — The gradient branch mutates _tabBarAppearance.BackgroundEffect = null and BackgroundColor = UIColor.Clear, but nothing restores those when the app later transitions gradient → solid/default. _tabBarAppearance is a cached ref field reused on the next call: taking the else branch removes the gradient layer, and if effectiveBarColor is null (default) the appearance is left with BackgroundEffect == null + BackgroundColor == UIColor.Clear, i.e. a transparent tab bar with no blur instead of the restored system default. Re-run ConfigureWithDefaultBackground() (or re-apply the captured defaultBarColor/effect) in the non-gradient branch. Note RemoveBackgroundLayer() is also called unconditionally on every appearance update, including the common no-gradient case.

}
}

[Obsolete("Use SetBackground(BottomNavigationView, Brush) instead.")]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Backward CompatibilitySetBackgroundColor is a shipped protected virtual extension point, and SetAppearance/ResetAppearance no longer call it. Any existing subclass that overrides SetBackgroundColor to customize the bottom bar background silently stops being invoked after this change — the override compiles, produces only an obsoletion warning, and the customization vanishes at runtime. Consider having SetBackground route the non-gradient case through SetBackgroundColor (e.g. #pragma-suppressed call to the obsolete member) for one release so overrides keep working, or call this out explicitly as a breaking change in the PR description/release notes.

return;
}

var color = (brush as SolidColorBrush)?.Color;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Logic and Correctness / Memory — Gradient → solid transition is not handled cleanly. After the gradient branch has installed a MauiDrawable via bottomView.UpdateBackground(brush), a subsequent solid/reset call reaches here where oldBackground as ColorDrawable and as ColorChangeRevealDrawable are both null, so lastColor falls back to Colors.Transparent and the ColorChangeRevealDrawable animates from transparent — a visible flash rather than a gradient→color transition. The previous MauiDrawable is also replaced by ViewCompat.SetBackground without being disposed (contrast with ViewExtensions.UpdateBackground, which disposes the outgoing MauiDrawable). Detect the gradient-drawable case explicitly and dispose/short-circuit the reveal animation.

};

AddContentPage(page, "Home");
AddBottomTab(page, "Home");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Regression Prevention — Switching AddContentPage to AddBottomTab (plus the new "Settings" tab) changes the rendered UI of Issue10445 on every platform, but only the iOS baselines (TestCases.iOS.Tests/snapshots/ios and ios-26) were regenerated. The existing baselines TestCases.Android.Tests/snapshots/android/ShellBackgroundSupportsGradientBrush.png, TestCases.WinUI.Tests/snapshots/windows/... and TestCases.Mac.Tests/snapshots/mac/... still show the old single-page layout, so ShellBackgroundSupportsGradientBrush will fail on Android/Windows/MacCatalyst. Regenerate all baselines — the Android one in particular is the only coverage for the ShellBottomNavViewAppearanceTracker half of this fix.


AddContentPage(page, "Home");
AddBottomTab(page, "Home");
AddBottomTab(new ContentPage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Regression Prevention — Coverage is limited to a single static screenshot of the "gradient wins" state. None of the transitions this PR actually changes are exercised: (a) TabBarBackgroundColor set → gradient must be ignored (precedence), (b) gradient → solid/default (ResetAppearance, iOS else branch at TabbedViewExtensions.cs:64), (c) TabbedPage.BarBackgroundColor set then cleared to null — which currently NREs on iOS (see TabbedPage.iOS.cs:849), and (d) tab re-selection/re-navigation re-running SetAppearance. At minimum add a case that toggles the background back to solid/null after the gradient, since that is where the stale-appearance and null-color bugs live.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@Dhivya-SF4094 — new AI review results are available based on commit 3d62979.

Gate No Tests Confidence Low Platform iOS


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ SKIPPED

No tests were detected in this PR.

Recommendation: Add tests to verify the fix using the write-tests-agent.


📋 Pre-Flight — Context & Validation

PR #37137 Pre-Flight

Context

  • PR: [.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android
  • Base/head: net11.0 / fix-shellGradientColor
  • Submitted head: 3d629795850264e3dc3e26c9a2aa14f3ddec8804
  • Local review commit: 053aeb87356f9cf63cb71a9b7ba344229093c34d
  • Issue: #37138, where a gradient assigned to Shell.Background renders white on iOS and gray on Android instead of appearing on the Shell navigation and bottom-tab backgrounds.
  • Requested candidate platform: iOS.
  • Gate: skipped because no new test was detected. Do not rerun the gate or create/overwrite gate/content.md.

Current PR Approach

The materialized PR changes 10 files (+68/-12). Its iOS path:

  1. Changes ShellAppearance.EffectiveTabBarBackgroundColor to return only TabBarBackgroundColor.
  2. Reconstructs the precedence TabBarBackgroundColorShell.BackgroundShell.BackgroundColor in SafeShellTabBarAppearanceTracker.
  3. Changes UpdateiOS15TabBarAppearance from a Color? background parameter to Paint?, renders GradientPaint through UITabBar.UpdateBackground, and clears the native background effect for gradients.
  4. Converts the two TabbedPage color call sites to Paint.
  5. Changes the existing Issue10445 host page to contain two bottom tabs and updates its iOS screenshot baselines.

Relevant paths:

  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
  • src/Core/src/Platform/iOS/TabbedViewExtensions.cs
  • src/Controls/src/Core/Shell/ShellAppearance.cs
  • src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs
  • src/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cs
  • src/Controls/tests/TestCases.HostApp/Issues/Issue10445.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue10445.cs

Constraints and Known Risks

  • Preserve the shared EffectiveTabBarBackgroundColor fallback for consumers on platforms not changed by an iOS candidate.
  • Preserve precedence: explicit Shell.TabBarBackgroundColor wins over Shell.Background, which wins over Shell.BackgroundColor.
  • A gradient-to-solid/default transition must restore native tab-bar appearance state, including the default blur/background effect.
  • BarBackgroundColor may be explicitly set to null; conversion to Paint must remain null-safe.
  • The existing screenshot test proves static gradient rendering but does not exercise background transitions.
  • Do not touch or revert the unrelated pre-existing .github/** and eng/** working-tree changes.

Bounded Validation

Primary test:

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "ShellBackgroundSupportsGradientBrush"

No additional mandatory regression-test commands were supplied. Do not run a full suite or invent broader validation. Each candidate gets one implementation/test pass and at most one focused correction/retest.

Applicable Repository Rules

Read and follow:

  • .github/instructions/threading-async.instructions.md
  • .github/instructions/public-api.instructions.md when changing protected/public API
  • .github/instructions/uitests.instructions.md if changing test files

Each attempt must invoke the try-fix skill exactly once, perform its expert self-review inline without spawning another agent, persist the skill artifacts, restore with EstablishBrokenBaseline.ps1 -Restore, and write its narrative to the requested try-fix-N/content.md.


🔬 Code Review — Deep Analysis

Expert PR Evaluation — PR #37137

Verdict: NEEDS_CHANGES

Confidence: low

Independent assessment

The submitted fix propagates Shell.Background brushes into the Android and iOS bottom-tab renderers, adds gradient rendering on iOS, and changes the existing Issue10445 host page plus iOS screenshot baselines. The basic static-gradient path is plausible, but the implementation changes shared fallback semantics and cached native appearance state in ways that regress existing scenarios.

Blocking findings

  1. TabbedPage.iOS.cs:849 and compatibility TabbedRenderer.cs:645 call the instance method Color.AsPaint() on a possibly explicit-null BarBackgroundColor, causing a NullReferenceException instead of restoring the default.
  2. ShellAppearance.EffectiveTabBarBackgroundColor drops its BackgroundColor fallback globally, while only two platform paths reconstruct the new precedence. Windows, Tizen, legacy Android, and pre-iOS 15 consumers can lose their existing tab-bar background.
  3. The iOS appearance helper does not fully reset its cached UITabBarAppearance when moving from a gradient to a solid/default background. Unsupported or null-valued paints can also leave stale native background state.
  4. Android no longer routes solid backgrounds through the shipped protected virtual SetBackgroundColor, silently bypassing subclass overrides. Replacing a gradient drawable with a solid transition can also animate from transparent and leave the outgoing MauiDrawable undisposed.
  5. The host page now renders a bottom-tab layout on every platform, but only iOS screenshot baselines were updated. The existing Android, Windows, and MacCatalyst snapshots no longer match, and no test exercises precedence or gradient-to-solid/default transitions.

Blast radius and failure-mode probing

  • Runs for all instances: Yes. Shared Shell fallback semantics and the common iOS tab appearance helper affect non-gradient Shell and TabbedPage instances.
  • Startup impact: No new static initialization, but appearance application occurs during page/tab creation.
  • Static/shared state: No new static state; cached native appearance and drawable state persist across repeated updates.
  • Explicit null: Crashes both iOS TabbedPage call paths.
  • Gradient → solid/default: Can retain a transparent/no-blur iOS appearance and produces an incorrect Android reveal transition.
  • Reconnect/reset: The submitted iOS reset path does not remove all MAUI background state.
  • Adjacent platforms: Shared fallback removal changes platforms outside the submitted Android/iOS rendering work.

Coverage and CI

The trusted Gate was skipped because it detected no new test. The submitted screenshot fixture covers only a static gradient state and has no matching updated baselines for all affected platforms. Per the review confidence rules, relevant coverage is undetermined and confidence is capped at low.

The raw inline findings are recorded in inline-findings.json.


🛠️ Try-Fix — Analysis & Comparison

PR #37137 — Try-Fix Final Aggregate

Candidates generated: exactly 2 (final allowed candidate completed)
Platform: iOS

Comparison

Candidate Root strategy Files Validation Result Findings Artifacts
1 Tracker-local brush resolution plus UIImage rasterization through BackgroundImage, then pattern color 1 First 8.63% screenshot delta; corrected 1.51% delta ❌ Fail 3 try-fix/attempt-1/, try-fix-1/content.md
2 Centralized ShellAppearance effective-brush semantic plus idempotent native Paint application/reset 3 Exact supplied test passed first run (1/1) ✅ Pass 0 try-fix/attempt-2/, try-fix-2/content.md

Conclusion: Candidate 2 is the only passing candidate. It preserves the existing effective-color and TabbedPage contracts, avoids candidate 1's rasterization family, and addresses repeated native-state transitions by resetting appearance state before each application.


PR #37137 — Try-Fix Candidate 1

Attempt directory: CustomAgentLogsTmp/PRState/37137/PRAgent/try-fix/attempt-1/
Platform: iOS
Result:Fail
Self-review findings: 3 (0 critical, 1 major, 1 moderate, 1 minor)

Approach

Root-cause hypothesis (different from the submitted PR): the gradient is lost because nothing in the iOS Shell tab-bar tracker ever looks at ShellAppearance.Background. SafeShellTabBarAppearanceTracker owns the UITabBar and its UITabBarAppearance, so brush resolution and brush rendering both belong there. The shared color-based UpdateiOS15TabBarAppearance contract and the cross-platform EffectiveTabBarBackgroundColor semantics are not the defect and were left untouched.

Implementation, confined to a single file (SafeShellTabBarAppearanceTracker.cs):

  1. Precedence resolved locally in ResolveTabBarBackground: TabBarBackgroundColor > Shell.Background (SolidColorBrush → color, GradientBrush → gradient) > IShellAppearanceElement.EffectiveTabBarBackgroundColor (unchanged TabBarBackgroundColor ?? BackgroundColor) > platform default.
  2. Gradients rasterised once via Paint.ToCALayer(...) + UIGraphicsImageRenderer, then handed to UIKit through the appearance system's own background slots (initially BackgroundImage; after the correction, UIColor.FromPatternImage on BackgroundColor plus tabBar.BackgroundColor for iOS 26). No CAGradientLayer is injected into the tab bar's layer tree.
  3. Transitions handled by rebuilding _tabBarAppearance whenever the gradient/non-gradient mode flips — the shared helper then re-runs ConfigureWithDefaultBackground(), restoring the default blur without manual un-mutation. tabBar.BackgroundColor is cleared on the way out for iOS 26.
  4. ResetAppearance drops the gradient and installs a fresh default appearance (the submitted PR leaves a gradient stuck on the tab bar after reset).
  5. UpdateLayout re-rasterises when the tab-bar bounds change (rotation / size class), and covers the case where bounds are not yet valid at SetAppearance time.

How this differs from the submitted PR

Submitted PR Candidate 1
Changes ShellAppearance.EffectiveTabBarBackgroundColor to drop the BackgroundColor fallback (cross-platform blast radius) ShellAppearance.cs untouched — the shared fallback is preserved for every other platform consumer
Widens shared UpdateiOS15TabBarAppearance from Color? to Paint? and converts two TabbedPage call sites via BarBackgroundColor.AsPaint() TabbedViewExtensions.cs, TabbedPage.iOS.cs, TabbedRenderer.cs untouched — the null-BarBackgroundColor path is safe by construction
Renders with an injected CAGradientLayer, manually clearing BackgroundEffect/BackgroundColor Rasterises the brush and feeds it through the appearance system; undoes state by rebuilding the appearance object
No handling for gradient removal or bounds changes ResetAppearance clears the gradient; UpdateLayout re-renders on size change

Files changed

  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs (+~120/−4) — only file changed

Test command and result

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "ShellBackgroundSupportsGradientBrush"
Pass Rendering mechanism Screenshot delta Outcome
1 UITabBarAppearance.BackgroundImage + BackgroundImageContentMode 8.63% Fail — gradient did not render at all; stock iOS 26 translucent tab bar
2 (one focused correction) UIColor.FromPatternImage on UITabBarAppearance.BackgroundColor + tabBar.BackgroundColor (iOS 26) 1.51% Fail — gradient renders correctly but is not pixel-aligned with the baseline

Both runs built cleanly (no compile errors) and executed the test on the simulator.

Failed ShellBackgroundSupportsGradientBrush [4 s]
  VisualTestUtils.VisualTestFailedException :
  Snapshot different than baseline: ShellBackgroundSupportsGradientBrush.png (1.51% difference)
Test Run Failed. Total tests: 1  Failed: 1

Failure analysis

  1. iOS 26 ignores UITabBarAppearance.BackgroundImage for the redesigned floating tab bar. Only BackgroundColor — on the appearance and on the UITabBar itself, which is exactly the case the existing shared helper already special-cases with if (OperatingSystem.IsIOSVersionAtLeast(26)) — reaches the rendered surface. This invalidated pass 1 outright.
  2. Pattern colors do not stretch. UIColor.FromPatternImage tiles the image from the fill origin instead of scaling it into the filled rect. Because the iOS 26 tab bar background is composited into an inset surface that differs from tabBar.Bounds, the gradient lands slightly offset relative to the baseline, which the submitted PR produced with a stretched CAGradientLayer. The residual 1.51% is concentrated in the tab-bar band; the yellow→green ramp itself is correct.

Verdict on the approach: the architectural half is sound and strictly less invasive than the PR — brush precedence and brush→native translation can live entirely inside the Shell tracker with zero changes to the shared color API or EffectiveTabBarBackgroundColor. The rendering half is what fails: rasterise-to-UIImage is the wrong primitive for a surface whose size and origin UIKit owns. A follow-up in the same shape would keep the tracker-local resolution and swap the pattern color for UIImage.CreateResizableImage or a stretched layer.

Inline expert self-review — 3 findings

Performed inline against .github/agents/maui-expert-reviewer.md (Overarching Principles; always-active Logic and Correctness, Regression Prevention, Complexity Reduction; plus Memory/Lifecycle and Backward Compatibility for Compatibility/). Refreshed after the test-loop correction so it reflects the final diff.

  1. [major] Logic and Correctness (iOS rendering)UIColor.FromPatternImage tiles from the drawing origin, not the tab bar bounds origin; offset/clipping rather than stretching. This finding independently predicted the exact observed failure mode.
  2. [moderate] Regression PreventiontabBar.BackgroundColor = null on the gradient→solid transition restores the UIKit default but does not restore a value set through another path; _defaultBarTint has a capture, BackgroundColor does not.
  3. [minor] Memory and Lifecycle_tabBarBackgroundBrush holds a strong Brush (Element) reference not cleared in Dispose(bool).

Full saved diff

diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
index e0ecca140f..2a5a255859 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
@@ -1,6 +1,8 @@
 #nullable disable
 using System;
 using System.ComponentModel;
+using CoreGraphics;
+using Microsoft.Maui.Graphics;
 using ObjCRuntime;
 using UIKit;
 
@@ -13,8 +15,30 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 		UIColor _defaultTint;
 		UIColor _defaultUnselectedTint;
 		UITabBarAppearance _tabBarAppearance;
+		Brush _tabBarBackgroundBrush;
+		CGSize _tabBarBackgroundImageSize;
+
 		public virtual void ResetAppearance(UITabBarController controller)
 		{
+			if (_tabBarBackgroundBrush is not null)
+			{
+				_tabBarBackgroundBrush = null;
+				_tabBarBackgroundImageSize = CGSize.Empty;
+				_tabBarAppearance = null;
+
+				var gradientTabBar = controller?.TabBar;
+				if (gradientTabBar is not null &&
+					(OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15)))
+				{
+					if (OperatingSystem.IsIOSVersionAtLeast(26))
+						gradientTabBar.BackgroundColor = null;
+
+					var defaultAppearance = new UITabBarAppearance();
+					defaultAppearance.ConfigureWithDefaultBackground();
+					gradientTabBar.StandardAppearance = gradientTabBar.ScrollEdgeAppearance = defaultAppearance;
+				}
+			}
+
 			if (_defaultTint == null)
 				return;
 
@@ -50,6 +74,21 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 
 		public virtual void UpdateLayout(UITabBarController controller)
 		{
+			if (_tabBarBackgroundBrush is null || _tabBarAppearance is null)
+				return;
+
+			if (!OperatingSystem.IsIOSVersionAtLeast(15) && !OperatingSystem.IsTvOSVersionAtLeast(15))
+				return;
+
+			var tabBar = controller?.TabBar;
+			if (tabBar is null)
+				return;
+
+			var size = tabBar.Bounds.Size;
+			if (size.Width == _tabBarBackgroundImageSize.Width && size.Height == _tabBarBackgroundImageSize.Height)
+				return;
+
+			ApplyGradientBackground(tabBar, _tabBarBackgroundBrush);
 		}
 
 		#region IDisposable Support
@@ -71,12 +110,27 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 		{
 			IShellAppearanceElement appearanceElement = appearance;
 
-			var backgroundColor = appearanceElement.EffectiveTabBarBackgroundColor;
+			var backgroundColor = ResolveTabBarBackground(appearance, out var gradientBrush);
 			var foregroundColor = appearanceElement.EffectiveTabBarForegroundColor;
 			var unselectedColor = appearanceElement.EffectiveTabBarUnselectedColor;
 			var titleColor = appearanceElement.EffectiveTabBarTitleColor;
 			var disabledColor = appearanceElement.EffectiveTabBarDisabledColor;
 
+			// Flipping between a gradient and a non-gradient background must not leave the
+			// previous mode's state (cleared blur effect, stale background image) behind, so
+			// the appearance is rebuilt from UIKit defaults whenever the mode changes.
+			if ((_tabBarBackgroundBrush is not null) != (gradientBrush is not null))
+			{
+				_tabBarAppearance = null;
+
+				if (gradientBrush is null && OperatingSystem.IsIOSVersionAtLeast(26))
+					controller.TabBar.BackgroundColor = null;
+			}
+
+			_tabBarBackgroundBrush = gradientBrush;
+			if (gradientBrush is null)
+				_tabBarBackgroundImageSize = CGSize.Empty;
+
 			controller.TabBar
 				.UpdateiOS15TabBarAppearance(
 					ref _tabBarAppearance,
@@ -88,6 +142,9 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 					titleColor ?? foregroundColor,
 					unselectedColor);
 
+			if (gradientBrush is not null)
+				ApplyGradientBackground(controller.TabBar, gradientBrush);
+
 			// Set disabled color in the global appearance for text-only tabs
 			if (disabledColor is not null && _tabBarAppearance is not null)
 			{
@@ -102,6 +159,81 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 			}
 		}
 
+		/// <summary>
+		/// Resolves the tab bar background honoring the documented precedence:
+		/// <c>Shell.TabBarBackgroundColor</c> &gt; <c>Shell.Background</c> &gt; <c>Shell.BackgroundColor</c> &gt; platform default.
+		/// Returns the solid color to hand to the shared appearance helper, or <see langword="null"/> when the
+		/// resolved background is a gradient (reported through <paramref name="gradientBrush"/>) or unset.
+		/// </summary>
+		static Color ResolveTabBarBackground(ShellAppearance appearance, out Brush gradientBrush)
+		{
+			gradientBrush = null;
+
+			if (appearance.TabBarBackgroundColor is not null)
+				return appearance.TabBarBackgroundColor;
+
+			var background = appearance.Background;
+			if (!Brush.IsNullOrEmpty(background))
+			{
+				if (background is SolidColorBrush solidColorBrush)
+					return solidColorBrush.Color;
+
+				if (background is GradientBrush)
+				{
+					gradientBrush = background;
+					return null;
+				}
+			}
+
+			return ((IShellAppearanceElement)appearance).EffectiveTabBarBackgroundColor;
+		}
+
+		[System.Runtime.Versioning.SupportedOSPlatform("ios15.0")]
+		[System.Runtime.Versioning.SupportedOSPlatform("tvos15.0")]
+		void ApplyGradientBackground(UITabBar tabBar, Brush brush)
+		{
+			if (_tabBarAppearance is null)
+				return;
+
+			var size = tabBar.Bounds.Size;
+
+			// Bounds are not usable yet; UpdateLayout re-applies once the tab bar has been laid out.
+			if (size.Width <= 0 || size.Height <= 0)
+				return;
+
+			var image = CreateGradientImage(brush, size);
+			if (image is null)
+				return;
+
+			// The gradient is handed to UIKit as a pattern color so it travels through the exact
+			// same native slots that already work for a solid tab bar background, including the
+			// iOS 26 tab bar which ignores UITabBarAppearance.BackgroundImage.
+			var patternColor = UIColor.FromPatternImage(image);
+
+			_tabBarAppearance.BackgroundEffect = null;
+			_tabBarAppearance.BackgroundColor = patternColor;
+
+			if (OperatingSystem.IsIOSVersionAtLeast(26))
+				tabBar.BackgroundColor = patternColor;
+
+			tabBar.StandardAppearance = tabBar.ScrollEdgeAppearance = _tabBarAppearance;
+			_tabBarBackgroundImageSize = size;
+		}
+
+		static UIImage CreateGradientImage(Brush brush, CGSize size)
+		{
+			var frame = new CGRect(CGPoint.Empty, size);
+			var layer = ((Paint)brush)?.ToCALayer(frame);
+
+			if (layer is null)
+				return null;
+
+			layer.Frame = frame;
+
+			using var renderer = new UIGraphicsImageRenderer(size);
+			return renderer.CreateImage(context => layer.RenderInContext(context.CGContext));
+		}
+
 		void UpdateTabBarAppearance(UITabBarController controller, ShellAppearance appearance)
 		{
 			IShellAppearanceElement appearanceElement = appearance;

PR #37137 — Try-Fix Candidate 2

Attempt directory: CustomAgentLogsTmp/PRState/37137/PRAgent/try-fix/attempt-2/
Platform: iOS
Result:Pass
Self-review findings: 0

Approach: Centralized effective tab-bar brush

The root defect is the absence of one Shell-level semantic for the effective tab-bar background. ShellAppearance now resolves TabBarBackgroundColor > Shell.Background > Shell.BackgroundColor as an internal Brush, while the existing IShellAppearanceElement.EffectiveTabBarBackgroundColor contract remains exactly TabBarBackgroundColor ?? BackgroundColor for all existing consumers.

The iOS Shell tracker consumes this centralized brush and no longer duplicates precedence. TabbedViewExtensions retains its original Color overload for existing TabbedPage callers and adds a Paint overload for Shell. Before every application it calls ConfigureWithDefaultBackground, removes the prior MAUI background layer, and clears the native view background; it then applies the current gradient or solid/default state. ResetAppearance performs the same native cleanup. Updates are therefore idempotent and gradient-to-solid/default transitions restore the default blur/background rather than retaining stale state.

How this differs

  • Submitted PR: deleted the shared effective-color fallback, reconstructed precedence in each platform tracker, widened the existing helper contract, and left transition state sticky.
  • Candidate 1: resolved and rasterized gradients locally in the tracker using UIImage mechanisms that iOS 26 ignored or tiled/misaligned.
  • Candidate 2: centralizes the cross-platform Shell semantic, preserves the effective-color and TabbedPage contracts, uses no UIImage/background-image/pattern/resizable-image strategy, and resets native appearance state deterministically.

Files changed

  • src/Controls/src/Core/Shell/ShellAppearance.cs (+9) — adds the internal centralized effective-brush semantic while preserving the effective-color fallback.
  • src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs (+12/−1) — consumes the centralized brush and restores native state on reset.
  • src/Core/src/Platform/iOS/TabbedViewExtensions.cs (+41/−4) — retains the Color contract, adds a Paint overload, and makes background application idempotent and transition-safe.

Test command and result

pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "ShellBackgroundSupportsGradientBrush"

Result: Pass on the first and only implementation/test pass; no correction or retest was used.

Passed ShellBackgroundSupportsGradientBrush [594 ms]
Test Run Successful.
Total tests: 1
     Passed: 1
✅ All tests passed

Analysis

The existing CALayer-backed paint path rendered the expected screenshot once it received the correctly resolved Shell brush. Reinitializing UITabBarAppearance from UIKit defaults and removing the previous background layer before every update prevents stale cleared effects, colors, or gradient layers from surviving repeated updates. The original color overload converts with barBackgroundColor?.AsPaint(), so explicitly null TabbedPage colors remain safe.

No failure analysis is applicable because the bounded test passed on the first run.

Inline expert self-review — 0 findings

Performed inline against .github/agents/maui-expert-reviewer.md on the final unchanged diff. Reviewed the overarching principles; always-active Logic and Correctness, Regression Prevention, and Complexity Reduction; plus Architectural Layer Placement, Null Safety, Backward Compatibility, Native Platform Defaults, iOS/macCatalyst, and Navigation & Shell. The review was clean; reviewer-findings.json contains [].

Full saved diff

diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
index e0ecca140f..768e1c4b0a 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs
@@ -19,6 +19,16 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 				return;
 
 			var tabBar = controller.TabBar;
+			if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15))
+			{
+				tabBar.RemoveBackgroundLayer();
+				tabBar.BackgroundColor = null;
+
+				_tabBarAppearance ??= new UITabBarAppearance();
+				_tabBarAppearance.ConfigureWithDefaultBackground();
+				tabBar.StandardAppearance = tabBar.ScrollEdgeAppearance = _tabBarAppearance;
+			}
+
 			tabBar.BarTintColor = _defaultBarTint;
 			tabBar.TintColor = _defaultTint;
 			tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
@@ -71,7 +81,6 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 		{
 			IShellAppearanceElement appearanceElement = appearance;
 
-			var backgroundColor = appearanceElement.EffectiveTabBarBackgroundColor;
 			var foregroundColor = appearanceElement.EffectiveTabBarForegroundColor;
 			var unselectedColor = appearanceElement.EffectiveTabBarUnselectedColor;
 			var titleColor = appearanceElement.EffectiveTabBarTitleColor;
@@ -84,7 +93,7 @@ namespace Microsoft.Maui.Controls.Platform.Compatibility
 					null,
 					foregroundColor ?? titleColor,
 					unselectedColor,
-					backgroundColor,
+					appearance.EffectiveTabBarBackground,
 					titleColor ?? foregroundColor,
 					unselectedColor);
 
diff --git a/src/Controls/src/Core/Shell/ShellAppearance.cs b/src/Controls/src/Core/Shell/ShellAppearance.cs
index b439633454..6890b79062 100644
--- a/src/Controls/src/Core/Shell/ShellAppearance.cs
+++ b/src/Controls/src/Core/Shell/ShellAppearance.cs
@@ -78,6 +78,15 @@ namespace Microsoft.Maui.Controls
 		public double FlyoutWidth => _doubleArray[0];
 		public double FlyoutHeight => _doubleArray[1];
 
+		internal Brush EffectiveTabBarBackground =>
+			TabBarBackgroundColor is not null
+				? new SolidColorBrush(TabBarBackgroundColor)
+				: !Brush.IsNullOrEmpty(Background)
+					? Background
+					: BackgroundColor is not null
+						? new SolidColorBrush(BackgroundColor)
+						: null;
+
 		Color IShellAppearanceElement.EffectiveTabBarBackgroundColor =>
 			TabBarBackgroundColor ?? BackgroundColor;
 
diff --git a/src/Core/src/Platform/iOS/TabbedViewExtensions.cs b/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
index 055c4ad399..4a2b36593b 100644
--- a/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
+++ b/src/Core/src/Platform/iOS/TabbedViewExtensions.cs
@@ -38,14 +38,53 @@ namespace Microsoft.Maui.Platform
 			Color? barBackgroundColor,
 			Color? selectedBarTextColor,
 			Color? unSelectedBarTextColor)
+		{
+			UpdateiOS15TabBarAppearance(
+				tabBar,
+				ref _tabBarAppearance,
+				defaultBarColor,
+				defaultBarTextColor,
+				selectedTabColor,
+				unselectedTabColor,
+				barBackgroundColor?.AsPaint(),
+				selectedBarTextColor,
+				unSelectedBarTextColor);
+		}
+
+		[System.Runtime.Versioning.SupportedOSPlatform("ios15.0")]
+		[System.Runtime.Versioning.SupportedOSPlatform("tvos15.0")]
+		internal static void UpdateiOS15TabBarAppearance(
+			this UITabBar tabBar,
+			ref UITabBarAppearance _tabBarAppearance,
+			UIColor? defaultBarColor,
+			UIColor? defaultBarTextColor,
+			Color? selectedTabColor,
+			Color? unselectedTabColor,
+			Paint? barBackground,
+			Color? selectedBarTextColor,
+			Color? unSelectedBarTextColor)
 		{
 			if (_tabBarAppearance == null)
-			{
 				_tabBarAppearance = new UITabBarAppearance();
-				_tabBarAppearance.ConfigureWithDefaultBackground();
+
+			_tabBarAppearance.ConfigureWithDefaultBackground();
+			tabBar.RemoveBackgroundLayer();
+			tabBar.BackgroundColor = null;
+
+			var effectiveBarColor = barBackground switch
+			{
+				null => defaultBarColor,
+				SolidPaint solidPaint => solidPaint.Color?.ToPlatform(),
+				_ => null,
+			};
+
+			if (barBackground is GradientPaint)
+			{
+				_tabBarAppearance.BackgroundEffect = null;
+				_tabBarAppearance.BackgroundColor = UIColor.Clear;
+				tabBar.UpdateBackground(barBackground);
 			}
 
-			var effectiveBarColor = (barBackgroundColor == null) ? defaultBarColor : barBackgroundColor.ToPlatform();
 			// Set BarBackgroundColor
 			if (effectiveBarColor != null)
 			{

📝 PR Finalize — Recommended Title & Description

Assessment: ✏️ Recommend updating — the current description incorrectly claims subclass compatibility and omits the shared EffectiveTabBarBackgroundColor behavior change and the exact screenshot-test scope.

Recommended title

[.NET 11] [Android/iOS] Shell: Render gradient backgrounds on bottom tab bars

Recommended description

### Root Cause

`Shell.Background` supports `SolidColorBrush`, `LinearGradientBrush`, and `RadialGradientBrush`, but the Shell bottom-tab appearance trackers used `EffectiveTabBarBackgroundColor`, which exposes only a `Color`. Gradient information therefore never reached the native bottom-tab background:

- On iOS, `UpdateiOS15TabBarAppearance` accepted only a `Color`.
- On Android, the tracker created a `MaterialShapeDrawable` or color-transition drawable from a single color.

### Description of Change

The Android and iOS appearance paths now resolve backgrounds in this order:

1. `Shell.TabBarBackgroundColor`
2. `Shell.Background`
3. `Shell.BackgroundColor`
4. Platform default

`ShellAppearance.EffectiveTabBarBackgroundColor` now returns only `TabBarBackgroundColor`; the Android and iOS 15+ trackers reconstruct the remaining brush/color fallback locally.

**iOS**

- Changed `UpdateiOS15TabBarAppearance` to accept `Paint?` instead of `Color?`.
- Converts Shell brushes to `SolidPaint`, `LinearGradientPaint`, or `RadialGradientPaint`.
- Renders gradient paint through the native tab-bar background layer.
- Clears the default tab-bar blur/background effect while a gradient is applied.
- Converts the `TabbedPage` and compatibility `TabbedRenderer` `BarBackgroundColor` call sites with `.AsPaint()`.

**Android**

- Added `SetBackground(BottomNavigationView, Brush)` and routes gradient brushes through the existing `UpdateBackground`/`MauiDrawable` path.
- Retains color-transition drawables for solid colors.
- Keeps `SetBackgroundColor(BottomNavigationView, Color)` as an obsolete protected virtual method that forwards to the new brush overload.
- Appearance updates now call the new brush overload directly. Subclasses that override only `SetBackgroundColor` are therefore no longer invoked by `SetAppearance` or `ResetAppearance`.

**Regression fixture**

- Changes the existing Issue10445 host page to render two bottom tabs.
- Updates the iOS and iOS 26 `ShellBackgroundSupportsGradientBrush` screenshot baselines.
- No new test method is added, and the Android, Windows, and MacCatalyst baselines are unchanged.

### Issues Fixed

Fixes #37138

### Validated the behaviour in the following platforms

- [x] Android
- [ ] Windows
- [x] iOS
- [x] Mac

### Screenshots

| | Before | After |
|---|---|---|
| iOS | <img src="https://github.com/user-attachments/assets/181734bd-8cc7-4201-8148-572ecd9308a5"> | <img src="https://github.com/user-attachments/assets/01ab4471-cb7d-47d6-ac54-35a7601f6012"> |
| Android | <img src="https://github.com/user-attachments/assets/a495d045-fefa-4da6-8353-bac7501703a5"> | <img src="https://github.com/user-attachments/assets/11066dd4-4f38-407e-8059-a2952d01ee93"> |

🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr-plus-reviewer

The submitted PR should not merge as-is. The expert review found concrete null crashes, cross-platform fallback regressions, stale iOS appearance state, and a broken Android subclass extension point. The consolidated reviewer candidate fixes those implementation defects while preserving the submitted Android and iOS gradient behavior, and its required iOS screenshot test passed 1/1.

Candidate ranking

Rank Candidate Required iOS validation Assessment
1 pr-plus-reviewer ✅ Pass (1/1) Most complete. Preserves shared color semantics and TabbedPage compatibility, centralizes brush precedence, restores idempotent iOS native state, retains Android virtual override behavior, and cleans up gradient transitions.
2 try-fix-2 ✅ Pass (1/1) Strong iOS-only alternative with centralized brush semantics and deterministic native reset. It passed on its first run and had no self-review findings, but does not carry the submitted Android implementation or its compatibility correction.
3 pr Gate ⚠️ skipped; no bounded result recorded Static gradient rendering is implemented, but ten expert findings include two explicit-null crashes and several major behavior regressions. It cannot win despite the intended rendering path.
4 try-fix-1 ❌ Fail (1.51% screenshot delta after correction) Architecturally localized, but UIColor.FromPatternImage tiles/misaligns on iOS 26. It also retained three self-review findings. Per the execution contract, this failed candidate ranks below both passing candidates.

Why pr-plus-reviewer wins

It combines the passing CALayer-backed iOS strategy with the submitted cross-platform scope and addresses the expert review's concrete failure modes rather than only the static screenshot. Its result was produced from the exact required sandbox, and the test result path confirms execution under /Users/cloudtest/vss/_work/_temp/pr-37137-pr-plus-reviewer.

Remaining coverage gap

The Gate was skipped because it detected no new test. The existing screenshot does not exercise precedence, explicit-null, reset/reconnect, or gradient-to-solid/default transitions, and this iOS-bounded comparison did not regenerate or run non-iOS snapshots. These are recorded uncertainties rather than grounds for another repair/test loop.


📱 UI Tests — Shell,TabbedPage,ViewBaseTests

Detected UI test categories: Shell,TabbedPage,ViewBaseTests

Deep UI tests — 462 passed, 0 failed, 20 skipped across 3 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Shell 305/320 (15 skipped) ✓
TabbedPage 45/50 (5 skipped) ✓
ViewBaseTests 112/112 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

🧭 Next Steps — reviewer patch required (pr-plus-reviewer)

The reviewer-enhanced candidate won, so the submitted PR still needs those changes.

Why: pr-plus-reviewer preserves the submitted Android and iOS gradient fix while correcting the expert review's null-safety, shared fallback, native-state transition, and Android compatibility defects. Its required iOS screenshot test passed 1/1, whereas the raw PR has blocking findings and try-fix-1 failed its regression test.

Apply PRAgent/pr-plus-reviewer/reviewer.patch from the CopilotLogs
artifact (or follow the report's Required submitted-PR change), push the update, and run
the review again.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 11, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please check the ai's suggestions?

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

Labels

area-controls-shell Shell Navigation, Routes, Tabs, Flyout partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android platform/ios platform/macos macOS / Mac Catalyst s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shell.Gradient color not working

5 participants