Skip to content

Fix iOS Shell tab badge styling - #35565

Merged
jfversluis merged 5 commits into
net11.0from
jfversluis/ios-shell-badge-styling
Jun 9, 2026
Merged

Fix iOS Shell tab badge styling#35565
jfversluis merged 5 commits into
net11.0from
jfversluis/ios-shell-badge-styling

Conversation

@jfversluis

@jfversluis jfversluis commented May 21, 2026

Copy link
Copy Markdown
Member

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!

Description

Fixes iOS Shell tab badge styling so BadgeColor and BadgeTextColor are applied to the actual native UITabBarItem used by each Shell section, including the UITabBarAppearance badge state values that drive rendering on iOS 15+ / iOS 26.

ShellSectionRenderer can asynchronously create or replace its TabBarItem after loading the tab icon. That path only restored title/accessibility metadata, so custom badge colors could be lost and iOS would render default red/white badges. This change reuses the existing Shell badge helper after the native item is created, updates badge property changes through the renderer's current ViewController.TabBarItem, and keeps null badge colors on the system defaults.

Screenshots

Before/after screenshots were captured locally on iPhone 17 Pro iOS 26.2:

  • shell-badge-before.png — visible default iOS red/white Shell tab badges before the fix
  • shell-badge-after.png — visible green #00C853 Shell tab badges with dark #111111 text after the fix
  • shell-toolbar-badges-sandbox.png — Sandbox sample showing Shell tab badges and primary ToolbarItem badges styled consistently after the appearance fix

Before
shell-badge-before

After
shell-badge-after

Full with ToolBarIcon
shell-toolbar-badges-sandbox-rerun-20260521-1605

Testing

  • dotnet build src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj -c Release -f net11.0-ios -r iossimulator-arm64 /p:CodesignRequireProvisioningProfile=false /p:ValidateXcodeVersion=false /p:TreatWarningsAsErrors=false
  • dotnet xharness apple test --app artifacts/bin/Controls.DeviceTests/Release/net11.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app --target ios-simulator-64_26.2 --device 07C63EC0-C474-428F-A850-30D7D9D2D991 -o artifacts/log/shell-ios --timeout 00:30:00 -v --set-env=TestFilter=Category=Shell205 Passed, 0 Failed
  • PATH="$PWD/CustomAgentLogsTmp/Sandbox/bin:$PATH" _MauiDotNetVersion=10.0 MtouchLink=SdkOnly pwsh .github/scripts/BuildAndRunSandbox.ps1 -Platform ios -DeviceUdid 07C63EC0-C474-428F-A850-30D7D9D2D991
Apply Shell badge styling to the actual native tab bar item after iOS ShellSectionRenderer creates or replaces it during async icon updates.

Add focused iOS device coverage for initial badge colors and preserving those colors after title/icon changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented May 21, 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 -- 35565

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35565"
@github-actions github-actions Bot added area-controls-shell Shell Navigation, Routes, Tabs, Flyout platform/ios platform/macos macOS / Mac Catalyst labels May 21, 2026
Keep the focused Shell badge tests in the existing Shell device-test category instead of adding a new category.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo

kubaflo commented May 21, 2026

Copy link
Copy Markdown
Collaborator

/review -b feature/regression-check -p ios

@MauiBot MauiBot added s/agent-review-incomplete s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels May 21, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet dotnet deleted a comment from MauiBot May 21, 2026
@kubaflo

kubaflo commented May 21, 2026

Copy link
Copy Markdown
Collaborator

/review -b feature/regression-check -p ios

@MauiBot

MauiBot commented May 21, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@kubaflo

kubaflo commented May 22, 2026

Copy link
Copy Markdown
Collaborator

/review -b feature/refactor-copilot-yml

@dotnet dotnet deleted a comment from MauiBot May 23, 2026
@kubaflo

kubaflo commented May 23, 2026

Copy link
Copy Markdown
Collaborator

🔍 Multimodal Code Review — PR #35565

Reviewers: Claude Opus 4.7 (expert-reviewer) + visual screenshot analysis
Verdict: Good fix with one correctness issue to address before merge


✅ Visual Review (Screenshots)

