[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android - #37137
[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android#37137Dhivya-SF4094 wants to merge 3 commits into
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37137Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37137" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
d8f4466 to
3d62979
Compare
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
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:
UpdateiOS15TabBarAppearancenow accepts aPaint?and applies gradient backgrounds viaUpdateBackground(...). - Android: Shell bottom nav appearance tracker now supports brush-based backgrounds (including gradients) and retains an obsolete color-based entry point.
- Tests: The
Issue10445HostApp 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. |
| if (barBackground is GradientPaint) | ||
| { | ||
| _tabBarAppearance.BackgroundEffect = null; | ||
| _tabBarAppearance.BackgroundColor = UIColor.Clear; | ||
| tabBar.BackgroundColor = UIColor.Clear; | ||
| tabBar.UpdateBackground(barBackground); | ||
| } | ||
| else | ||
| { | ||
| tabBar.RemoveBackgroundLayer(); | ||
| } |
| Color IShellAppearanceElement.EffectiveTabBarBackgroundColor => | ||
| TabBarBackgroundColor ?? BackgroundColor; | ||
| TabBarBackgroundColor; |
| bottomView.ItemIconTintList = _itemIconTint; | ||
|
|
||
| SetBackgroundColor(bottomView, backgroundColor); | ||
| SetBackground(bottomView, background); |
| AddBottomTab(page, "Home"); | ||
| AddBottomTab(new ContentPage |
| bottomView.ItemIconTintList = GetDefaultTabColorList(_shellContext.AndroidContext); | ||
| bottomView.ItemTextColor = GetDefaultTabColorList(_shellContext.AndroidContext); | ||
| SetBackgroundColor(bottomView, null); | ||
| SetBackground(bottomView, null); |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[critical] Logic and Correctness — Color.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, |
There was a problem hiding this comment.
🔍 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; |
There was a problem hiding this comment.
🔍 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:763→UpdateTopNavAreaBackground(null); Windows top-nav area reverts to the platform default when onlyShell.BackgroundColoris set.src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Tizen.cs:106.src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs:149— now always writesDefaultBottomNavigationViewBackgroundColorinto the existingColorDrawable.src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs:114(UpdateTabBarAppearance, the pre-iOS-15 path) still readsEffectiveTabBarBackgroundColordirectly 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; |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Architectural Layer Placement — The precedence chain (TabBarBackgroundColor → Shell.Background → Shell.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); |
There was a problem hiding this comment.
🔍 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; |
There was a problem hiding this comment.
🔍 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 null → effectiveBarColor = defaultBarColor; if defaultBarColor is null (the Shell tracker passes null for defaultBarColor — SafeShellTabBarAppearanceTracker.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 |
There was a problem hiding this comment.
🔍 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:
SolidPaintwith anullColor→solidPaint.Color?.ToPlatform()yieldsnull, anddefaultBarColoris never applied. PreviouslybarBackgroundColor == nullmapped straight todefaultBarColor. This is reachable today viaBarBackgroundColor.AsPaint()atTabbedRenderer.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"); |
There was a problem hiding this comment.
🔍 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 |
There was a problem hiding this comment.
🔍 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:
- Precedence:
Shell.TabBarBackgroundColormust still win over a gradientShell.Background(the new nested conditional inShellBottomNavViewAppearanceTracker.cs:56-61andSafeShellTabBarAppearanceTracker.cs:74-80). - The removed
EffectiveTabBarBackgroundColor→BackgroundColorfallback: a Shell with onlyShell.BackgroundColorset must still tint the tab bar (a device/unit test here would have caught the Windows/Tizen/ShellItemRendererregression). - Gradient → solid/cleared transition on both Android and iOS (see the
TabbedViewExtensions.cs:57comment).
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 |
There was a problem hiding this comment.
🔍 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.
This comment has been minimized.
This comment has been minimized.
|
/azp run |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
MauiBot
left a comment
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[critical] Logic and Correctness — Color.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, |
There was a problem hiding this comment.
🔍 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; |
There was a problem hiding this comment.
🔍 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:763—Shell.BackgroundColorno 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 withDefaultBottomNavigationViewBackgroundColorwhen onlyShell.BackgroundColoris set.
Prefer exposing the resolved value onShellAppearanceitself (e.g. an internalEffectiveTabBarBackgroundbrush 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 |
There was a problem hiding this comment.
🔍 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 |
There was a problem hiding this comment.
🔍 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:
Shell.Background = new ImageBrush{...}converts toImageSourcePaint, hits_ => null, so no background is applied at all AND_tabBarAppearance.BackgroundColorkeeps the previously applied color (the appearance object is cached in thereffield across calls). Android'sUpdateBackgrounddoes render image paints, so the platforms diverge.- Clearing a previously-set bar color (
SolidColorBrushwith a nullColor, orBarBackgroundColor = nullwhileIsSetis 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_ => defaultBarColorfor non-gradient/unsupported paints (or handle them viatabBar.UpdateBackground).
| _ => null, | ||
| }; | ||
|
|
||
| if (barBackground is GradientPaint) |
There was a problem hiding this comment.
🔍 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.")] |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Backward Compatibility — SetBackgroundColor 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; |
There was a problem hiding this comment.
🔍 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"); |
There was a problem hiding this comment.
🔍 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 |
There was a problem hiding this comment.
🔍 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
left a comment
There was a problem hiding this comment.
AI Review Summary
@Dhivya-SF4094 — new AI review results are available based on commit
3d62979.
🗂️ 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.Backgroundrenders 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:
- Changes
ShellAppearance.EffectiveTabBarBackgroundColorto return onlyTabBarBackgroundColor. - Reconstructs the precedence
TabBarBackgroundColor→Shell.Background→Shell.BackgroundColorinSafeShellTabBarAppearanceTracker. - Changes
UpdateiOS15TabBarAppearancefrom aColor?background parameter toPaint?, rendersGradientPaintthroughUITabBar.UpdateBackground, and clears the native background effect for gradients. - Converts the two
TabbedPagecolor call sites toPaint. - 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.cssrc/Core/src/Platform/iOS/TabbedViewExtensions.cssrc/Controls/src/Core/Shell/ShellAppearance.cssrc/Controls/src/Core/TabbedPage/TabbedPage.iOS.cssrc/Controls/src/Core/Compatibility/Handlers/TabbedPage/iOS/TabbedRenderer.cssrc/Controls/tests/TestCases.HostApp/Issues/Issue10445.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue10445.cs
Constraints and Known Risks
- Preserve the shared
EffectiveTabBarBackgroundColorfallback for consumers on platforms not changed by an iOS candidate. - Preserve precedence: explicit
Shell.TabBarBackgroundColorwins overShell.Background, which wins overShell.BackgroundColor. - A gradient-to-solid/default transition must restore native tab-bar appearance state, including the default blur/background effect.
BarBackgroundColormay be explicitly set tonull; conversion toPaintmust 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/**andeng/**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.mdwhen changing protected/public API.github/instructions/uitests.instructions.mdif 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
TabbedPage.iOS.cs:849and compatibilityTabbedRenderer.cs:645call the instance methodColor.AsPaint()on a possibly explicit-nullBarBackgroundColor, causing aNullReferenceExceptioninstead of restoring the default.ShellAppearance.EffectiveTabBarBackgroundColordrops itsBackgroundColorfallback 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.- The iOS appearance helper does not fully reset its cached
UITabBarAppearancewhen moving from a gradient to a solid/default background. Unsupported or null-valued paints can also leave stale native background state. - 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 outgoingMauiDrawableundisposed. - 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):
- Precedence resolved locally in
ResolveTabBarBackground:TabBarBackgroundColor>Shell.Background(SolidColorBrush→ color,GradientBrush→ gradient) >IShellAppearanceElement.EffectiveTabBarBackgroundColor(unchangedTabBarBackgroundColor ?? BackgroundColor) > platform default. - Gradients rasterised once via
Paint.ToCALayer(...)+UIGraphicsImageRenderer, then handed to UIKit through the appearance system's own background slots (initiallyBackgroundImage; after the correction,UIColor.FromPatternImageonBackgroundColorplustabBar.BackgroundColorfor iOS 26). NoCAGradientLayeris injected into the tab bar's layer tree. - Transitions handled by rebuilding
_tabBarAppearancewhenever the gradient/non-gradient mode flips — the shared helper then re-runsConfigureWithDefaultBackground(), restoring the default blur without manual un-mutation.tabBar.BackgroundColoris cleared on the way out for iOS 26. ResetAppearancedrops the gradient and installs a fresh default appearance (the submitted PR leaves a gradient stuck on the tab bar after reset).UpdateLayoutre-rasterises when the tab-bar bounds change (rotation / size class), and covers the case where bounds are not yet valid atSetAppearancetime.
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
- iOS 26 ignores
UITabBarAppearance.BackgroundImagefor the redesigned floating tab bar. OnlyBackgroundColor— on the appearance and on theUITabBaritself, which is exactly the case the existing shared helper already special-cases withif (OperatingSystem.IsIOSVersionAtLeast(26))— reaches the rendered surface. This invalidated pass 1 outright. - Pattern colors do not stretch.
UIColor.FromPatternImagetiles 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 fromtabBar.Bounds, the gradient lands slightly offset relative to the baseline, which the submitted PR produced with a stretchedCAGradientLayer. 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.
- [major] Logic and Correctness (iOS rendering) —
UIColor.FromPatternImagetiles from the drawing origin, not the tab bar bounds origin; offset/clipping rather than stretching. This finding independently predicted the exact observed failure mode. - [moderate] Regression Prevention —
tabBar.BackgroundColor = nullon the gradient→solid transition restores the UIKit default but does not restore a value set through another path;_defaultBarTinthas a capture,BackgroundColordoes not. - [minor] Memory and Lifecycle —
_tabBarBackgroundBrushholds a strongBrush(Element) reference not cleared inDispose(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> > <c>Shell.Background</c> > <c>Shell.BackgroundColor</c> > 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 |
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.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check the ai's suggestions?
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:
iOS
Android
Issues Fixed:
Fixes #37138
Validated the behaviour in the following platforms
Screenshots