Fix iOS toolbar empty badge indicator#35504
Conversation
Use the UIKit indicator badge factory when ToolbarItem.BadgeText is an empty string so iOS and Mac Catalyst render the documented dot badge instead of a numeric zero badge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35504Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35504" |
|
/review -b feature/regression-check |
🤖 AI Summary
📊 Review Session —
|
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
📱 ToolbarTests ToolbarTests |
✅ FAIL — 12s | ❌ FAIL — 8s |
🔴 Without fix — 📱 ToolbarTests: FAIL ✅ · 12s
Determining projects to restore...
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/TestUtils/src/DeviceTests/TestUtils.DeviceTests.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/Core/tests/DeviceTests.Shared/Core.DeviceTests.Shared.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/TestUtils/src/DeviceTests.Runners/TestUtils.DeviceTests.Runners.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Core/maps/src/Maps.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/Maps/src/Controls.Maps.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Essentials/src/Essentials.csproj::TargetFramework=net11.0]
🟢 With fix — 📱 ToolbarTests: FAIL ❌ · 8s
Determining projects to restore...
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/TestUtils/src/DeviceTests/TestUtils.DeviceTests.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/Core/tests/DeviceTests.Shared/Core.DeviceTests.Shared.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/TestUtils/src/DeviceTests.Runners/TestUtils.DeviceTests.Runners.csproj::TargetFramework=net11.0-android]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Essentials/src/Essentials.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Core/src/Core.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Core/maps/src/Maps.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(188,5): error NETSDK1045: The current .NET SDK does not support targeting .NET 11.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 11.0. Download the .NET SDK from https://aka.ms/dotnet/download [/home/vsts/work/1/s/src/Controls/Maps/src/Controls.Maps.csproj::TargetFramework=net11.0]
/home/vsts/work/1/s/.dotnet/sdk/10.0.100/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.TargetFrameworkInference.targets(130,5): error NETSDK1139: The target platform identifier android was not recognized. [/home/vsts/work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-android]
⚠️ Failure Details
- ❌ ToolbarTests FAILED with fix (should pass)
📁 Fix files reverted (1 files)
src/Controls/src/Core/Compatibility/iOS/Extensions/ToolbarItemExtensions.cs
🧪 UI Tests
Full UI test matrix will run (no specific categories detected from PR changes).
🔍 Pre-Flight — Context & Validation
Issue: N/A (no linked issue — Issues Fixed: N/A in PR body)
PR: #35504 — Fix iOS toolbar empty badge indicator
Author: jfversluis
Head: jfversluis/ios-badge-dot-fix @ e7404cf055e9f37ebd571004ee417be30e94cdea
Platforms Affected: iOS 26+, MacCatalyst 26+
Files Changed: 1 implementation, 1 test (new)
Summary of PR's Fix
In ToolbarItemExtensions.UpdateBadge, the branch handling BadgeText == string.Empty previously
called UIBarButtonItemBadge.Create(0) (creates a numeric badge showing "0"). The PR replaces
that single call with UIBarButtonItemBadge.CreateIndicatorBadge() (creates the documented
dot/indicator badge), matching MAUI's documented contract:
BadgeText = null→ no badgeBadgeText = ""→ indicator dotBadgeText = "<digits>"→ numeric badgeBadgeText = "<text>"→ string badge
Key Findings
- The PR is a 1-line surgical fix in
ToolbarItemExtensions.cs:220-221plus a new device test in
Controls.DeviceTests(Elements/Toolbar/ToolbarTests.iOS.cs). Footer newline restored. - The test guards execution with
IsBarButtonItemBadgeSupported()checking iOS 26 / MacCatalyst 26. - No public API surface change. No XAML surface change. No threading impact (UpdateBadge runs on UI thread).
- The only consumer of
BadgeTextempty-string semantics is this method. - Whitespace-only strings (e.g.,
" ") still flow through thestringbranch and produce a
string badge of whitespace — not normalized to the indicator dot. This may or may not be desired
but is consistent with current behaviour for other branches. - The
UpdateBadgemethod is repeated in theSecondarySubToolbarItemclass (later in the same
file) — confirm whether the same numeric-zero bug exists there and whether the PR should patch it
too.
Code Review Summary
Verdict: LGTM (with a small consideration)
Confidence: medium
Errors: 0 | Warnings: 1 | Suggestions: 1
Key code review findings:
⚠️ CheckSecondarySubToolbarItem.UpdateBadge(same file, later in the file) for the
sameCreate(0)pattern — the fix may need to be applied there too for parity.- 💡 Consider treating
string.IsNullOrWhiteSpace(badgeText) && badgeText is not nullas the
indicator branch so" "doesn't render as a whitespace badge — minor UX nit, not required.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #35504 | Replace Create(0) with CreateIndicatorBadge() in the Length == 0 branch. |
⏳ PENDING (Gate blocked) | ToolbarItemExtensions.cs |
Original PR |
Gate Note
Gate ran on android platform but the PR is iOS-only — and the local environment lacks
the .NET 11 SDK and net11.0-android workloads. Both with-fix and without-fix builds fail
identically with NETSDK1045 (.NET 11 not installed) and NETSDK1139 (android TPI not recognized). The gate result is a tooling/environment limitation, not a signal about the
PR's correctness. Try-Fix candidates below are evaluated by code analysis only.
🔧 Fix — Analysis & Comparison
Try-Fix Aggregate Summary — PR #35504
PR: #35504 — Fix iOS toolbar empty badge indicator
Platform requested for gate: android (mismatched — PR is iOS/MacCatalyst-only)
Gate result: ❌ FAILED (environment blocker: .NET 11 SDK and net11.0-android /
net11.0-ios workloads are not installed locally — both with-fix and without-fix
builds fail identically with NETSDK1045 / NETSDK1139). Try-Fix candidates
evaluated by code analysis only.
Candidates explored
| # | Approach | Different from PR? | Test result | Verdict |
|---|---|---|---|---|
| 1 | Switch-expression refactor of UpdateBadge mapping null/""/digits/text → factory call. |
Structural reorganization only; behaviourally equivalent. | ❎ Worse — adds churn, no behavioural gain. | |
| 2 | Treat string.IsNullOrWhiteSpace(badgeText) as the indicator branch. |
Yes — broadens the empty-only branch to include whitespace-only. | ❎ Worse — violates documented ToolbarItem.BadgeText contract and creates an iOS-only semantic that diverges from Android/Windows handlers. |
|
| 3 | Stop. No further meaningfully different correct alternative exists. | n/a | n/a | ✅ Accept PR as-is. |
Why the search stopped at 3 candidates
The PR is a 2-character production-code change replacing
UIBarButtonItemBadge.Create(0) with UIBarButtonItemBadge.CreateIndicatorBadge()
in the badgeText.Length == 0 branch of
ToolbarItemExtensions.UpdateBadge. The fix:
- Uses the canonical Apple iOS 26 / MacCatalyst 26 factory for the indicator
badge — no other public Apple API achieves this. - Aligns iOS with the documented MAUI contract that "empty string shows a dot
indicator" (seeToolbarItem.BadgeTextXML doc) and with the Android /
Windows implementations that already do this. - Is covered by a new device test that asserts
Badge != null && Badge.StringValue == nullafter transitioning
BadgeTextfrom"3"tostring.Empty, gated on
OperatingSystem.IsIOSVersionAtLeast(26)/IsMacCatalystVersionAtLeast(26).
Alternative shapes are either equivalent (candidate 1) or actively wrong
(candidate 2 — broadens a documented contract on one platform only).
Generating further "variations" would be trivial restylings (e.g., extracting
a helper, swapping Length == 0 for string.IsNullOrEmpty) — explicitly
excluded by the task spec.
Final recommendation to the PR-review report phase
Accept the PR's fix. It is the minimal correct change. The Try-Fix phase
did not find a superior alternative, and the only semantically distinct
alternative (whitespace-normalization) regresses a cross-platform contract.
Environment note for next runs
The gate platform was specified as "android" but PR #35504 only touches an
iOS/MacCatalyst-only file (Compatibility/iOS/Extensions/ToolbarItemExtensions.cs)
and an iOS-gated device test. Future gate runs for this class of PR should use
ios or maccatalyst as the platform, and the host must have the .NET 11
SDK and the corresponding iOS workload installed (net11.0-ios).
📋 Report — Final Recommendation
Comparative Report — PR #35504 Fix Candidates
PR: #35504 — Fix iOS toolbar empty badge indicator
Author: jfversluis
Head: jfversluis/ios-badge-dot-fix @ e7404cf055e9f37ebd571004ee417be30e94cdea
Platform requested for gate: android (mismatched — PR is iOS / MacCatalyst-only)
Gate environment caveat (shared by every candidate)
The gate result was ❌ FAILED, but the failure is environmental, not behavioural: the local runner lacks the .NET 11 SDK and the net11.0-android / net11.0-ios workloads. Both without-fix and with-fix builds fail identically with NETSDK1045 (no .NET 11 SDK) and NETSDK1139 (TPI android not recognized). The same blocker prevented every Try-Fix and the expert-eval sandbox copy from executing the test suite.
Per the autonomous-execution policy, we proceed on code analysis. The "regression tests" axis is therefore unobserved for every candidate — there is no candidate that passed regression tests and no candidate that demonstrably failed them. The ranking rule "candidates that failed regression tests rank lower than those that passed" applies vacuously: no candidate is penalised under it.
Candidates
| ID | Source | Diff vs PR | Test result | Verdict |
|---|---|---|---|---|
pr |
The PR as submitted | — (baseline) | Env-blocked; correct by inspection (uses canonical Apple CreateIndicatorBadge() factory; matches documented MAUI contract; matches Android/Windows behaviour). |
✅ Minimal correct fix. |
pr-plus-reviewer |
PR + actionable expert-reviewer feedback applied | Diff-identical to pr. Both reviewer suggestions were re-validated and discarded: (1) the SecondarySubToolbarItem parity concern is a misread — that class has no UpdateBadge method; (2) whitespace normalization violates the documented cross-platform contract. |
Equivalent to pr. |
✅ Tied with pr; no additional value. |
try-fix-1 |
Switch-expression refactor of UpdateBadge |
Same factory call as pr; just restructures the four-state mapping as a switch expression. |
Env-blocked; behaviourally equivalent to pr by inspection. |
❎ Worse — adds churn (≈15 line delta vs 2-character delta) for zero behavioural gain. Unrelated to the bug being fixed. |
try-fix-2 |
string.IsNullOrWhiteSpace(badgeText) as the indicator branch |
Semantically broader than pr: empty or whitespace renders the indicator. |
Env-blocked. | ❎ Worse — violates the documented ToolbarItem.BadgeText contract ("an empty string to show a dot indicator"); diverges from Android (BadgeUtils) and Windows (InfoBadge) handlers which both key off IsNullOrEmpty/Length == 0. Per-platform semantic divergence is exactly what MAUI handlers must not introduce. |
try-fix-3 |
Stop signal | — | n/a | n/a — explicit stop after observing no further meaningfully different correct approach. |
Ranking
pr(winner) — minimal, surgical, correct. 2-character production-code change against a single well-isolated branch. Backed by a new device test. Uses Apple's documented public factory for the iOS 26 / MacCatalyst 26 indicator badge.pr-plus-reviewer— diff-identical topr; ranks equal but offers no additional value, so we prefer the PR's existing commits.try-fix-1— equivalent behaviour, more churn. Reject.try-fix-2— incorrect contract change. Reject.try-fix-3— explicit stop, not a candidate.
Why pr wins
- Minimal blast radius. Replaces one factory call with the canonical Apple one — touches no other code path, no other platform, no public API.
- Documented contract alignment. Implements "empty string → dot indicator" on iOS to match the public XML doc on
ToolbarItem.BadgeTextand the existing Android/Windows behaviour. Eliminates a per-platform divergence rather than introducing one (whichtry-fix-2would). - Test coverage. Adds a device test asserting the post-transition shape (
Badge != null && Badge.StringValue == null), correctly gated onOperatingSystem.IsIOSVersionAtLeast(26)/IsMacCatalystVersionAtLeast(26). - No reviewer-actionable issues. Expert review found 0 errors, the single
⚠️ warning was a misread (noUpdateBadgeexists onSecondarySubToolbarItem), and the single 💡 suggestion was independently explored astry-fix-2and rejected on contract grounds.
Recommendation
Accept PR #35504 as-is. No additional commits required. The Try-Fix phase and expert reviewer both converged on the same conclusion: the PR is the minimal correct change.
|
@jfversluis looks good |
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 of Change
On iOS 26 and Mac Catalyst 26,
ToolbarItem.BadgeText = string.Emptyshould render the documented empty indicator badge dot, whilenullhides the badge and non-empty values render badge content. The existing net11.0 implementation mapped the empty string toUIBarButtonItemBadge.Create(0), which creates a numeric zero badge instead of the indicator badge.This updates the empty-string branch to use
UIBarButtonItemBadge.CreateIndicatorBadge()and keeps the existing null, numeric, string,BadgeColor, andBadgeTextColorbehavior unchanged. It also adds focused iOS/MacCatalyst device coverage that verifies updating an existing numeric badge to an empty string produces a badge with no string value on supported OS versions.Validation:
dotnet test src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj -f net11.0 -p:TargetFrameworks=net11.0 --no-restore --filter FullyQualifiedName~ToolbarItemBadgeTestsdotnet build src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj -f net11.0-ios -p:TargetFrameworks=net11.0-ios -p:RuntimeIdentifier=iossimulator-arm64 -p:IncludeIosTargetFrameworks=true -p:IncludeAndroidTargetFrameworks=false -p:IncludeMacCatalystTargetFrameworks=false -p:IncludeWindowsTargetFrameworks=false -p:IosTargetFrameworkVersion=26.2 -p:IosTargetFrameworkVersionSdkDefault=26.2 -p:NoWarn=CS1591%3BCS8622%3BCA1416%3BCA2200 --no-restore -v:minimalIssues Fixed
N/A