The before/after screenshots clearly confirm the fix works:

  • Before: Default iOS red/white badges on both "Inbox" (7) and "Alerts" (42) tabs
  • After: Custom green (#00C853) badge backgrounds with dark (#111111) text on both tabs
  • Full Sandbox: Both Shell tab badges (bottom) and ToolbarItem badges (top) render consistently with the correct custom colors on iPhone 17 Pro / iOS 26.2

The visual evidence is convincing — the fix correctly bridges the gap between the MAUI BadgeColor/BadgeTextColor properties and the native UITabBarAppearance API on iOS 15+.


🔴 Major Finding

WeakBadgeTextAttributes = new NSDictionary() does not reset to system defaults (ShellItemRenderer.cs, new appearance code)

When badgeTextColor is null but badgeColor is set, the code assigns an empty NSDictionary() to WeakBadgeTextAttributes. On UIKit, the correct way to restore the system-default badge text style is to assign null (Objective-C nil). An empty dictionary explicitly overrides system defaults with no attributes, which can produce different rendering than the system-managed badge style.

This is the exact scenario exercised by the third test (ShellTabBadgeBackgroundColorAppliesWithDefaultTextColor), but the test assertion doesn't verify the actual rendered text color matches the system default.

Suggested fix:

appearance.WeakBadgeTextAttributes = badgeTextColor is null
    ? null
    : new UIStringAttributes { ForegroundColor = badgeTextColor.ToPlatform() }.Dictionary;

🟡 Moderate Findings

  1. Clearing badges wipes entire per-item appearance — When both badge colors are null, setting StandardAppearance = null and ScrollEdgeAppearance = null clears the entire per-item appearance, not just badge fields. If anything else sets per-item appearance for non-badge reasons (selected/unselected title colors, icon tint, etc.), this will silently clobber it. Safer approach: clone and zero only badge-specific fields.

  2. ScrollEdgeAppearance forkingCreateTabBarBadgeAppearance falls back to cloning tabBar?.StandardAppearance for scroll-edge when ScrollEdgeAppearance is nil. On iOS 15+, UIKit implicitly uses StandardAppearance at runtime when scroll-edge is nil. Cloning Standard into a freestanding ScrollEdge forks the state, so subsequent changes to StandardAppearance (e.g., theme changes via appearance tracker) won't propagate. Consider only setting scroll-edge when the tab bar actually has its own ScrollEdgeAppearance.

  3. Per-update allocation pressure — Every badge update allocates 2 UITabBarAppearance clones + walks 3 layouts × 4 states = 12 state writes, each with potential NSDictionary/UIColor allocations. For apps with frequent badge mutations (chat unread counters), consider caching the last-applied (color, textColor) tuple and skipping rebuilds when unchanged.

  4. Threading in UpdateTabBarItem callback — The LoadImage continuation in ShellSectionRenderer.UpdateTabBarItem() may not be guaranteed to run on the main thread. The new UpdateTabBarItemBadge call touches UITabBarItem.StandardAppearance/ScrollEdgeAppearance which requires main-thread access. Also, TabBarController can be null if the section detaches before the async callback completes — the resulting null tabBar silently falls back to ConfigureWithDefaultBackground, which won't inherit the tab bar's actual styling.

  5. Test coverage gaps — The three tests cover happy paths but miss: (a) clearing badge colors back to null (exercises the if (badgeColor is null && badgeTextColor is null) branch), and (b) asserting that the rendered text color with only BadgeColor set actually matches the iOS system default.


🟢 Minor Findings

  • Null safety: renderer.ViewController.TabBarItemViewController itself isn't null-checked. Quick fix: renderer.ViewController?.TabBarItem.
  • Method naming: SetTabItemsEnabledState now also drives badge updates, making the name misleading. Consider renaming to SetTabItemsInitialState or splitting badge init into its own method.

👍 What's Good

  • Switching from index-based TabBar.Items[index] to renderer.ViewController.TabBarItem is a genuine improvement — more direct and avoids index mismatches
  • internal static visibility is the right choice for cross-file same-assembly access
  • SupportedOSPlatform attributes + OperatingSystem.IsIOSVersionAtLeast checks are correctly linker-friendly
  • Covering all 3 layout appearances (stacked, inline, compact-inline) × 4 states (normal, selected, disabled, focused) is thorough
  • The device tests follow good patterns (ControlsHandlerTestBase, RunInNewWindowCollection, AssertEventually)
  • The fix for ShellSectionRenderer.UpdateTabBarItem() calling UpdateTabBarItemBadge after icon load is the core bug fix and is correctly placed

Review performed with Claude Opus 4.7 expert reviewer + visual screenshot analysis

@kubaflo

kubaflo commented May 24, 2026

Copy link
Copy Markdown
Collaborator

/review -b feature/refactor-copilot-yml

@MauiBot

MauiBot commented May 24, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Merge Conflict Detected — This PR has merge conflicts with its target branch. Please rebase onto the target branch and resolve the conflicts.

@kubaflo

kubaflo commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

AI code review for net11.0 target

Verdict: LGTM (minor notes)

iOS/MacCatalyst Shell change that applies tab-badge background/text colors through UITabBarAppearance (iOS/MacCatalyst 15+), in addition to the legacy BadgeColor/SetBadgeTextAttributes path. It also refactors UpdateTabBarItemBadge to take the UITabBarItem directly (with a null guard) and wires it from ShellSectionRenderer.UpdateTabBarItem, fixing badge styling being lost after tab-bar item updates. Good device-test coverage (ShellBadgeTests.iOS.cs) including a persistence-after-update case.

What looks good

  • All appearance helpers are correctly guarded with [SupportedOSPlatform("ios15.0")]/maccatalyst15.0 and called only after the OperatingSystem.IsIOSVersionAtLeast(15) check.
  • CreateTabBarBadgeAppearance seeds the item appearance from the existing item/tab-bar appearance, preserving the developer's configured background instead of wiping it.
  • The both-colors-null branch resets StandardAppearance/ScrollEdgeAppearance to null, restoring defaults.

Minor notes (non-blocking)

  • Setting tabBarItem.StandardAppearance/ScrollEdgeAppearance overrides any per-item appearance an app may have configured elsewhere; the copy-from-base mitigates this, but worth a maintainer sanity check for apps that set custom tab-bar appearances.
  • A fresh UITabBarAppearance is allocated on each badge update. Fine for a low-frequency path, but avoid calling it on hot paths.
  • WeakBadgeTextAttributes is set to an empty NSDictionary when no text color is supplied — confirm this doesn't clear a previously-set system default font/size unexpectedly.

CI: Failing legs (AOT macOS, RunOniOS_*) also fail identically on unrelated PRs in this batch (including a Windows-only PR), indicating infra/known failures rather than PR-caused regressions. A maintainer should confirm the new iOS device tests ran green.

Confidence: Medium. The appearance edge cases look handled; iOS rendering should be visually verified.

Automated, non-approval review comment from the net11.0 fleet reviewer. Not a substitute for human approval.

jfversluis and others added 2 commits June 8, 2026 16:14
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kubaflo

kubaflo commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

AI code review refresh for net11.0 target

Head reviewed: 177f4aa
Target branch: net11.0 (open, mergeable)

Verdict: LGTM (with an unrelated-CI caveat — see below). Non-approval; this is automated reviewer feedback only.

What changed since the prior review (round 11, marker 2026-06-01)

Two new commits refine the implementation and address review feedback — they do not regress the prior positive assessment:

  • a2733cfAddress Shell badge appearance review feedback
    • Null-guards: renderer.ViewController?.TabBarItem and an early if (tabBarItem is null) return; in UpdateTabBarItemBadge. Good defensive hardening.
    • Clearing badge text color no longer wipes the whole attribute dictionary. Instead of replacing WeakBadgeTextAttributes with an empty/NSDictionary, it now removes only the ForegroundColor key from a mutable copy, preserving any other badge text attributes (font, etc.). This is the correct, surgical behavior.
    • Adds a ShellTabBadgeTextColorClearsToDefault device test covering the clear-to-default path, plus matching appearance-text-color assertion helpers.
  • 177f4aaCorrect Shell badge text color test data: swaps #111111/#FF00FF between the two persistence/clear tests so the colors are distinct from each other and from Tab2. Test-data-only, low risk.

Code assessment

  • OS-version guarding is consistent and correct: appearance work is gated on IsIOSVersionAtLeast(15) || IsMacCatalystVersionAtLeast(15) and the appearance helpers carry [SupportedOSPlatform("ios15.0"/"maccatalyst15.0")]. UITabBarItem.ScrollEdgeAppearance is iOS 15+, so the 15 floor is appropriate.
  • Appearance lifecycle looks sound: when both badgeColor and badgeTextColor are null the item's StandardAppearance/ScrollEdgeAppearance are reset to null (back to the tab bar default); otherwise the per-item appearance is cloned from the existing item appearance or the tab bar's appearance (ScrollEdgeAppearance ?? StandardAppearance fallback), then badge colors are applied across Stacked/Inline/CompactInline × Normal/Selected/Disabled/Focused states.
  • ShellSectionRenderer.UpdateTabBarItem now re-applies the badge after rebuilding TabBarItem, which is what the new "persist after updates" test exercises.

Devil's advocate (low-signal, non-blocking)

  • The appearance is applied across all four state appearances uniformly; native badge styling is generally state-agnostic here, so this is fine, just slightly broad.
  • internal static UpdateTabBarItemBadge(..., UITabBar tabBar = null) is now called from ShellSectionRenderer with TabBarController?.TabBar, which can be null early in the lifecycle; the helper tolerates a null tabBar (falls back to a default UITabBarAppearance), so no issue.

CI

Required checks are red, but the failures appear unrelated to this PR:

  • The failing iOS Release / TrimFull / AOT integration jobs are template build failures (Project Test*.csproj failed to build) caused by ILLink : Trim analysis error IL2026 in HybridWebViewHandler — a pre-existing net11.0 trim/template break, not the Shell renderer change in this PR.
  • Build Analysis is also failing (typically aggregates the above / known-issue matching).
  • All build/pack, Windows/macOS Helix unit tests, and the non-trim iOS integration jobs are green.
  • Note: this PR's new ShellBadgeTests.iOS.cs device tests run in the device-test pipeline, which is not part of this maui-pr build — I did not independently confirm their pass/fail here.

Confidence: High that the diff is correct and the CI failures are not caused by this change; Medium on full device-test verification (not visible in this build).


Automated net11.0 fleet review (round 12). This is not an approval or a request for changes — a human reviewer should make the merge decision.

@jfversluis
jfversluis merged commit 74e739f into net11.0 Jun 9, 2026
30 of 36 checks passed
@jfversluis
jfversluis deleted the jfversluis/ios-shell-badge-styling branch June 9, 2026 09:18
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-controls-shell Shell Navigation, Routes, Tabs, Flyout platform/ios platform/macos macOS / Mac Catalyst s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

3 participants