Repository navigation
[iOS] Fix for ScrollView SafeAreaEdges="Container" Double Safe-Area Reservation Causing Phantom Scroll Range - #37765
Conversation
### Description of Change This pull request makes a minor update to the UI test pipeline configuration - Pipeline configuration simplification: * Removed the **duplicate** code that set the `$deviceType` variable and appended the `--skin` argument to the test command in `eng/pipelines/common/ui-tests-steps.yml`. The same code was already added at [line 133](https://github.com/dotnet/maui/blob/main/eng/pipelines/common/ui-tests-steps.yml#L133) as part of [PR dotnet#33409](dotnet#33409). The duplicate code was also added at [line 156](https://github.com/dotnet/maui/blob/main/eng/pipelines/common/ui-tests-steps.yml#L156) as part of [PR dotnet#31631](dotnet#31631).
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change This follow-up repairs the skill-evaluation failures exposed by dotnet#36799 and hardens the credentialed Vally evaluator so skill regressions, infrastructure failures, incomplete trials, parser differences, and reporting-side failures are classified accurately. ### Root Causes - `try-fix-capabilities` could exceed its numeric threshold while remaining red because Vally independently hard-fails execution errors and incomplete trials. Aggregate score never overrides those vetoes. - `pr-review-gh-auth` routed correctly, but its isolated fixture omitted the phase-specific unauthenticated public-GitHub fallback required by the grader. - Later failures exposed evaluator defects rather than skill regressions: PAT authentication did not prove model entitlement, model discovery did not initially match Vally 0.12's effective execution plan, matrix-only token offsets removed run-level rotation entropy, and parallel legs amplified shared PAT contention. - Run `31518503277` restored the synthetic target correctly, but one agent deleted Vally's pre-existing untracked `try-fix/` input to force an empty `git status`; Vally correctly treated that deletion as workspace drift. - Run `31524806025` completed every evaluation successfully, but a transient GitHub API fetch failure occurred before the comment step exported its already-computed verdict, making reporting red despite green suites. - Review found parser-semantic splits between Psych and Vally's pinned `yaml@2.9.0`: duplicate keys could be first-wins in AST policy checks but last-wins at runtime, while YAML merge keys/tags were interpreted differently and could hide executable evaluator fields from trusted validation. ### Changes - Replaced unreliable operational probes with bounded fixtures while preserving repeated trials, behavioral rubrics, strict thresholds, and hard-veto semantics. - Added the unauthenticated GitHub fallback fixture and deterministic skill-invocation/tool-call graders. - Upgraded to Vally 0.12 and made aggregation fail closed on execution errors, incomplete/skipped stimuli, malformed output, invocation failures, and missing mandatory coverage. - Added trusted fixture preparation plus path, symlink, Git, environment, executable, attachment, hook, settings, MCP, executor, and repository-control isolation. - Serialized matrix legs while retaining three trials per stimulus. - Made model discovery use only executed `eval*.vally.yaml` specs and Vally 0.12's effective executor and prompt-grader fields; implicit prompt-grader fallbacks are rejected. - Added model-aware PAT selection with run/rerun/matrix-distributed rotation and exact sentinel probes through the pinned evaluator runtime; reused it in the advisory soak. - Clarified that `try-fix` must preserve every pre-existing untracked path, including evaluator-loaded skill inputs. Trusted validation enforces both the skill contract and restoration fixture instruction while retaining `diff-empty` as the method-independent veto. - Exported the authoritative evaluation verdict before PR-comment API calls, added bounded API retries, and made comment publication advisory so transient GitHub failures cannot erase or invert the commit status. - Reduced soak workflow permissions to `contents: read` and made `/user` transport failures explicit without exposing PATs. - Made required-stimulus policy fail closed when a configured stimulus is renamed or removed, and bound each fixture marker/ref to the YAML stimulus that owns it so a correctly named decoy cannot satisfy the policy. - Parse each eval spec once and reject duplicate semantic mapping keys, textual YAML merge keys, and normalized explicit merge tags recursively before loading. This prevents Psych/Vally parser disagreement from bypassing prompt, ref, repository-control, or executable-environment validation. - Limited frozen-worktree test detection to full immutable commit IDs; ordinary explicit branch bases continue using authoritative PR file metadata. - Added deterministic regressions for model planning, PAT distribution and transport errors, deleted evaluator controls, host-only XAML invocation, evaluator-owned input preservation, required-stimulus rename/decoy/parser bypasses, branch-versus-immutable test detection, and reporting-side verdict preservation. ### Validation Current head: `fd8f3d543e1a1eb90e8432a0cbae07a18b52dbdd`. - Exact-head [Skill Validation run 31807806898](https://github.com/dotnet/maui/actions/runs/31807806898), [`maui-pr` build 1553690](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1553690), Build Analysis, CLA, static checks, Pester, and the hermeticity gate are green. - All evaluated suites passed. Key suites: `try-fix-restore-protocol` **1.00 / 1.00**, `try-fix-capabilities` **0.93 / 0.80**, `pr-review-gh-auth` **1.00 / 0.80**, and `verify-tests-fail-without-fix-capabilities` **0.94 / 0.87**. - Local trusted evaluator/reporting harness: **70 tests, 306 assertions, 0 failures/errors/skips**. - All **16** Vally specs pass strict lint with pinned `@microsoft/vally-cli@0.12.0`. - All **8** skill test directories pass trusted validation and resolve the effective plan to `gpt-5.6-sol` plus `claude-opus-5`; non-eval soak/hermeticity specs do not contaminate matrix selection. - Ruby syntax, Bash syntax, ShellCheck, workflow YAML parsing, Pester (**8 passed**), and `git diff --check` pass. - Final whole-diff review found no high-confidence correctness or security blockers; all review threads are resolved. - `pull_request_target` executes trusted workflow/helper code from default `main`; candidate selector/reporting helper behavior is therefore covered deterministically here and becomes the credentialed trusted path after merge. ### Issues Fixed Follow-up to dotnet#36799. --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vally Fixture <vally-fixture@example.invalid> Copilot-Session: b9f7de2c-86d7-47cd-8ac8-aeff57d0b0f3 Copilot-Session: 806beb89-5041-4c7b-bcbf-998d6fbab8dc
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change Extend the existing `release-readiness` skill with deterministic, copy-ready Preview and servicing-release handoff generation from readiness JSON plus separately verified public evidence. - Keep missing or unverified release facts as explicit `TBD` values and enforce public-safe URLs, structured metadata, NuGet configuration, and Markdown output. - Correct Preview workload installation guidance with package-source mapping and the prior-major public compatibility feed while keeping workload-set packages isolated to `dotnet-workloads`. - Add focused regression coverage and run it in the release-readiness validation workflow. The renderer is a projection of the existing readiness engines rather than a second survey, so Preview and SR semantics remain distinct while sharing one public handoff format. ### Safety and Architecture - Public output requires an explicit JSON boolean `PublicEvidence: true`. - The renderer rebuilds NuGet XML from an allow-list and accepts only public `dnceng/public` Azure Artifacts feeds or the exact NuGet.org service index. - Public evidence URLs, structured build metadata, timestamps, Markdown, local paths, credential-shaped values, and owner/sign-off prose are validated or omitted. - Workload-set packages map only to `dotnet-workloads`; component and compatibility packages resolve through their appropriate public feeds. ### What Not to Do - Do not use the handoff renderer as a second readiness survey or let it select a newer build implicitly. - Do not copy private source-page content, credentials, owners, or sign-offs into public evidence. - Do not map unrelated component packages to `dotnet-workloads`; missing upstream packages can otherwise surface as misleading authorization failures. ### Validation - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseHandoff.ps1` - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 -SkipE2E` ### Issues Fixed N/A --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3db5742d-7e40-4a23-a3ea-de3bc3f0848e
…t#36568) <!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause MAUI's release flow implemented NuGet.org authentication, retries, conflict handling, quota rotation, and publishing in `eng/scripts/push_nuget_org.ps1`. Gather and push failures could also continue without failing the release. The repository additionally carried a duplicate internal release YAML. ### Description of Change This makes `eng/pipelines/ci-official-release.yml` the canonical release pipeline and moves package publication to the approved 1ES release path: - Resolve the BAR build once and run one fail-fast `darc gather-drop`, limited to BAR NuGet package assets rather than unrelated symbol/blob assets. - Preserve package include/exclude behavior and validate non-empty workload-pack and workload-manifest sets. - Produce separate SBOM-backed `MauiPacksForNuGet` and `MauiManifestsForNuGet` artifacts. - Publish from production `releaseJob` jobs with `1ES.PublishNuget@1`, `useDotNetTask: false`, and the protected `nuget.org (dotnetframework)` service connection. - Require workload packs to publish and become resolvable before manifest approval is available. - Remove `maui-release-internal.yml`, `push_nuget_org.ps1`, raw API-key inputs, and the arbitrary feed URL. - Document the canonical release and recovery procedure. No public API changes are involved. ### Duplicate and Partial-Publish Recovery Internal build [3050066](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050066) proved that the service connection authenticates, but also proved that the external-feed-backed task fails on HTTP 409 even with `allowPackageConflicts: true`. Task-level retries are unsafe after a partial batch because the retry can immediately encounter duplicates created by the first attempt. The final implementation therefore: - Generates an `expected-packages.json` identity manifest for each immutable artifact. - Rejects duplicate package identities, duplicate package file names, and unexpected `.nupkg` files before publication. - Verifies the copied helper's SHA-256 before execution. - Filters packages already visible through NuGet.org's flat-container API before publishing. - Uses separate recovery-only `nugetAlreadyAttemptedPackFilters` and `nugetAlreadyAttemptedManifestFilters` parameters for packages accepted but not yet visible during NuGet.org validation lag. - Runs `1ES.PublishNuget@1` only when packages remain and does not use task-level retries. - Verifies the complete expected package set with transient-query retries and a 30-minute deadline. ### Internal Validation - Internal branch: [`copilot/replace-custom-nuget-publishing`](https://dev.azure.com/dnceng/internal/_git/dotnet-maui?version=GBcopilot%2Freplace-custom-nuget-publishing) - Final internal commit: [`adf2ad774cd3a2092450df153e6b46a09a127552`](https://dev.azure.com/dnceng/internal/_git/dotnet-maui/commit/adf2ad774cd3a2092450df153e6b46a09a127552?refName=refs%2Fheads%2Fcopilot%2Freplace-custom-nuget-publishing) - Build 3050066 confirmed the registered `dotnet-maui-release` definition already executes the canonical YAML, so no pipeline-definition migration is needed. - Build [3050237](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050237) showed that a fail-fast unfiltered gather stops on a missing Foldable symbol blob even though the shipping NuGet package downloads successfully. The gather now uses Darc's slash-free package-asset convention to exclude symbol/blob paths while remaining fail-fast for every publishable package. - Build [3050279](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050279) confirmed the package-only gather succeeds and the `nuget.org (dotnetframework)` connection authenticates: it published `Microsoft.AspNetCore.Components.WebView.Maui.10.0.100`, then NuGet.org rejected `Microsoft.AspNetCore.Components.WebView.Maui.Msi.arm64.10.0.100` with HTTP 403. The first package lists `dotnetframework` as an owner; the MSI package lists only `Microsoft` and `Xamarin`. This is a NuGet.org package-ownership/service-connection blocker, not a pipeline task-wiring failure. - Pester 5.9.0: 1,668 tests passed, including 15 package-filtering/retry tests and 3 release-pipeline regression tests. - PowerShell parsing, YAML parsing, documentation spellcheck, and whitespace validation pass. Before production use, either the service-connection identity must be granted ownership of every MAUI package (including Xamarin-owned MSI packages), or the release pipeline must be given an approved Xamarin-scoped NuGet.org service connection. Once that external credential issue is resolved, rerun the same commit and BAR selection; the recovery logic will omit the package already published by build 3050279 and continue with the remaining set. ### What Not to Do - Do not rely on `allowPackageConflicts` for an external NuGet.org feed; build 3050066 proved it does not make 409 responses successful. - Do not add task-level retries around a multi-package publish; they are not safe after partial publication. - Do not restore broad `--continue-on-error` gather behavior; exclude non-release symbol/blob assets and keep publishable package gathering fail-fast. - Do not gather a different BAR drop while recovering a partial release; rerun the same commit and selection filters. - Do not exclude the Xamarin-owned MSI packages to make the build green; they are shipping assets and require a credential with matching NuGet.org ownership. ### Issues Fixed Fixes dotnet#36567 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com> Co-authored-by: Rui Marinho <me@ruimarinho.net> Copilot-Session: 150fe76a-707e-48ca-9e9d-38bfccead829
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change Enhance local net11 preview release-readiness runs with automatic health checks for the internal official `dotnet-maui` Azure DevOps pipeline (definition 1095, `dnceng/internal`). The local release-readiness skill/agent explicitly requests enriched output, then independently discovers builds for `refs/heads/net11.0` and the evaluated release branch when it exists. It requests a bounded, queue-time-ordered five-build window and prefers the newest exact-HEAD build, then scans in queue order while skipping candidates proven stale and buffering indeterminate evidence. Disagreeing possible outcomes remain UNKNOWN; a later proven-current failure remains RED only when every buffered candidate is also a completed failure/cancellation. A terminal window containing only same-branch failed/canceled indeterminate builds is `failed-or-stale` and BLOCKED because every possible build is either red or stale. The check reports build status, ID/number, source SHA, and URL. Current successful builds are green; failures/cancellations are red; partial success is a WATCH state requiring manual review; running builds are in progress; builds behind the newest trigger-eligible commit are stale; missing or malformed evidence is unknown. When branch HEAD advances, bounded local Git evaluates exact first-parent history and merge-result changes against `eng/pipelines/ci-official.yml`. Successful no-op or excluded-only advances remain current, while add/revert and merge-result-only source changes still require a newer official build. A conclusive Git non-ancestor result is stale; unavailable Git evidence remains unknown unless every possible same-branch result is independently blocking. ### Public/internal boundary Direct script output remains public-safe by default. GitHub Actions skips the internal query before invoking Azure CLI, while the local skill/agent explicitly passes `'-PublicSafe:$false'` for enriched artifacts that remain local. Public-safe output omits internal branch records, build IDs/numbers, SHAs, URLs, and the local-only table. Missing local Azure authentication fails open. Partial failures preserve successful branch evidence, so known red/stale results cannot disappear. Azure, GitHub, and local Git subprocesses have bounded execution; process exit and redirected-output draining share the same deadline. Local Git runs from the explicit repository root, performs a bounded fetch of only the evaluated branch when objects are absent, and maps unavailable currency evidence through the outcome lattice rather than assuming current or stale. Windows `.cmd`/`.bat` Azure CLI launchers run through `ComSpec` with structured arguments. Manual build overrides are accepted only when the build belongs to definition 1095, and Azure-controlled build numbers are constrained to the expected numeric format before entering agent-facing output. ### Tests - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1` — 2,276 passed, 0 failed - PowerShell parser validation for the helper, preview engine, and test harness - Live local smoke successfully queried both requested refs through the enriched local path - First-parent empty-commit, same-tree merge, merge-result source change, add/revert, non-ancestor, missing-object, exact/proven-current candidate selection, old-SHA retry, uncertain-failed/older-green UNKNOWN, uncertain-failed/older-failed RED, blank-source-SHA/canceled certainty, terminal failed-or-stale Details/NextAction, public-safe generic BLOCKED rendering, explicit-CWD, Windows launcher, trigger-exclusion, exact-path, timeout, auth-ordering, malformed-output, and public-redaction fixtures - GitHub Actions/public-safe smoke confirms the internal fetcher is not invoked and internal coordinates remain absent ### Design tradeoffs - Azure CLI remains the local adapter so public automation gains no credentials or SDK dependency. - A five-build, server-ordered window keeps the query bounded; selection skips conclusively stale candidates, buffers uncertainty, and reports only outcomes common to every possible currency result. - Certain terminal blocking uses a distinct `failed-or-stale` classification rather than falsely claiming the selected build is current; local guidance restores currency evidence before choosing failure repair versus a current-HEAD rerun, while public-safe output remains generic. - Local Git inspects exact first-parent commit and merge-result paths rather than an aggregate compare diff, preventing second-parent history and net-zero merges from producing false stale results while preserving add/revert evidence. - Missing local objects trigger one bounded fetch of the evaluated ref; unresolved evidence remains UNKNOWN unless every possible outcome blocks, while a conclusive non-ancestor result is stale. - Total authentication unavailability is skipped; partial access loss retains observed branch evidence. - `-InternalBuildId` remains a diagnostic override, but normal local skill/agent runs use branch-based discovery. ### Issues Fixed N/A --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e39d9d4-19e4-45f5-80f1-2bb3035c18c9
Removes the repository-owned VG20 import and synchronizes the relevant Arcade credential-removal changes on `main`. Tracked by https://dev.azure.com/dnceng/internal/_workitems/edit/12131 Copilot-Session: e890b71a-c1aa-416c-a15c-be8da9fdd9b4
### Description of Change Replace MAUI's long-lived `dn-bot-ceapex-package-r` dependency with a short-lived Entra token for OneLocBuild package restores. - Acquire the Azure DevOps token through the existing DevDiv `dnceng-onelocbuild-ceapex` workload-identity service connection. - Reuse Arcade's shared `get-federated-access-token.yml` helper. - Pass the resulting secret `CeapexEntraToken` to OneLocBuild for the ceapex LCL/LocTools feeds. - Keep the existing GitHub PR authentication unchanged. The DevDiv service connection is configured for workload identity federation and MAUI pipeline 13330 is authorized to use it. ### Validation - Parsed the updated pipeline as YAML. - Verified the shared federated-token helper exists on this branch. - Verified the legacy ceapex PAT is no longer referenced by MAUI's repo-owned localization pipeline. ### Issues Fixed Tracks [dnceng work item 10151](https://dev.azure.com/dnceng/internal/_workitems/edit/10151). Copilot-Session: 80f6a0c6-7947-4200-bb78-7f0fc01d671a
…atic event with WeakEventManager (dotnet#37645) <!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details When a page or view model subscribes to BindingDiagnostics.BindingFailed (a helpful event that tells you when a data binding fails) and forgets to unsubscribe, that page never gets cleaned up from memory. Every page it holds — plus its images, view models, and data — stays alive until the app closes. Over time, the app uses more and more memory. ### Root Cause BindingFailed is a static event, which means it lives for the whole life of the app. Normal C# events keep a strong reference to whoever subscribed, so as long as the event is alive, its subscribers stay alive too. Since this event never goes away, any subscriber that doesn't call -= is stuck in memory forever. ### Description of Change Switched BindingFailed from a normal event to one backed by WeakEventManager, which keeps only a weak reference to subscribers — so the garbage collector can clean them up even if someone forgets to unsubscribe. Validated the behavior in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes dotnet#37245 ### Fix reference [maui/src/Controls/src/Core/Application/Application.cs at bf62313 · …](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/Application/Application.cs#L263) [maui/src/Controls/src/Core/Command.cs at bf62313 · dotnet/maui](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/Command.cs#L128) [maui/src/Controls/src/Core/ImageSource.cs at bf62313 · dotnet/maui](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/ImageSource.cs#L175) ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/86c6d76d-a051-458b-beb3-6c254545f87a" >| <video src="https://github.com/user-attachments/assets/e3b02ce7-71a7-4e90-94e0-dd6587c8e3cf">|
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary - regenerate all 14 Copilot-based agentic workflows with gh-aw v0.86.2 - stage the installed Copilot CLI in a portable runner-local path instead of invoking `/usr/local/bin/copilot` - explicitly disable shell access in the automatic issue/PR labeler, as required when `min-integrity: none` - adopt gh-aw v0.86.2's intentional disabled-expiry sentinel for action-failure issues when no maintenance workflow exists ## Root cause The failing runs install Copilot CLI successfully, but gh-aw v0.85.4 then invokes `/usr/local/bin/copilot`. That path no longer exists on the hosted runner, so the engine exits with `ENOENT` before producing output or making a model request. gh-aw v0.86.2 includes the upstream path-portability fix and resolves the installed executable from `PATH`. The same generated path affected every Copilot workflow, including the labeler, whose failure reporting is intentionally disabled. Regeneration also incorporates [github/gh-aw#51425](github/gh-aw#51425). Because this repository does not generate an `agentics-maintenance.yml` consumer for the implicit action-failure expiry, all 14 lockfiles now set `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` to `"0"`. In gh-aw v0.86.2, explicit `0` means expiration is disabled; it prevents unenforceable expiry markers and does not make failure issues expire immediately. ## Validation - `gh aw compile --strict --validate --actionlint --no-emit` - verified all 14 lockfiles use gh-aw v0.86.2 and portable Copilot staging - verified no generated workflow references `/usr/local/bin/copilot` - verified all 14 generated locks use gh-aw's `"0"` disabled-expiry sentinel consistently No existing open MAUI PR addresses this gh-aw/Copilot path failure. Fixes dotnet#37430 Fixes dotnet#37431 Fixes dotnet#37432 Fixes dotnet#37435 Fixes dotnet#37442 --------- Co-authored-by: Copilot CI <copilot-ci@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74cff858-a5a6-486e-a1d3-1d516fc3bdaa Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description Adds .NET MAUI 10.0.100 and 11.0.0-preview.7 to both version selectors in the bug-report issue form. This lets users report issues against the released SR10 build and the active .NET 11 Preview 7 version. ---------
…net#37686) <!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Problem The internal `dotnet-maui` official build currently fails to compile the pipeline YAML, before any stage runs: ``` /eng/pipelines/common/variables.yml (Line: 81, Col: 7): Expected at least one key-value pair in the mapping ``` ### Cause dotnet#37589 removed `- group: Publish-Build-Assets`, but that group was the only child of the conditional that enclosed it, leaving an empty block behind: ```yaml - group: AzureDevOps-Artifact-Feeds-Pats - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: # <- nothing under it ``` Azure DevOps requires a conditional insertion block to contain at least one entry, so template expansion fails outright. ### Why PR validation stayed green The empty block is nested inside an outer guard: ```yaml - ${{ if or(eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: ``` For the public `maui-pr` definition that outer condition is false, so Azure DevOps short-circuits and never expands the inner block. The breakage is therefore invisible to PR CI and only manifests on the internal official build. ### Fix Remove the empty conditional. No file in the repository references the `Publish-Build-Assets` variable group any more, so it is deleted outright rather than repopulated. ### Validation - `eng/pipelines/common/variables.yml` parses as valid YAML. - Swept all 39 YAML files under `eng/pipelines/` for conditional blocks with no body — this was the only one. - `grep` for `Publish-Build-Assets` across `eng/` and `.github/` returns no remaining references. This blocks the internal build entirely, so it is worth merging ahead of larger infra work such as dotnet#36257 (which carries the same one-line fix, but cannot be validated on the internal pipeline until this is on `main`). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Updates the dotnet tool version.
…le with keyboard waits (refs dotnet#37556) (dotnet#37741) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! Workflow artifact: ci-fix Artifact kind: deflake Refs: dotnet#37556 Target branch: main Attempt: 1/10 ## Attempt 1 of 10 Synchronize the test with actual keyboard visibility before validating the keyboard-dependent safe-area layouts. ### Previous attempts (prior commits on this PR) None. This attempt differs by: this is the first attempt. ## Root cause The latest `main` build 1563072 failed `SafeAreaNoWhiteSpaceAfterKeyboardDismissAndEdgeToggle` on run 0 because `ContentGrid` had not shrunk after tapping the entry, then passed on built-in retry run 1. The test immediately queried layout after the tap and immediately changed edges after dismissing the keyboard, without first proving either keyboard transition completed. Flake class: test-quality ## Why this was flaky `Issue28986_ContentPage.SafeAreaNoWhiteSpaceAfterKeyboardDismissAndEdgeToggle` retried its layout assertion but had no synchronization point confirming that Android had shown the keyboard. It also dismissed the keyboard and immediately restored all safe-area edges. Depending on keyboard animation timing, the assertions could observe the pre-transition layout; build 1563072 demonstrates the intermittent fail-then-pass behavior. ## De-flake The test now asserts `WaitForKeyboardToShow()` before measuring the keyboard-visible layout and `WaitForKeyboardToHide()` before restoring all safe-area edges. Every existing layout assertion remains unchanged. No timeout was increased, no retry attribute was added, and no assertion was weakened. ## Fix Add the existing Appium keyboard-state waits at the two transition boundaries so the test evaluates the same behavior in deterministic UI states. ## What is unverified / where I need help - The Android UI test could not be run because this runner has no device rig. - Please confirm the API 36 SafeAreaEdges leg consistently observes both transitions and preserves the intended edge-toggle sequence. ## Validation - Command: `not run because no Android device rig is available` - Result: not run ## Evidence - Original failing build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1554582 - Latest verified-failing build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1563072 --- Filed by [`ci-status-fix`](https://github.com/dotnet/maui/blob/main/.github/workflows/ci-status-fix.md). This is the single PR for dotnet#37556: the workflow watches its own CI and pushes up to **10 attempts on this same PR** (it never opens a second PR). It advances only when the fix's own build settles red and that red is caused by the fix. Comments, reviews, and commits do not transfer ownership; the loop remains autonomous until this PR is closed. Eligible `CHANGES_REQUESTED` reviews are handled through Track C. After 10 attempts it stops and defers to humans. In round 1 a maintainer still needs to comment `/azp run maui-pr` (plus the gated uitests/devicetests legs when relevant) to exercise each new commit. > [!WARNING] > <details> > <summary>Firewall blocked 1 domain</summary> > > The following domain was blocked by the firewall during workflow execution: > > - `dc.services.visualstudio.com` > > To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "dc.services.visualstudio.com" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [CI Failure Fixer (main)](https://github.com/dotnet/maui/actions/runs/32573008235) · gpt56 · 446.8 AIC · ⌖ 82.9 AIC · ⊞ 31.1K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+ci-status-fix%22&type=pullrequests) <!-- gh-aw-agentic-workflow: CI Failure Fixer (main), engine: copilot, model: gpt-5.6-sol, id: 32573008235, workflow_id: ci-status-fix, run: https://github.com/dotnet/maui/actions/runs/32573008235 --> <!-- gh-aw-workflow-id: ci-status-fix --> <!-- gh-aw-workflow-call-id: dotnet/maui/ci-status-fix --> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…otnet#37557) (dotnet#37742) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! Workflow artifact: ci-fix Artifact kind: deflake Refs: dotnet#37557 Target branch: main Attempt: 1/10 ## Attempt 1 of 10 Synchronize `SafeAreaPerEdgeValidation` with actual keyboard visibility before validating keyboard-dependent safe-area layouts. ### Previous attempts (prior commits on this PR) None. This attempt differs by: this is the first attempt. ## Root cause The latest `main` build 1563072 failed `Issue28986.SafeAreaPerEdgeValidation` on run 0 because `ContentGrid` remained at height 2789 after tapping the entry, then passed on built-in retry run 1. The test immediately queried layout after the tap without proving that Android had shown the keyboard. Flake class: test-quality ## Why this was flaky `Issue28986.SafeAreaPerEdgeValidation` retried its layout assertion but had no synchronization point confirming that Android had completed the keyboard transition. Depending on keyboard animation timing, the assertion could poll the unchanged pre-keyboard layout; build 1563072 demonstrates the intermittent fail-then-pass behavior. ## De-flake The test now asserts `WaitForKeyboardToShow()` before measuring the keyboard-visible layout and `WaitForKeyboardToHide()` before validating the restored layout. Every existing layout assertion remains unchanged. No timeout was increased, no retry attribute was added, and no assertion was weakened. ## Fix Add the existing Appium keyboard-state waits at the two transition boundaries so the test evaluates the same behavior in deterministic UI states. ## What is unverified / where I need help - The Android UI test could not be run because this runner has no device rig. - Please confirm the API 36 SafeAreaEdges leg consistently observes both transitions. ## Validation - Command: `not run because no Android device rig is available` - Result: not run ## Evidence - Original failing build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1554582 - Latest verified-failing build: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1563072 --- Filed by [`ci-status-fix`](https://github.com/dotnet/maui/blob/main/.github/workflows/ci-status-fix.md). This is the single PR for dotnet#37557: the workflow watches its own CI and pushes up to **10 attempts on this same PR** (it never opens a second PR). It advances only when the fix's own build settles red and that red is caused by the fix. Comments, reviews, and commits do not transfer ownership; the loop remains autonomous until this PR is closed. Eligible `CHANGES_REQUESTED` reviews are handled through Track C. After 10 attempts it stops and defers to humans. In round 1 a maintainer still needs to comment `/azp run maui-pr` (plus the gated uitests/devicetests legs when relevant) to exercise each new commit. > [!WARNING] > <details> > <summary>Firewall blocked 1 domain</summary> > > The following domain was blocked by the firewall during workflow execution: > > - `dc.services.visualstudio.com` > > To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "dc.services.visualstudio.com" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [CI Failure Fixer (main)](https://github.com/dotnet/maui/actions/runs/32573008235) · gpt56 · 446.8 AIC · ⌖ 82.9 AIC · ⊞ 31.1K · [◷](https://github.com/search?q=repo%3Adotnet%2Fmaui+%22gh-aw-workflow-id%3A+ci-status-fix%22&type=pullrequests) <!-- gh-aw-agentic-workflow: CI Failure Fixer (main), engine: copilot, model: gpt-5.6-sol, id: 32573008235, workflow_id: ci-status-fix, run: https://github.com/dotnet/maui/actions/runs/32573008235 --> <!-- gh-aw-workflow-id: ci-status-fix --> <!-- gh-aw-workflow-call-id: dotnet/maui/ci-status-fix --> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…feAreaEdges="Container" double-reserves the safe area on iOS in MauiScrollView.cs.
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37765Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37765" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR fixes iOS ScrollView phantom scrolling caused by duplicate safe-area reservation and adds regression coverage for issue #36800.
Changes:
- Avoids manual content-size padding when UIKit applies adjusted insets.
- Adds regression assertions and an iOS diagnostic host page.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Review summary |
|---|---|
src/Core/src/Platform/iOS/MauiScrollView.cs |
Critical (1 vote): Update the existing content-size test for the new no-double-reservation contract. |
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36800.cs |
Adds regression assertions for issue #36800. |
src/Controls/tests/TestCases.HostApp/Issues/Issue36800.cs |
Moderate (2 votes): Format the Phantom diagnostic using invariant culture to prevent locale-dependent parsing. |
Suppressed comments (4)
src/Controls/tests/TestCases.HostApp/Issues/Issue36800.cs:62
- This diagnostic does not prove that the changed branch was exercised: it never checks that
AdjustedContentInsetis non-zero or that the native behavior isAlways. If UIKit reports a zero system inset for this small-content fixture, both the old code and the new code take theSystemAdjustedContentInset == Zeropath and the test can pass after reverting the fix. Include the native behavior/inset in the result and fail or otherwise mark the run when the active-inset setup was not reached, or use a fixture that guarantees that state.
double phantom = (native.ContentSize.Height + native.AdjustedContentInset.Top + native.AdjustedContentInset.Bottom) - native.Bounds.Height;
src/Controls/tests/TestCases.HostApp/Issues/Issue36800.cs:7
MauiScrollViewis the UIKit implementation shared by iOS and MacCatalyst (the same file already contains a MacCatalyst-specific branch), but this new page is markedPlatformAffected.iOSand the companion test is guarded by#if IOS. Thus the changed safe-area path has no MacCatalyst regression coverage. Either include MacCatalyst in the page/test scope and UIKit guards, or document why this shared behavior is intentionally excluded.
[Issue(IssueTracker.Github, 36800, "ScrollView with SafeAreaEdges=\"Container\" reserves the safe area twice, causing phantom scroll range", PlatformAffected.iOS)]
src/Controls/tests/TestCases.HostApp/Issues/Issue36800.cs:31
- The repro only uses content that fits in the viewport, so it checks the small-content phantom range but not the tall-content case described by the fix. The changed branch also determines the native size while content is scrollable; add a tall-content scenario that reaches the bottom and validates the maximum offset so dead over-scroll regressions are caught.
// Small content that comfortably fits within the viewport - the ScrollView
// should NOT be scrollable at all when the bug is fixed.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36800.cs:1
MauiScrollView.csunderPlatform/iOSis shared by iOS and MacCatalyst (the class also contains anOperatingSystem.IsMacCatalyst()branch), so this changed content-size path affects both platforms.#if IOSprevents this regression test from compiling in the Mac test project, and the host page is tagged onlyPlatformAffected.iOS; please add MacCatalyst coverage or document a concrete technical reason this UIKit path cannot reproduce there.
#if IOS // Fix is specific to iOS's ContentInsetAdjustmentBehavior/AdjustedContentInset handling; not applicable on Android/Catalyst/Windows
| // range should be zero. When the safe area is reserved twice, this value | ||
| // is positive and equals the safe-area thickness. | ||
| double phantom = (native.ContentSize.Height + native.AdjustedContentInset.Top + native.AdjustedContentInset.Bottom) - native.Bounds.Height; | ||
| _diagLabel.Text = $"Phantom={phantom}"; |
| } | ||
| } | ||
| else if (ContentInsetAdjustmentBehavior != UIScrollViewContentInsetAdjustmentBehavior.Automatic) | ||
| else if (SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentBehavior.Never) |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 4 findings
See inline comments for details.
| } | ||
| } | ||
| else if (ContentInsetAdjustmentBehavior != UIScrollViewContentInsetAdjustmentBehavior.Automatic) | ||
| else if (SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentBehavior.Never) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Regression Prevention — This new condition inverts the contract asserted by the existing device test ContentSizeExpandsToViewport (src/Controls/tests/DeviceTests/Elements/ScrollView/ScrollViewTests.iOS.cs:34), and that test is not updated in this PR.
That test builds a ScrollView with SafeAreaEdges.Container (→ ContentInsetAdjustmentBehavior.Always) and asserts:
SafeAreaInsets.InsetRect(rect(0,0,ContentSize)).Height == AdjustedContentInset.InsetRect(Bounds).Height
With Always and a zero ContentInset, AdjustedContentInset == SafeAreaInsets, so the assertion reduces to ContentSize.Height == Bounds.Height.
- Before this change, the
else ifwasContentInsetAdjustmentBehavior != Automatic, which is unconditionally true in theelsearm, soAlwaysfell through to the padding and producedContentSize.Height = (Bounds.H - safeArea.V) + safeArea.V = Bounds.H→ assertion holds. - After this change,
Always+ non-zeroSystemAdjustedContentInsetskips the padding, soContentSize.Height = Bounds.H - safeArea.V→ the assertion becomesBounds.H - 2*safeArea.V == Bounds.H, which fails for any non-zero vertical safe area.
Concrete scenario: the device-test run executes ContentSizeExpandsToViewport on a simulator/device where the scroll view's SafeAreaInsets are non-zero (notched iPhone, or iPad with a status-bar inset) — the test fails. It only stays green if the test host's scroll view happens to have zero safe area insets, in which case it is passing vacuously and never covered this behavior at all.
The two behaviors cannot both be correct. Please either update ContentSizeExpandsToViewport to encode the new intended contract (content size equals the viewport, i.e. ContentSize.Height == Bounds.Height - AdjustedContentInset.Vertical) with a comment explaining the change, or explain why the old assertion is still expected to hold. Encoding the new contract in that device test is also cheaper and far more deterministic than the added Appium UI test, which needs a real safe area on the run device to discriminate.
| width += _safeArea.HorizontalThickness; | ||
| height += _safeArea.VerticalThickness; | ||
| } | ||
| // UIKit is already reserving the safe area natively via a non-zero |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[minor] Complexity Reduction / Performance-Critical Path — The predicate SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentBehavior.Never now appears three times in this type: ValidateSafeArea (line 390), CrossPlatformArrange (line 453), and again on line 500. These three sites must stay in lockstep — if they ever diverge, _safeArea is derived from one source while the arrange bounds and the content-size padding assume another, which is exactly the double-reserve class of bug this PR fixes. Consider extracting it to a single private property (e.g. bool UIKitReservesSafeAreaNatively) so the three sites cannot drift.
Secondary point on the same line: SystemAdjustedContentInset is a computed property that reads two ObjC properties (AdjustedContentInset and ContentInset) across the managed/native boundary on every access. CrossPlatformArrange is on the layout hot path and now evaluates it twice per pass (line 453 and line 500). Hoisting it into a local at the top of the method removes two bridge round-trips per arrange.
| // range should be zero. When the safe area is reserved twice, this value | ||
| // is positive and equals the safe-area thickness. | ||
| double phantom = (native.ContentSize.Height + native.AdjustedContentInset.Top + native.AdjustedContentInset.Bottom) - native.Bounds.Height; | ||
| _diagLabel.Text = $"Phantom={phantom}"; |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Regression Prevention / Logic and Correctness — The value is formatted with the device's current culture but parsed with CultureInfo.InvariantCulture in the UI test (src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36800.cs:29). $"Phantom={phantom}" on a double uses CurrentCulture, so on a device configured for a comma-decimal locale (de-DE, fr-FR, etc.) this emits e.g. Phantom=0,5. Invariant double.Parse uses NumberStyles.Float | AllowThousands, so "0,5" is parsed as the thousands-grouped value 5, not 0.5 — the test then reports a phantom range of 5pt for content that is actually correct, or silently reports a wrong magnitude in the failure message.
Format with the invariant culture to match the parser, e.g. _diagLabel.Text = string.Format(CultureInfo.InvariantCulture, "Phantom={0}", phantom); (or phantom.ToString(CultureInfo.InvariantCulture)).
| var phantomValueText = diagText.Substring("Phantom=".Length); | ||
| var phantom = double.Parse(phantomValueText, CultureInfo.InvariantCulture); | ||
|
|
||
| Assert.That(phantom, Is.LessThanOrEqualTo(1.0), |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Regression Prevention — This assertion cannot discriminate the bug from a device that simply has no safe area, so it can pass vacuously. phantom is ContentSize.Height + AdjustedContentInset.Top + AdjustedContentInset.Bottom - Bounds.Height, which is exactly the safe-area thickness when the bug is present and 0 when it is fixed. If the scroll view's vertical safe area insets are zero on the run device/orientation (iPad simulator variants, landscape configurations, or any future runner change), phantom is ~0 with or without the fix and the test goes green regardless — the regression it exists to catch would no longer be detected.
Have OnDumpClicked also emit the measured AdjustedContentInset.Top + AdjustedContentInset.Bottom and assert here that it is greater than zero before asserting phantom <= 1.0. That turns a silently-vacuous pass into an explicit precondition failure.
Related, on line 29: double.Parse is reached for the "Phantom=NoHandler" branch too (it satisfies Does.StartWith("Phantom=") on line 26), so a missing handler surfaces as an opaque FormatException instead of a diagnosable assertion. Assert the text is not "Phantom=NoHandler"/"Phantom=Unknown" before parsing.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@BagavathiPerumal — new AI review results are available based on commit
e00da3a.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: IOS · Base: main · Merge base: e34b3fb9
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue36800 Issue36800 |
✅ FAIL — 313s | ✅ PASS — 118s |
🔴 Without fix — 🖥️ Issue36800: FAIL ✅ · 313s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.TestCases.Tests.Issues.Issue36800.ScrollViewWithContainerSafeAreaDoesNotDoubleReserveSafeArea() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36800.cs:line 31
🟢 With fix — 🖥️ Issue36800: PASS ✅ · 118s
(no coded error found; showing last 1200 chars)
d the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.07] Discovering: Controls.TestCases.iOS.Tests
[xUnit.net 00:00:00.23] Discovered: Controls.TestCases.iOS.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.iOS.Tests/Debug/net10.0/Controls.TestCases.iOS.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 8/25/2026 4:41:25 AM FixtureSetup for Issue36800(iOS)
>>>>> 8/25/2026 4:41:30 AM ScrollViewWithContainerSafeAreaDoesNotDoubleReserveSafeArea Start
>>>>> 8/25/2026 4:41:31 AM ScrollViewWithContainerSafeAreaDoesNotDoubleReserveSafeArea Stop
Passed ScrollViewWithContainerSafeAreaDoesNotDoubleReserveSafeArea [586 ms]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue36800.trx
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 22.1125 Seconds
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue36800.trx
📁 Fix files reverted (1 files)
src/Core/src/Platform/iOS/MauiScrollView.cs
📋 Pre-Flight — Context & Validation
Issue: #36800 - iOS: ScrollView with SafeAreaEdges="Container" reserves the safe area twice
PR: #37765 - [iOS] Fix for ScrollView SafeAreaEdges="Container" Double Safe-Area Reservation Causing Phantom Scroll Range
Platform: iOS
Base / review commit: e34b3fb92b92408f9c5e3818d81923d75a394164 / 69320c9b1f54ec3aad54d9667981345989e27b18
Files changed: 1 implementation, 2 UI-test files
Gate: PASSED - the primary test fails without the PR fix and passes with it. Do not rerun gate verification.
Problem and Current Fix
For SafeAreaEdges.Container, MauiScrollView maps to UIKit's ContentInsetAdjustmentBehavior.Always. UIKit then reserves the safe area through AdjustedContentInset, while CrossPlatformArrange also adds _safeArea thickness to native ContentSize. The resulting scroll range is inflated by the inset thickness.
The PR changes the manual ContentSize padding condition from every non-Automatic behavior to only SystemAdjustedContentInset == UIEdgeInsets.Zero or ContentInsetAdjustmentBehavior.Never. Its new iOS UI test measures:
ContentSize.Height + AdjustedContentInset.Top + AdjustedContentInset.Bottom - Bounds.Height
and requires the value to be at most 1 point.
Changed Files
src/Core/src/Platform/iOS/MauiScrollView.cs- implementation; only production target for alternative fixes.src/Controls/tests/TestCases.HostApp/Issues/Issue36800.cs- new iOS repro page.src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36800.cs- new iOS Appium regression.
Required Tests
Primary test:
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue36800"Mandatory existing regression identified by review:
pwsh .github/skills/run-device-tests/scripts/Run-DeviceTests.ps1 -Project Controls -Platform ios -IncludeClasses "Microsoft.Maui.DeviceTests.ScrollViewTests" -IncludeMethods "ContentSizeExpandsToViewport"No other regression tests were supplied. Do not run a category or full suite.
Review Constraints and Edge Cases
- Existing PR logic is not an alternative candidate: do not merely repeat the
SystemAdjustedContentInset == Zero || Nevercontent-size condition. ScrollViewTests.ContentSizeExpandsToViewportcurrently expectsSafeAreaInsets.InsetRect(ContentSize).Sizeto equalAdjustedContentInset.InsetRect(Bounds).Size. Review feedback predicts the PR's reduced nativeContentSizeviolates that established contract. Record empirical results rather than changing the test.- The new host diagnostic formats with current culture while its test parses invariant culture. This is a test-quality finding, not permission to edit the added test files during try-fix.
- Confirm the candidate actually exercises
Alwayswith a non-zero system inset; a zero-inset pass would not prove the bug fixed. - Consider small and tall content,
Never/Automatic, rotation/keyboard invalidation, and the shared MacCatalyst implementation, but test only the two commands above on iOS. - Never gate a per-view callback on
Window.SafeAreaInsets, and never compare raw UIKit insets with edge-filtered_safeArea. - Existing uncommitted
.github/engchanges are harness-owned. Do not alter, revert, clean, or include them in candidate diffs. - Follow
.github/instructions/safe-area-ios.instructions.md,.github/instructions/threading-async.instructions.md, and the inline expert self-review required by thetry-fixskill.
Candidate Baseline
EstablishBrokenBaseline.ps1 must be the only mechanism used to establish and restore the broken implementation. Candidate edits are limited to the script's RevertedFiles allow-list. Added PR test files remain read-only. Every attempt must restore with:
pwsh .github/scripts/EstablishBrokenBaseline.ps1 -RestoreOriginal PR Candidate
| Source | Approach | Gate | Known concern |
|---|---|---|---|
| PR #37765 | Stop padding native ContentSize when Always has a non-zero system-adjusted inset |
PASS | Predicted to break ContentSizeExpandsToViewport; test diagnostic is locale-sensitive |
🔬 Code Review — Deep Analysis
Expert Evaluation — PR #37765
Verdict: REQUEST CHANGES
Confidence: medium
Independent assessment
The production change correctly targets the double-reservation path: when
SafeAreaEdges.Container selects UIKit's Always behavior and
SystemAdjustedContentInset is non-zero, CrossPlatformArrange stops adding
the same safe-area thickness to ContentSize. The change is localized to the
iOS/MacCatalyst scroll-view implementation and adds an iOS UI regression test.
The trusted Gate passed: the new test fails without the submitted fix and
passes with it.
Blocking concern
The new condition conflicts with the established
ScrollViewTests.ContentSizeExpandsToViewport device-test contract. Under
Always with a non-zero safe area, the submitted fix leaves
ContentSize.Height at Bounds.Height - safeArea, while that test expects the
native content size to expand to the full viewport. The required existing
regression test must determine whether this is a real behavior regression or
an obsolete contract; it cannot be assumed safe from the new UI test alone.
Additional findings
- The HostApp diagnostic formats
doublewith the current culture, but the UI
test parses withInvariantCulture, producing incorrect values or failures
on comma-decimal locales. - The UI test can pass vacuously when the device has zero vertical adjusted
inset, and itsNoHandlersentinel reachesdouble.Parserather than
yielding a diagnostic assertion. - The native/manual safe-area ownership predicate now appears three times in
MauiScrollView; centralizing it would prevent future drift and avoid
repeated managed/native property reads in the layout hot path.
The raw file:line findings are persisted in inline-findings.json.
🛠️ Try-Fix — Analysis & Comparison
Try-Fix Aggregate — PR #37765 (iOS ScrollView SafeAreaEdges="Container" double safe-area reservation)
Issue: #36800 · Platform: iOS · Target file: src/Core/src/Platform/iOS/MauiScrollView.cs
Gate: PASSED before try-fix (primary test fails without the PR fix, passes with it). Not rerun.
Candidates final: 2 of 2 attempted. None produced a Pass. Final verdict: BLOCKED.
| # | Source | Approach | Result | Artifacts |
|---|---|---|---|---|
| 0 | PR #37765 (baseline approach, not a try-fix candidate) | Keep Always; narrow the manual ContentSize padding condition to SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == Never |
Gate PASS; predicted to break ContentSizeExpandsToViewport |
— |
| 1 | try-fix attempt-1 | Single-Owner Safe Area — map uniform SafeAreaEdges.Container to ContentInsetAdjustmentBehavior.Never so MAUI alone reserves the safe area |
Blocked (baseline could not be established; 0 files changed; 0 self-review findings) | CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix/attempt-1/ |
| 2 | try-fix attempt-2 | Native Inset-Ledger Compensation — keep Always and content-size padding, but offset only UIKit's duplicate effective scroll-inset ledger through explicit ContentInset |
Blocked (baseline could not be established; 0 files changed; 0 self-review findings) | CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix/attempt-2/ |
Original PR approach (#37765)
CrossPlatformArrange in MauiScrollView.cs previously padded native ContentSize by
_safeArea thickness for every non-Automatic adjustment behavior. For SafeAreaEdges.Container,
UpdateContentInsetAdjustmentBehavior() selects UIScrollViewContentInsetAdjustmentBehavior.Always, so
UIKit also reserves the safe area through AdjustedContentInset. The reservation is therefore counted
twice and the scrollable range is inflated by the inset thickness even when the content fits.
The PR's diff (against base e34b3fb92b92408f9c5e3818d81923d75a394164):
- else if (ContentInsetAdjustmentBehavior != UIScrollViewContentInsetAdjustmentBehavior.Automatic)
+ else if (SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentBehavior.Never)
{
+ // UIKit is not currently reserving any space for the safe area on
+ // this scroll view ... (comment)
width += _safeArea.HorizontalThickness;
height += _safeArea.VerticalThickness;
}
+ // UIKit is already reserving the safe area natively ... (comment)Known concerns carried into try-fix: it leaves two safe-area owners alive and reconciles them with a
conditional that depends on UIKit having already published a non-zero inset for the current layout pass
(fragile across first layout, rotation, keyboard); and it shrinks native ContentSize below the viewport,
which is the invariant ScrollViewTests.ContentSizeExpandsToViewport asserts. The Issue36800 host
diagnostic also formats with current culture while the test parses invariant culture (test-quality only).
Candidates must not restate this mechanism — a renamed or rearranged conditional content-size-padding
fix does not count as an alternative.
Candidate 1 — Single-Owner Safe Area (Container → Never) — BLOCKED
Full self-contained narrative: CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix-1/content.md
Attempt artifacts: CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix/attempt-1/
(baseline.log, approach.md, result.txt = Blocked, fix.diff (empty), test-output.log,
analysis.md, reviewer-findings.json = [], reviewer-findings.diff (empty))
Approach
Change UpdateContentInsetAdjustmentBehavior() so a uniform SafeAreaRegions.Container maps to
UIScrollViewContentInsetAdjustmentBehavior.Never instead of .Always, joining SafeAreaRegions.All and
the SoftInput regions that already do so. MAUI then owns the container safe area outright:
ValidateSafeArea() takes its GetInset() branch, CrossPlatformArrange() insets the arrange rect
(origin and size), and the existing manual padding restores ContentSize to the full Bounds size. With
AdjustedContentInset identically zero, the scrollable range for fitting content is zero. Container
semantics hold because GetInset() derives from UIView.SafeAreaInsets (bars/notch, no keyboard).
Prior approach avoided
PR #37765 (the only prior approach; no earlier try-fix attempts exist). Its hypothesis is that the manual
content-size padding is the wrong half of an otherwise correct dual-ownership model. The failure mechanism
it preserves is simultaneous dual ownership of the safe area (UIKit's AdjustedContentInset plus
MAUI's _safeArea), reconciled only by a layout-pass-ordering-sensitive runtime conditional, and it
leaves ContentSize smaller than the viewport.
Mechanism-level difference
The candidate never touches the padding decision; it removes one owner at the source. Because the behavior
is Never, UIKit publishes no AdjustedContentInset, so both inset terms in
ContentSize.Height + AdjustedContentInset.Top + AdjustedContentInset.Bottom - Bounds.Height are
identically zero and the safe area is expressed exactly once, in the arrange rect, via the code path that
already serves every other non-Automatic region. The PR nulls MAUI's reservation and keeps UIKit's; this
candidate nulls UIKit's and keeps MAUI's — and in doing so also preserves the ContentSize == Bounds
invariant the existing device test encodes.
Files changed
None — blocked before any edit. fix.diff is empty; git status --porcelain --untracked-files=no -- src/ is empty.
(empty diff)
Tests
| # | Command | Result |
|---|---|---|
| 1 | pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue36800" |
NOT RUN (blocked) |
| 2 | pwsh .github/skills/run-device-tests/scripts/Run-DeviceTests.ps1 -Project Controls -Platform ios -IncludeClasses "Microsoft.Maui.DeviceTests.ScrollViewTests" -IncludeMethods "ContentSizeExpandsToViewport" |
NOT RUN (blocked) |
Neither was executed: with no candidate implementation on disk, both would have exercised the unmodified
PR tree and produced evidence attributable to the PR's fix rather than to this candidate. A Pass may never
be manufactured that way.
Overall result
Blocked. Inline expert self-review: 0 findings (no diff to review — documented Step 6 no-diff path).
Failure analysis
EstablishBrokenBaseline.ps1 aborted at its fail-fast cleanliness gate (line 392):
Working directory is not clean. git status --porcelain --untracked-files=no reports 40 modified
tracked files, all under .github/scripts/, .github/skills/, and eng/scripts/ — harness-owned changes
this invocation forbids altering, reverting, cleaning, or committing. The script has no bypass switch
(-BaseBranch, -DryRun, -Restore only), and its printed remediation (git commit / git checkout -- .) is prohibited by both the invocation and try-fix Principle 6. .github/.baseline-state.json was never
created, so per Step 2 the correct action is Blocked with no tracked-file edits.
Step 9 ran exactly pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore, completing with
Restored False / No baseline state found — the accepted verified no-state outcome for a Step-2 block
with zero edits. src/ re-verified clean; all harness paths left untouched.
Predicted test-contract conflict (code reading, not empirical)
ContentSizeExpandsToViewport asserts SafeAreaInsets.InsetRect(ContentSize).Size == AdjustedContentInset.InsetRect(Bounds).Size; under Always (where AdjustedContentInset == SafeAreaInsets) this reduces to ContentSize.Height == Bounds.Height. Issue36800 requires
ContentSize.Height + AdjustedContentInset.Top + AdjustedContentInset.Bottom - Bounds.Height <= 1.
Together these imply SafeAreaInsets.Vertical <= 1, so both can hold at once only when the scroll view's
own SafeAreaInsets is (near) zero in the device-test host. The PR's approach shrinks ContentSize below
Bounds and violates the device test whenever that inset is non-zero; the Never candidate keeps
ContentSize == Bounds and zeroes AdjustedContentInset, satisfying Issue36800 while staying
compatible with the device test's invariant. Unverified — must be confirmed on device.
Guidance for the next candidate
- The block is environmental, not attempt-specific. Every candidate will hit the same
EstablishBrokenBaseline.ps1cleanliness failure until the harness commits or stashes its own
.github/engedits. ExpectBlockedat Step 2 unless that precondition changes, and do not clean
the tree to work around it. - Mechanisms already claimed: (a) PR #37765 — conditional content-size padding under
Always
[excluded]; (b) candidate 1 —Container→Never, single-owner safe area. Choose a third distinct
root-cause hypothesis. - Mechanisms shown to be dead ends by analysis: zeroing
_safeArea/_appliesSafeAreaAdjustments
while keepingAlways(arrange bounds then equal fullBounds, content fillsBounds.Height, and the
non-zeroAdjustedContentInsetreproduces the phantom range); and subtracting
SystemAdjustedContentInsetfrom the padding term (arithmetic rearrangement of the PR's mechanism, and
therefore excluded). - Do not modify the added PR test files (
Issue36800.cshost page and shared test) or
ScrollViewTests.iOS.cs. Record empirical conflicts instead of manufacturing compatibility.
Candidate 2 — Native Inset-Ledger Compensation — BLOCKED
Full self-contained narrative: CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix-2/content.md
Attempt artifacts: CustomAgentLogsTmp/PRState/37765/PRAgent/try-fix/attempt-2/
(baseline.log, approach.md, result.txt = Blocked, fix.diff (empty), test-output.log,
analysis.md, reviewer-findings.json = [], reviewer-findings.diff (empty))
Approach
Keep ContentInsetAdjustmentBehavior.Always, keep the baseline MAUI _safeArea arrange geometry and
manual ContentSize padding rule unchanged, but compensate the duplicate reservation in UIKit's
scroll-range ledger by applying an equal-and-opposite explicit ContentInset for the container-safe-area
component when UIKit publishes its system adjustment. The intended synchronization point was
AdjustedContentInsetDidChange/safe-area invalidation, using pixel-level equality to prevent callback
oscillation.
Prior approaches avoided
PR #37765 conditionally suppresses manual ContentSize padding when SystemAdjustedContentInset is
non-zero; this candidate does not reproduce or arithmetically rearrange that content-size condition.
Candidate 1 remaps Container from Always to Never so MAUI becomes the sole safe-area owner; this
candidate keeps Always, keeps UIKit's non-zero system adjustment, and does not use behavior remapping or
that single-owner mechanism.
Mechanism-level difference
The distinct root-cause hypothesis is that the duplicate quantity is recorded in two different native
ledgers: MAUI's arranged-content geometry and UIKit's effective scroll insets. Rather than changing the
content extent (the PR) or selecting one safe-area owner (candidate 1), the proposed mechanism preserves
both layout inputs and offsets only the duplicated term in UIKit's effective scroll-range accounting via
explicit ContentInset. SystemAdjustedContentInset would remain non-zero evidence that .Always is
active. This remains a proposal only: the baseline safety gate prevented implementation and empirical
validation.
Files changed
None. src/Core/src/Platform/iOS/MauiScrollView.cs was never edited. The exact candidate diff is:
(empty diff)Tests
| # | Exact command | Result |
|---|---|---|
| 1 | pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "Issue36800" |
NOT RUN — BLOCKED BEFORE IMPLEMENTATION |
| 2 | pwsh .github/skills/run-device-tests/scripts/Run-DeviceTests.ps1 -Project Controls -Platform ios -IncludeClasses "Microsoft.Maui.DeviceTests.ScrollViewTests" -IncludeMethods "ContentSizeExpandsToViewport" |
NOT RUN — BLOCKED BEFORE IMPLEMENTATION |
The required order was therefore not entered. Running either command without a candidate diff would have
tested the unmodified PR tree and could not establish a candidate-2 Pass. Active .Always/non-zero-inset
evidence was unavailable because no candidate build or test could run.
Overall result
Blocked. A Pass requires both commands to execute and pass; neither executed. Inline expert self-review:
0 findings (reviewer-findings.json is []; no candidate diff existed).
Concrete failure analysis
pwsh .github/scripts/EstablishBrokenBaseline.ps1 failed its mandatory dirty-worktree safety gate before
creating .github/.baseline-state.json. It reported 40 tracked modifications, all in harness-owned
.github and eng paths that this invocation expressly prohibited altering, reverting, cleaning, staging,
committing, or stashing. With no baseline state there was no RevertedFiles allow-list, so editing the target
would violate the try-fix baseline-file boundary and could not be safely restored. The mechanism is therefore
unimplemented and non-empirical; its recursion behavior, edge filtering, rotation/keyboard invalidation, and
iOS/MacCatalyst effects remain untested.
All required no-diff artifacts were captured and passed the artifact existence gate. Step 9 then ran exactly
pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore, which completed with the accepted no-state result
Restored False / No baseline state found. A post-restore check confirmed .github/.baseline-state.json
was absent and the target implementation file had no attempt-created diff.
FINAL STEP 5a verdict
BLOCKED — 0 Pass, 0 Fail, 2 Blocked across exactly two candidates. Both invocations stopped at the
mandatory baseline cleanliness gate before implementation, so neither supplied empirical test evidence.
Candidate 1 proposed behavior remapping/single ownership; candidate 2 proposed native inset-ledger
compensation while retaining .Always and the baseline content-size rule. STEP 5a stopped after two
invocations as strictly bounded; no third candidate was attempted.
🏁 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Winner: pr-plus-reviewer
pr-plus-reviewer is the strongest candidate because it preserves the submitted
fix that passed the trusted Gate, then addresses the expert review's concrete
test-quality and maintainability findings. Its strengthened Issue36800 test
passed on iOS while requiring a non-zero adjusted inset, so the result exercises
the actual Always double-reservation scenario rather than passing vacuously.
The submitted PR still needs these reviewer changes, so approval is not
recommended.
Comparative ranking
| Rank | Candidate | Implementation | Validation | Assessment |
|---|---|---|---|---|
| 1 | pr-plus-reviewer |
Submitted fix plus centralized ownership predicate and robust invariant/non-zero-inset diagnostics | Gate inherited PASS; strengthened Issue36800 PASS (1/1); ContentSizeExpandsToViewport BLOCKED by Xcode/workload mismatch |
Winner. Best implemented and tested candidate, but the existing device-test contract remains unverified. |
| 2 | pr |
Suppress manual ContentSize padding when UIKit already supplies a non-zero system inset |
Trusted Gate PASS | Directionally correct, but retains locale-sensitive and potentially vacuous test diagnostics, duplicates the ownership predicate, and has an unresolved expert concern about ContentSizeExpandsToViewport. |
| 3 | try-fix-1 |
Proposed mapping SafeAreaEdges.Container to Never so MAUI alone owns the inset |
BLOCKED before implementation; no diff; no tests | Plausible arithmetic, but unimplemented and incompatible with the review requirement to prove Always with a non-zero native inset. |
| 4 | try-fix-2 |
Proposed compensating UIKit's inset ledger with an opposite explicit ContentInset |
BLOCKED before implementation; no diff; no tests | Unimplemented and carries additional callback, oscillation, edge-filtering, rotation, keyboard, and MacCatalyst risk. |
No candidate produced a regression-test failure. Both try-fix candidates rank
below the implemented candidates because they were blocked before producing
code or empirical evidence. The required existing device regression also did
not fail: its build was blocked before execution because the installed .NET iOS
workload requires Xcode 26.0 while this host has Xcode 26.5. That uncertainty
must not be represented as a pass.
Expert findings disposition
| Finding | Raw PR | pr-plus-reviewer |
|---|---|---|
Existing ContentSizeExpandsToViewport contract may conflict with the fix |
Unresolved | Still uncertain; required test was attempted once but blocked before execution |
| Current-culture formatting with invariant parsing | Present | Fixed |
| Zero-inset vacuous pass and opaque missing-handler parse failure | Present | Fixed |
| Triplicated ownership predicate and repeated native reads | Present | Fixed |
The raw PR is therefore not the winning candidate. Apply
pr-plus-reviewer/reviewer.patch, then obtain one valid
ContentSizeExpandsToViewport result in a compatible iOS toolchain before
merge.
🔗 Regression Cross-Reference
🔍 Regression Cross-Reference
⚠ Overlaps with prior bug-fix PRs — same files modified, but no exact line revert detected.
📱 UI Tests — SafeAreaEdges,ViewBaseTests
Detected UI test categories: SafeAreaEdges,ViewBaseTests
✅ Deep UI tests — 222 passed, 0 failed across 2 categories on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
SafeAreaEdges |
110/110 ✓ | — |
ViewBaseTests |
112/112 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
🧭 Next Steps — reviewer changes required
The reviewer-enhanced candidate identified changes that are not yet in the submitted PR.
Why: pr-plus-reviewer preserves the Gate-proven production fix while correcting the expert review's locale, vacuous-test, missing-handler, and predicate-drift concerns. Its strengthened Issue36800 test passed with a non-zero adjusted inset; the existing device regression remains uncertain because an Xcode/workload mismatch blocked it before execution.
Address the actionable findings in this review before merging.
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…eservation Causing Phantom Scroll Range (#37765) > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator). When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content. The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset. As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable. The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged. This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors. Tested the behavior in the following platforms. - [x] iOS - [x] Android - [ ] Mac - [ ] Windows Fixes #36800 |Before Fix|After Fix| |--|--| <video src="https://github.com/user-attachments/assets/19c064e4-4622-4d1d-992c-340a56fb948f"> |<video src="https://github.com/user-attachments/assets/57fa7cae-6e95-4573-885d-8f2979750f38">| --------- Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
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!
Issue Description
On iOS, a ScrollView configured with SafeAreaEdges="Container" incorrectly reserves the safe area twice, resulting in an additional phantom scroll range. When the content is smaller than the viewport, the ScrollView becomes unnecessarily scrollable. The extra scrollable area corresponds exactly to the combined safe-area insets (for example, 82 pt on an iPhone 11: 48 pt for the status bar and 34 pt for the home indicator).
When the content is taller than the viewport, the same issue causes an unnecessary dead over-scroll space at the end of the content.
Root Cause
The issue occurs because MauiScrollView.CrossPlatformArrange always pads the scroll content's measured size by the safe-area thickness when ContentInsetAdjustmentBehavior is not Automatic, without checking whether UIKit has already reserved the same space through AdjustedContentInset.
As a result, when SafeAreaEdges="Container" is set and the native inset is active, the safe area is accounted for twice—once by UIKit and once by the manual padding. This inflates the content size beyond its actual bounds and produces a phantom scrollable range, even when the content should not be scrollable.
Description of Change
The fix narrows the manual-padding condition so that it applies only when the native inset is genuinely inactive—that is, when SystemAdjustedContentInset is zero or ContentInsetAdjustmentBehavior is Never. In all other cases, UIKit's own inset is trusted to reserve the safe area, and the content size is left unchanged.
This removes the double reservation for SafeAreaEdges="Container" while preserving the existing None and Automaticbehaviors.
Tested the behavior in the following platforms.
Issues Fixed
Fixes #36800
Output
36800-BeforeFix.mov
36800-AfterFix.mov