[Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction - #36431
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36431Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36431" |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 2 findings
See inline comments for details.
There was a problem hiding this comment.
Pull request overview
This PR targets a Mac Catalyst-specific UIKit behavior where UICollectionView silently clamps contentOffset (typically after a non-animated ScrollTo near the end of the list) during window state transitions like Picker dismissal, which makes CollectionView appear to “jump”/lose its scroll position.
Changes:
- Adds a Mac Catalyst-only KVO observer on
contentOffsetinMauiCollectionViewto detect sudden downward clamps and restore the intended position viaScrollToItem. - Arms/clears the pending “restore target” around non-animated
ScrollTooperations and clears the restore state when the user begins dragging. - Adds a new UI test page + UITest for Issue #34271 and updates Mac Catalyst PublicAPI entries for new overrides.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs | Mac Catalyst-only KVO tracking and restore logic for silent contentOffset clamps; observer lifecycle in MovedToWindow. |
| src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs | Arms pending restore after non-animated ScrollToItem; clears restore on ItemsSource/layout changes (Mac Catalyst). |
| src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs | Clears pending restore on user drag start (Mac Catalyst). |
| src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | Records new/updated Mac Catalyst override signatures introduced by the fix. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue34271.cs | Adds a HostApp reproduction page for the scenario (scroll-to-last + Picker interaction + layout recomputation trigger). |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34271.cs | Adds an Appium-based UITest to validate scroll position is preserved across Picker dismissal and forced layout recomputation. |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 3 findings
See inline comments for details.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@Vignesh-SF3580 — new AI review results are available based on this last commit:
8e85bae. To request a fresh review after new comments or commits, comment/review rerun.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ❌ FAILED
Platform: CATALYST · Base: main · Merge base: 0395a53b
🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue34271 Issue34271 |
❌ PASS — 449s | ✅ PASS — 121s |
🔴 Without fix — 🖥️ Issue34271: PASS ❌ · 449s
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 627 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Essentials/src/Essentials.csproj (in 5.19 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/maps/src/Maps.csproj (in 6.97 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 6.97 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/BlazorWebView/src/Maui/Microsoft.AspNetCore.Components.WebView.Maui.csproj (in 6.32 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Core/src/Core.csproj (in 6.97 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 6.97 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Foldable/src/Controls.Foldable.csproj (in 6.99 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/Maps/src/Controls.Maps.csproj (in 6.97 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj (in 7.15 sec).
1 of 11 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Maps.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Maps.dll
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Xaml.dll
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-maccatalyst26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Foldable.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-maccatalyst/maccatalyst-x64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-maccatalyst/maccatalyst-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:01:42.76
Determining projects to restore...
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 811 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 751 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 746 ms).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 1.88 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 2.87 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 3.95 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 7 sec).
Restored /Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.Mac.Tests/Controls.TestCases.Mac.Tests.csproj (in 7.04 sec).
5 of 13 projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.Mac.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)
Starting test execution, please wait...
A total of 1 test files matched 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.08] Discovering: Controls.TestCases.Mac.Tests
[xUnit.net 00:00:00.20] Discovered: Controls.TestCases.Mac.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 7/14/2026 4:26:15 PM FixtureSetup for Issue34271(Mac)
>>>>> 7/14/2026 4:26:16 PM CollectionViewScrollPositionPreservedAfterPickerDismiss Start
>>>>> 7/14/2026 4:26:29 PM CollectionViewScrollPositionPreservedAfterPickerDismiss Stop
NUnit Adapter 4.5.0.0: Test execution complete
Passed CollectionViewScrollPositionPreservedAfterPickerDismiss [13 s]
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue34271.trx
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 35.7911 Seconds
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue34271.trx
🟢 With fix — 🖥️ Issue34271: PASS ✅ · 121s
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Maps/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Maps.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Microsoft.AspNetCore.Components.WebView.Maui -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-maccatalyst26.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Xaml.dll
Controls.Foldable -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Foldable.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Maps -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-maccatalyst26.0/Microsoft.Maui.Controls.Maps.dll
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-maccatalyst/maccatalyst-x64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Detected signing identity:
Code Signing Key: "" (-)
Provisioning Profile: "" () - no entitlements
Bundle Id: com.microsoft.maui.uitests
App Id: com.microsoft.maui.uitests
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.TestCases.HostApp -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-maccatalyst/maccatalyst-arm64/Controls.TestCases.HostApp.dll
Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
Optimizing assemblies for size. This process might take a while.
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:01:13.11
Determining projects to restore...
All projects are up-to-date for restore.
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.CustomAttributes -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
##vso[build.updatebuildnumber]10.0.100-ci+azdo.14657503
Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
UITest.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
VisualTestUtils -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
VisualTestUtils.MagickNet -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
UITest.NUnit -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
UITest.Appium -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
UITest.Analyzers -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
Controls.TestCases.Mac.Tests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)
Starting test execution, please wait...
A total of 1 test files matched 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.06] Discovering: Controls.TestCases.Mac.Tests
[xUnit.net 00:00:00.18] Discovered: Controls.TestCases.Mac.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.TestCases.Mac.Tests/Debug/net10.0/Controls.TestCases.Mac.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 7/14/2026 4:32:40 PM FixtureSetup for Issue34271(Mac)
>>>>> 7/14/2026 4:32:40 PM CollectionViewScrollPositionPreservedAfterPickerDismiss Start
>>>>> 7/14/2026 4:32:54 PM CollectionViewScrollPositionPreservedAfterPickerDismiss Stop
Passed CollectionViewScrollPositionPreservedAfterPickerDismiss [14 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue34271.trx
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 24.0253 Seconds
>>> TRX_RESULT_FILE: /Users/cloudtest/vss/_work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue34271.trx
⚠️ Failure Details
- ❌ Issue34271 PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (6 files)
src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cssrc/Controls/src/Core/Handlers/Items2/CarouselViewHandler2.iOS.cssrc/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cssrc/Controls/src/Core/Handlers/Items2/iOS/CarouselViewController2.cssrc/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cssrc/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
📱 UI Tests — CollectionView
Detected UI test categories: CollectionView
❌ Deep UI tests — 309 passed, 81 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
CollectionView |
309/391 (81 ❌) | 162 diff PNGs |
❌ CollectionView — 81 failed tests
CollectionViewHeaderBlankWhenLastItemRemoved
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionViewHeaderBlankWhenLastItemRemoved.png (43.53% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullabl
...
SelectedItemVisualIsCleared
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: SelectedItemVisualIsCleared.png (42.70% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, N
...
CheckEmptyViewMargin
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CheckEmptyViewMargin.png (29.68% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable
...
ItemShouldbeScrolledbasedOnGroupHeader
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: ItemShouldbeScrolledbasedOnGroupHeader.png (40.15% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 re
...
ItemsWrapGridShouldUpdateBasedOnCollectionViewSize
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: ItemsWrapGridWithDefaultWidth.png (37.63% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.Issues.Issue26083.ItemsWrapGridShouldUpdateBasedOnCollectionViewSize() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue26083.cs:line 28
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.RuntimeMetho
...
VerifyScrollToByItemWithEndPositionAndVerticalList_Carrot
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyScrollToByItemWithEndPositionAndVerticalList_Carrot.png (42.70% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String
...
VerifyScrollToByItemWithMakeVisiblePositionAndVerticalList_Carrot
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyScrollToByItemWithMakeVisiblePositionAndVerticalList_Carrot.png (42.70% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot
...
VerifyMeasureAllItemsWithObservableCollection
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyMeasureAllItemsWithObservableCollection.png (30.81% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
at Microsoft.Maui.TestCases.Tests.CollectionView_ScrollingFeatureTests.VerifyMeasureAllItemsWithObservableCollection() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/CollectionView_ScrollingFeatureTests.cs:line 53
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, Object
...
AccessibilityTraitsSetCorrectly
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: AccessibilityTraitsSetCorrectlyNone.png (24.87% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retry
...
RefreshShouldNotChangeSize
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: RefreshShouldNotChangeSize.png (22.73% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nu
...
VerifyFlowDirectionRTLAndMeasureAllItemsWithObservableCollection
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyFlowDirectionRTLAndMeasureAllItemsWithObservableCollection.png (30.57% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(
...
VerifyCollectionViewVisualState
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyCollectionViewVisualState.png (40.81% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDela
...
FooterShouldDisplayAtBottomOfEmptyView
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: FooterShouldDisplayAtBottomOfEmptyView.png (18.82% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 re
...
CVHorizontalLinearItemsLayoutItemSpacing
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CVHorizontalLinearItemsLayoutItemSpacing.png (31.48% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1
...
VerifyScrollToByIndexWithMakeVisiblePositionAndVerticalList_Carrot
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyScrollToByIndexWithMakeVisiblePositionAndVerticalList_Carrot.png (42.70% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreensho
...
Issue18751Test
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: Issue18751Test.png (41.16% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
at Microsoft.Maui.TestCases.Tests.Issues.Issue18751.Issue18751Test() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue18751.cs:line 22
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.RuntimeMethodHandle.InvokeMethod(O
...
VerifyCustomSizedEmptyViewDisplaysCorrectly_WithRightToLeftFlowDirection
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyCustomSizedEmptyViewDisplaysCorrectly_WithRightToLeftFlowDirection.png (42.37% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScr
...
FlowdirectionShouldWorkForHeaderFooter
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: FlowdirectionShouldWorkForHeaderFooter.png (22.92% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 re
...
VerifyModelItemsGroupedListWhenSingleModePreSelection
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyModelItemsGroupedListWhenSingleModePreSelection.png (39.69% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
at Microsoft.Maui.TestCases.Tests.CollectionView_ItemsSourceFeatureTests.VerifyModelItemsGroupedListWhenSingleModePreSelection() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/CollectionView_ItemsSourceFeatureTests.cs:line 748
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Voi
...
CollectionViewSelectedItemBackgroundShouldPersistAfterModalNavigation
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionViewSelectedItemBackgroundShouldPersistAfterModalNavigation.png (40.89% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreen
...
CollectionViewSelectedItemBackgroundLost
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionViewSelectedItemBackgroundLost.png (41.50% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1
...
VerifyMeasureAllItemsWithGroupedList
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyMeasureAllItemsWithGroupedList.png (35.60% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
at Microsoft.Maui.TestCases.Tests.CollectionView_ScrollingFeatureTests.VerifyMeasureAllItemsWithGroupedList() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/CollectionView_ScrollingFeatureTests.cs:line 105
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig
...
PointerOverWithSelectedStateShouldWork
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: PointerOverWithSelectedStateShouldWork.png (42.74% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 re
...
GroupedCollectionViewItems
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: GroupedCollectionViewItems.png (40.23% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nu
...
CollectionviewFooterHideswhenDynamicallyAddorRemoveItems
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionviewFooterHideswhenDynamicallyAddorRemoveItems.png (43.47% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String n
...
CollectionViewMeasureFirstItem
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionViewMeasureFirstItem.png (26.31% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay
...
VerticalGridCollectionViewRTLColumnMirroringShouldWork
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerticalGridCollectionViewRTLColumnMirroringShouldWork.png (37.08% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String nam
...
VerifyCVBackgroundAndBackgroundColorWithVSM
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyCVBackgroundAndBackgroundColorWithVSM.png (40.82% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable
...
VerifyStringItemsObservableCollectionWhenMultipleModePreSelection
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: VerifyStringItemsObservableCollectionWhenMultipleModePreSelection.png (41.11% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance, Boolean includeTitleBar) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
at Microsoft.Maui.TestCases.Tests.CollectionView_ItemsSourceFeatureTests.VerifyStringItemsObservableCollectionWhenMultipleModePreSelection() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/CollectionView_ItemsSourceFeatureTests.cs:line 630
at System.RuntimeMethodHandle.InvokeMethod(ObjectH
...
CollectionViewHeaderSizewithIsVisibleBinding
VisualTestUtils.VisualTestFailedException :
Snapshot different than baseline: CollectionViewHeaderSizewithIsVisibleBinding.png (32.51% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.
More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullabl
...
(+51 more — see TRX in artifact)
🔍 AI analysis of failures — PR-related vs unrelated
🔍 AI-generated triage (GitHub Copilot CLI) — a heuristic judgement of whether each deep UI test failure is connected to this PR's changes. Verify before relying on it.
Likely PR-related: one or more failures appear connected to this PR's changes.
- ✗ PR-related — Mac Catalyst CollectionView visual snapshots (81 tests): every reported failure is in the CollectionView category on the catalyst run, and this PR changes Mac Catalyst-compiled CollectionView/ItemsView2 scroll behavior in
MauiCollectionViewand related iOS handler code, making broad CollectionView screenshot shifts plausibly caused by the new content-offset restore logic. - ✗ PR-related — Programmatic scroll and position-preservation scenarios (~many tests): representative failures like
VerifyScrollToByItemWithEndPositionAndVerticalList_CarrotexerciseScrollTo/scroll positioning, which is the exact behavior the PR modifies for non-animated Mac Catalyst CollectionView scrolls. - ✗ PR-related — Header/footer, empty view, grouping, selection, and layout CollectionView screenshots (~many tests): although these are not all direct scroll-restore tests, they render through the same modified Mac Catalyst CollectionView handler and show large, consistent snapshot differences rather than an unrelated driver/session error.
Strongest signal: the failures are narrowly concentrated in CollectionView on the platform targeted by the PR, not spread across unrelated controls or infrastructure patterns.
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)
📋 Pre-Flight — Context & Validation
Issue: #34271 - [Unstable] I9_Scrolling - Changing/Hovering the 'ScrollToPosition' would cause the item to scroll without clicking the button
PR: #36431 - [Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction
Platforms Affected: Mac Catalyst
Files Changed: 6 implementation, 2 test
Key Findings
- PR current fix uses Mac Catalyst-only KVO on
MauiCollectionView.contentOffsetafter non-animated programmaticScrollToItem, then asynchronously restores the saved index path. - Linked issue reproduces on Mac Catalyst by scrolling near the end of a CollectionView, opening/hovering/dismissing a Picker, and seeing UIKit silently clamp the CollectionView offset.
- GitHub CLI was unauthenticated; PR/issue context was gathered through the public GitHub API and local branch/diff.
EstablishBrokenBaseline.ps1could not run because unrelated.githubfiles were already dirty; try-fix attempts were isolated by patching/restoring only PR target files and saving each candidate diff.
Code Review Summary
Verdict: NEEDS_CHANGES
Confidence: low
Errors: 3 | Warnings: 0 | Suggestions: 0
Key code review findings:
- ✗
CarouselViewDelegator2.DraggingStartedbypasses the newItemsViewDelegator2.DraggingStartedclear, so user swipes can leave stale restore state armed. - ✗ The async restore closure can execute after the target is cleared or replaced because it does not verify generation/current target before
ScrollToItem. - ✗ Horizontal CarouselView paths are armed, but the detector only compares Y offset, so horizontal resets are missed.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36431 | Mac Catalyst KVO detects contentOffset drops and asynchronously calls ScrollToItem for the last programmatic target. |
❌ Gate already failed before this phase | MauiCollectionView.cs, Items2 handler/controller/delegator files, PublicAPI, UI test files |
Original PR; code review found stale-target, user-drag, and horizontal-axis risks. |
🔬 Code Review — Deep Analysis
Code Review — PR #36431
Independent Assessment
What this changes: Adds Mac Catalyst-only contentOffset KVO restore logic for MauiCollectionView, then arms/clears it around non-animated ScrollToItem paths in Items2/CarouselView.
Inferred motivation: Work around UIKit silently clamping/resetting CollectionView scroll position after Picker/window transitions.
Reconciliation with PR Narrative
Author claims: Fixes Mac Catalyst CollectionView scroll reset after Picker dismissal using KVO-based restore.
Agreement/disagreement: The motivation matches the code. The approach is plausible, but current edge cases can restore stale targets or miss CarouselView/horizontal/user-scroll paths.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
CarouselView bypasses DraggingStarted clear |
MauiBot inline comments | ❌ Unresolved | ItemsViewDelegator2.DraggingStarted added, but CarouselViewDelegator2.DraggingStarted still overrides without base. |
| Async restore can run after target changed/cleared | MauiBot inline comments | ❌ Unresolved | Queued restore closure still calls ScrollToItem without checking generation/current target. |
Old restore armed during new ScrollToItem |
MauiBot inline comments | ✅ Fixed | Current code clears before scroll in ItemsViewHandler2.iOS.cs. |
| CarouselView controller scroll paths not armed | MauiBot inline comments | ✅ Fixed | Current code arms in CarouselViewController2.cs. |
Blast Radius Assessment
- Runs for all instances: Mac Catalyst CollectionView/CarouselView handler paths using Items2.
- Startup impact: no.
- Static/shared state: no static state, but per-view state survives until explicitly cleared.
- Handler/platform impact: yes, confidence max medium before CI cap.
CI Status
- Required-check result: unavailable via
ghbecause GitHub CLI is unauthenticated in this environment. - Classification: undetermined.
- Action taken: confidence capped low; no LGTM.
Findings
❌ Error — CarouselView user drag does not clear pending restore
src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs
The new clear-on-drag hook is bypassed for CarouselView2 because CarouselViewDelegator2.DraggingStarted overrides this method and does not call base. Since this PR now arms restores from CarouselView paths, a user drag/swipe can leave stale restore state armed and later snap back to the old programmatic target.
❌ Error — Queued restore can execute after cancellation or a newer target
src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs
The dispatched closure captures section/item/scrollPosition, but before calling ScrollToItem it only checks handle/window/dragging and index bounds. If user drag, layout/source change, or a newer ScrollTo clears/rearms the restore before the queued block runs, the stale closure can still scroll to the old target.
❌ Error — Horizontal CarouselView restores are armed but Y-only detection cannot see them
src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs
The detector only compares ContentOffset.Y, while this PR arms restore from CarouselView paths that can be horizontal. A horizontal reset/clamp can leave Y == 0, so the reset is not detected and the feature silently fails for those armed paths.
Failure-Mode Probing
- User drags CarouselView after programmatic scroll: pending restore remains armed because the override skips the base hook.
- New
ScrollTo(B)occurs while restore forAis queued: stale closure can still restoreA. - Horizontal CarouselView reset: KVO fires, but Y comparison treats it as normal/no-op.
Verdict: NEEDS_CHANGES
Confidence: low due undetermined CI; findings confidence high.
Summary: The fix targets the right Mac Catalyst failure mode, but unresolved handler and async-race issues can override user intent or miss CarouselView scenarios.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Layout-pass visible-cell anchor restore in ItemsViewController2; no KVO. |
❌ Failed | 6 files | Build fixed after one iteration, then actual UI test timed out waiting for target item amid external image load failures. |
| 2 | try-fix | Cached previous CollectionViewContentSize to prevent transient estimated-size clamp. |
4 files | Target test passed, but self-review found stale oversized content-size risk on legitimate same-count shrink. | |
| 3 | try-fix | Current viewport content-size floor based on visible layout attributes. | 4 files | Target test passed, but self-review found legitimate shrink/removal can still leave blank content. | |
| 4 | try-fix | Mac Catalyst-only larger dynamic layout estimates (128f) for Items2 list/grid compositional layouts. |
✅ Passed | 4 files for independent test; production core is 1 file | Target test passed; self-review clean. |
| PR | PR #36431 | KVO detects contentOffset drops and async-restores saved ScrollToItem target. |
❌ Gate already failed | 6 implementation + 2 test files | Original PR; code review found stale async target, Carousel user-drag, and horizontal-axis risks. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| gpt-5.5 / maui-expert-reviewer | 1 | Yes | Candidate 1: layout-pass visible-cell anchor restore. |
| gpt-5.5 / maui-expert-reviewer | 2 | Yes | Candidate 2: cached content-size stabilization after Candidate 1 failure. |
| gpt-5.5 / maui-expert-reviewer | 3 | Yes | Candidate 3: non-cached viewport content-size floor after Candidate 2 self-review failure. |
| gpt-5.5 / maui-expert-reviewer | 4 | Yes | Candidate 4: Mac Catalyst-only larger dynamic estimates after Candidate 3 self-review failure. |
Exhausted: No — Candidate 4 passed the targeted regression and had a clean expert self-review.
Selected Fix: Candidate #4 — It avoids the PR's KVO observer/async stale-target/user-drag/horizontal-axis failure modes, avoids Candidate 2/3 stale content-size floors, requires no public API, and changes the estimated-size input that triggers the clamp rather than restoring after the fact.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current title/description accurately describe the submitted KVO PR, but the winning candidate is the layout-estimate fix (try-fix-4), so the metadata would be stale if that candidate is used.
Recommended title
[MacCatalyst] CollectionView: Preserve scroll position after Picker interaction
Recommended description
### Root Cause
On Mac Catalyst, opening/dismissing a Picker or other window-state transitions can cause UIKit to recalculate `UICollectionView` bounds using compositional layout estimated item sizes before the realized item heights are available.
Items2 list/grid layouts currently use `NSCollectionLayoutDimension.CreateEstimated(30f)` for dynamic dimensions. For taller item templates, that estimate can make UIKit's transient `contentSize` smaller than the actual content size. When the CollectionView has been programmatically scrolled near the bottom, UIKit clamps `contentOffset` to the smaller estimated maximum (`max(0, estimatedContentSize - frameHeight)`), shifting the visible item upward after the Picker interaction.
### Description of Change
Increase the Items2 dynamic compositional layout estimate on Mac Catalyst from `30f` to `128f` through a platform-specific `EstimatedDynamicDimension` constant in `LayoutFactory2`.
This fixes the issue at the source of the clamp by giving UIKit a closer transient content-size estimate during Mac Catalyst layout recalculation, instead of observing `contentOffset` and restoring the position after UIKit has already moved it.
The estimate remains `30f` on non-Mac Catalyst platforms.
### Key Technical Details
- `LayoutFactory2` now uses `EstimatedDynamicDimension` for dynamic list/grid item and group dimensions.
- The change is Mac Catalyst-specific (`#if MACCATALYST`) and affects Items2 compositional layouts.
- No new public API is required.
### What NOT to Do (for future agents)
- Don't restore this with long-lived `contentOffset` KVO unless all user-scroll, stale-target, and handler lifecycle paths are guarded; that approach can fight legitimate user input and queued scroll requests.
- Don't cache or floor `CollectionViewContentSize` indefinitely; legitimate item shrink/removal can otherwise leave blank trailing scroll space.
### Issues Fixed
Fixes #34271
### Platforms Tested
- [x] Mac Catalyst
🏁 Report — Final Recommendation
Comparative Report — PR #36431
Candidates
| Rank | Candidate | Result | Assessment |
|---|---|---|---|
| 1 | try-fix-4 |
✅ Passed targeted regression | Best candidate. It changes the Mac Catalyst Items2 compositional layout estimate from 30f to 128f, reducing the underestimated content-size window that lets UIKit clamp the offset. It avoids KVO, public API changes, stale async restore races, and user-scroll misclassification. |
| 2 | try-fix-2 |
✅ Passed targeted regression, then rejected by self-review | The cached content-size floor passed the test, but it can preserve an oversized stale content size when item heights legitimately shrink without an item-count change, leaving blank trailing scroll space. |
| 3 | try-fix-3 |
✅ Passed targeted regression, then rejected by self-review | The viewport content-size floor passed the test, but visible attributes can still exist after legitimate shrink/removal, so it can also block valid UIKit clamping and show nonexistent/blank content. |
| 4 | pr-plus-reviewer |
Not validated after reviewer changes | Safer than the raw PR if the reviewer feedback is applied, but it still relies on KVO and async restore in platform scroll plumbing. It would need fixes for CarouselView drag clearing, stale queued restores, horizontal-axis tracking, and an unprotected direct CarouselView loop-correction ScrollToItem path, then a fresh regression run. |
| 5 | pr |
❌ Gate failed / code review found major issues | Raw PR fix targets the right behavior but leaves stale restore/user intent risks, misses horizontal detection, and leaves an adjacent direct non-animated CarouselView loop-correction scroll unprotected. The gate also reported that the submitted regression test passed without the fix, so it did not prove the bug. |
| 6 | try-fix-1 |
❌ Failed targeted regression | Layout-pass anchor restore timed out waiting for the target item and had broader layout-pass blast radius. Failed candidates rank below passing candidates. |
Winner
Winner: try-fix-4
try-fix-4 is the strongest candidate because it addresses the cause described in the PR itself: UIKit clamps against an underestimated compositional layout content size during Mac Catalyst window/Picker transitions. Raising the Mac Catalyst dynamic estimate is simpler and less invasive than installing a persistent KVO observer and asynchronously restoring scroll state. It passed the targeted regression while avoiding the stale-target, CarouselView drag-clearing, horizontal-axis, adjacent-scroll-call-site, and public API risks found in the PR/KVO approach.
Notes
- The overall gate for the PR failed because the regression test passed without the fix; do not treat the PR's test as proof of correctness.
- Passing candidates are ranked above candidates that failed regression tests, per requirement.
try-fix-4still needs final product review for the chosen estimate value and smoke coverage across grouped/horizontal layouts, but it has the best correctness/risk tradeoff among the explored candidates.
🧭 Next Steps — alternative fix proposed (try-fix-4)
Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-4 as the strongest fix.
Why: try-fix-4 wins because it passed the targeted Mac Catalyst regression and fixes the underestimated compositional-layout size input that causes UIKit to clamp the scroll offset. It avoids the raw PR's KVO observer, async stale-target restore, and user-scroll misclassification risks.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (try-fix-4)
diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
--- a/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/iOS/LayoutFactory2.cs
@@
internal static class LayoutFactory2
{
+#if MACCATALYST
+ const float EstimatedDynamicDimension = 128f;
+#else
+ const float EstimatedDynamicDimension = 30f;
+#endif
+
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
@@
- NSCollectionLayoutDimension.CreateEstimated(30f),
+ NSCollectionLayoutDimension.CreateEstimated(EstimatedDynamicDimension),
|
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check if test failures are related?
@kubaflo I verified the failed UI tests on iOS and macOS, and they all pass locally. The remaining failures are on Android and Windows, while the changes in this PR are specific to macOS. |
kubaflo
left a comment
There was a problem hiding this comment.
Can you please resolve conflicts?
8e85bae to
a7ba21d
Compare
@kubaflo I have resolved the conflicts. |
…t after Picker interaction (#36431) > [!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! **Note:** This PR was created because the previous PR (#34356) was closed. ### Root Cause On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the `UICollectionView` `contentOffset` when the view is programmatically scrolled near or to the last item. During these transitions, UIKit recalculates scroll bounds using estimated item sizes (`NSCollectionLayoutDimension.CreateEstimated(30f)`). Because the estimated `contentSize` is smaller than the actual `contentSize`, UIKit calculates a lower maximum valid `contentOffset` and silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset: `max(0, estimatedContentSize - frameHeight)`. No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery. **Why items near the bottom are affected:** Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected. **Why only Mac Catalyst:** The same window transitions on iOS do not trigger this recalculation behavior. **Why conventional fixes fail:** Standard callbacks (`LayoutSubviews`, `ViewDidLayoutSubviews`, `Scrolled`) are not triggered because this is not a layout pass or a user scroll. KVO on `contentOffset` is the only mechanism that fires synchronously at the moment UIKit mutates the property. ### Description of Change **KVO-Based Scroll Restore** The fix uses `NSObject.AddObserver("contentOffset", ...)` (Key-Value Observing) to detect the silent reset and restore the scroll position. **Flow:** 1. After a **non-animated** `ScrollTo`, `SetPendingScrollRestore(section, item, position)` is called, storing the target index path as plain `int` fields and enabling tracking. 2. KVO `OnContentOffsetChanged` monitors every `contentOffset` change: - While the new Y is within 10px of the last known Y: treat as normal movement, update reference. - If Y drops more than 10px below the last known Y: **silent reset detected** → async-dispatches `ScrollToItem` to restore position. 3. `DraggingStarted` clears the restore target when the user manually scrolls (respects user intent). 4. KVO lifecycle is managed in `MovedToWindow` — started when view attaches, stopped when it detaches. **Why relative threshold (not `Y < 1.0`):** UIKit does not always reset to Y=0. It clamps to `max(0, estimatedContentSize - frameHeight)`, which can be a non-zero value. A fixed `Y < 1.0` check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value. **Why plain int storage (not NSIndexPath):** `NSIndexPath` is created inside a `using` block in `ScrollToRequested` and is disposed before the KVO callback fires. Storing `section` and `item` as `int` fields avoids this lifetime issue. **Why `ScrollToItem` instead of `SetContentOffset`:** `ScrollToItem` recalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position. `SetContentOffset` uses a raw pixel value that becomes stale after layout changes. ### Issues Fixed Fixes #34271 **Files Changed** | File | Change | |------|--------| | `src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs` | KVO observer infrastructure, scroll restore state, `SetPendingScrollRestore`, `ClearPendingScrollRestore`, `MovedToWindow` lifecycle | | `src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs` | Call `SetPendingScrollRestore` after non-animated `ScrollToItem`; capture `section`/`item` as ints inside `using` block | | `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs` | `DraggingStarted` override to clear restore target on user drag | | `src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt` | New public override: `ItemsViewDelegator2.DraggingStarted` | **Total**: fix spans 4 production files ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/ab398886-d90c-43d1-bd88-270a8e3925c1"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/e0b01b70-4959-406b-b94f-afd5601fa7d0">) |
…t after Picker interaction (#36431) > [!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! **Note:** This PR was created because the previous PR (#34356) was closed. ### Root Cause On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the `UICollectionView` `contentOffset` when the view is programmatically scrolled near or to the last item. During these transitions, UIKit recalculates scroll bounds using estimated item sizes (`NSCollectionLayoutDimension.CreateEstimated(30f)`). Because the estimated `contentSize` is smaller than the actual `contentSize`, UIKit calculates a lower maximum valid `contentOffset` and silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset: `max(0, estimatedContentSize - frameHeight)`. No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery. **Why items near the bottom are affected:** Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected. **Why only Mac Catalyst:** The same window transitions on iOS do not trigger this recalculation behavior. **Why conventional fixes fail:** Standard callbacks (`LayoutSubviews`, `ViewDidLayoutSubviews`, `Scrolled`) are not triggered because this is not a layout pass or a user scroll. KVO on `contentOffset` is the only mechanism that fires synchronously at the moment UIKit mutates the property. ### Description of Change **KVO-Based Scroll Restore** The fix uses `NSObject.AddObserver("contentOffset", ...)` (Key-Value Observing) to detect the silent reset and restore the scroll position. **Flow:** 1. After a **non-animated** `ScrollTo`, `SetPendingScrollRestore(section, item, position)` is called, storing the target index path as plain `int` fields and enabling tracking. 2. KVO `OnContentOffsetChanged` monitors every `contentOffset` change: - While the new Y is within 10px of the last known Y: treat as normal movement, update reference. - If Y drops more than 10px below the last known Y: **silent reset detected** → async-dispatches `ScrollToItem` to restore position. 3. `DraggingStarted` clears the restore target when the user manually scrolls (respects user intent). 4. KVO lifecycle is managed in `MovedToWindow` — started when view attaches, stopped when it detaches. **Why relative threshold (not `Y < 1.0`):** UIKit does not always reset to Y=0. It clamps to `max(0, estimatedContentSize - frameHeight)`, which can be a non-zero value. A fixed `Y < 1.0` check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value. **Why plain int storage (not NSIndexPath):** `NSIndexPath` is created inside a `using` block in `ScrollToRequested` and is disposed before the KVO callback fires. Storing `section` and `item` as `int` fields avoids this lifetime issue. **Why `ScrollToItem` instead of `SetContentOffset`:** `ScrollToItem` recalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position. `SetContentOffset` uses a raw pixel value that becomes stale after layout changes. ### Issues Fixed Fixes #34271 **Files Changed** | File | Change | |------|--------| | `src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs` | KVO observer infrastructure, scroll restore state, `SetPendingScrollRestore`, `ClearPendingScrollRestore`, `MovedToWindow` lifecycle | | `src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs` | Call `SetPendingScrollRestore` after non-animated `ScrollToItem`; capture `section`/`item` as ints inside `using` block | | `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs` | `DraggingStarted` override to clear restore target on user drag | | `src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt` | New public override: `ItemsViewDelegator2.DraggingStarted` | **Total**: fix spans 4 production files ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/ab398886-d90c-43d1-bd88-270a8e3925c1"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/e0b01b70-4959-406b-b94f-afd5601fa7d0">) |
…t after Picker interaction (#36431) > [!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! **Note:** This PR was created because the previous PR (#34356) was closed. ### Root Cause On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the `UICollectionView` `contentOffset` when the view is programmatically scrolled near or to the last item. During these transitions, UIKit recalculates scroll bounds using estimated item sizes (`NSCollectionLayoutDimension.CreateEstimated(30f)`). Because the estimated `contentSize` is smaller than the actual `contentSize`, UIKit calculates a lower maximum valid `contentOffset` and silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset: `max(0, estimatedContentSize - frameHeight)`. No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery. **Why items near the bottom are affected:** Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected. **Why only Mac Catalyst:** The same window transitions on iOS do not trigger this recalculation behavior. **Why conventional fixes fail:** Standard callbacks (`LayoutSubviews`, `ViewDidLayoutSubviews`, `Scrolled`) are not triggered because this is not a layout pass or a user scroll. KVO on `contentOffset` is the only mechanism that fires synchronously at the moment UIKit mutates the property. ### Description of Change **KVO-Based Scroll Restore** The fix uses `NSObject.AddObserver("contentOffset", ...)` (Key-Value Observing) to detect the silent reset and restore the scroll position. **Flow:** 1. After a **non-animated** `ScrollTo`, `SetPendingScrollRestore(section, item, position)` is called, storing the target index path as plain `int` fields and enabling tracking. 2. KVO `OnContentOffsetChanged` monitors every `contentOffset` change: - While the new Y is within 10px of the last known Y: treat as normal movement, update reference. - If Y drops more than 10px below the last known Y: **silent reset detected** → async-dispatches `ScrollToItem` to restore position. 3. `DraggingStarted` clears the restore target when the user manually scrolls (respects user intent). 4. KVO lifecycle is managed in `MovedToWindow` — started when view attaches, stopped when it detaches. **Why relative threshold (not `Y < 1.0`):** UIKit does not always reset to Y=0. It clamps to `max(0, estimatedContentSize - frameHeight)`, which can be a non-zero value. A fixed `Y < 1.0` check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value. **Why plain int storage (not NSIndexPath):** `NSIndexPath` is created inside a `using` block in `ScrollToRequested` and is disposed before the KVO callback fires. Storing `section` and `item` as `int` fields avoids this lifetime issue. **Why `ScrollToItem` instead of `SetContentOffset`:** `ScrollToItem` recalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position. `SetContentOffset` uses a raw pixel value that becomes stale after layout changes. ### Issues Fixed Fixes #34271 **Files Changed** | File | Change | |------|--------| | `src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs` | KVO observer infrastructure, scroll restore state, `SetPendingScrollRestore`, `ClearPendingScrollRestore`, `MovedToWindow` lifecycle | | `src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs` | Call `SetPendingScrollRestore` after non-animated `ScrollToItem`; capture `section`/`item` as ints inside `using` block | | `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs` | `DraggingStarted` override to clear restore target on user drag | | `src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt` | New public override: `ItemsViewDelegator2.DraggingStarted` | **Total**: fix spans 4 production files ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/ab398886-d90c-43d1-bd88-270a8e3925c1"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/e0b01b70-4959-406b-b94f-afd5601fa7d0">) |
…t after Picker interaction (#36431) > [!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! **Note:** This PR was created because the previous PR (#34356) was closed. ### Root Cause On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the `UICollectionView` `contentOffset` when the view is programmatically scrolled near or to the last item. During these transitions, UIKit recalculates scroll bounds using estimated item sizes (`NSCollectionLayoutDimension.CreateEstimated(30f)`). Because the estimated `contentSize` is smaller than the actual `contentSize`, UIKit calculates a lower maximum valid `contentOffset` and silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset: `max(0, estimatedContentSize - frameHeight)`. No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery. **Why items near the bottom are affected:** Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected. **Why only Mac Catalyst:** The same window transitions on iOS do not trigger this recalculation behavior. **Why conventional fixes fail:** Standard callbacks (`LayoutSubviews`, `ViewDidLayoutSubviews`, `Scrolled`) are not triggered because this is not a layout pass or a user scroll. KVO on `contentOffset` is the only mechanism that fires synchronously at the moment UIKit mutates the property. ### Description of Change **KVO-Based Scroll Restore** The fix uses `NSObject.AddObserver("contentOffset", ...)` (Key-Value Observing) to detect the silent reset and restore the scroll position. **Flow:** 1. After a **non-animated** `ScrollTo`, `SetPendingScrollRestore(section, item, position)` is called, storing the target index path as plain `int` fields and enabling tracking. 2. KVO `OnContentOffsetChanged` monitors every `contentOffset` change: - While the new Y is within 10px of the last known Y: treat as normal movement, update reference. - If Y drops more than 10px below the last known Y: **silent reset detected** → async-dispatches `ScrollToItem` to restore position. 3. `DraggingStarted` clears the restore target when the user manually scrolls (respects user intent). 4. KVO lifecycle is managed in `MovedToWindow` — started when view attaches, stopped when it detaches. **Why relative threshold (not `Y < 1.0`):** UIKit does not always reset to Y=0. It clamps to `max(0, estimatedContentSize - frameHeight)`, which can be a non-zero value. A fixed `Y < 1.0` check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value. **Why plain int storage (not NSIndexPath):** `NSIndexPath` is created inside a `using` block in `ScrollToRequested` and is disposed before the KVO callback fires. Storing `section` and `item` as `int` fields avoids this lifetime issue. **Why `ScrollToItem` instead of `SetContentOffset`:** `ScrollToItem` recalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position. `SetContentOffset` uses a raw pixel value that becomes stale after layout changes. ### Issues Fixed Fixes #34271 **Files Changed** | File | Change | |------|--------| | `src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs` | KVO observer infrastructure, scroll restore state, `SetPendingScrollRestore`, `ClearPendingScrollRestore`, `MovedToWindow` lifecycle | | `src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs` | Call `SetPendingScrollRestore` after non-animated `ScrollToItem`; capture `section`/`item` as ints inside `using` block | | `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs` | `DraggingStarted` override to clear restore target on user drag | | `src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt` | New public override: `ItemsViewDelegator2.DraggingStarted` | **Total**: fix spans 4 production files ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/ab398886-d90c-43d1-bd88-270a8e3925c1"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/e0b01b70-4959-406b-b94f-afd5601fa7d0">) |
…t after Picker interaction (#36431) > [!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! **Note:** This PR was created because the previous PR (#34356) was closed. ### Root Cause On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the `UICollectionView` `contentOffset` when the view is programmatically scrolled near or to the last item. During these transitions, UIKit recalculates scroll bounds using estimated item sizes (`NSCollectionLayoutDimension.CreateEstimated(30f)`). Because the estimated `contentSize` is smaller than the actual `contentSize`, UIKit calculates a lower maximum valid `contentOffset` and silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset: `max(0, estimatedContentSize - frameHeight)`. No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery. **Why items near the bottom are affected:** Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected. **Why only Mac Catalyst:** The same window transitions on iOS do not trigger this recalculation behavior. **Why conventional fixes fail:** Standard callbacks (`LayoutSubviews`, `ViewDidLayoutSubviews`, `Scrolled`) are not triggered because this is not a layout pass or a user scroll. KVO on `contentOffset` is the only mechanism that fires synchronously at the moment UIKit mutates the property. ### Description of Change **KVO-Based Scroll Restore** The fix uses `NSObject.AddObserver("contentOffset", ...)` (Key-Value Observing) to detect the silent reset and restore the scroll position. **Flow:** 1. After a **non-animated** `ScrollTo`, `SetPendingScrollRestore(section, item, position)` is called, storing the target index path as plain `int` fields and enabling tracking. 2. KVO `OnContentOffsetChanged` monitors every `contentOffset` change: - While the new Y is within 10px of the last known Y: treat as normal movement, update reference. - If Y drops more than 10px below the last known Y: **silent reset detected** → async-dispatches `ScrollToItem` to restore position. 3. `DraggingStarted` clears the restore target when the user manually scrolls (respects user intent). 4. KVO lifecycle is managed in `MovedToWindow` — started when view attaches, stopped when it detaches. **Why relative threshold (not `Y < 1.0`):** UIKit does not always reset to Y=0. It clamps to `max(0, estimatedContentSize - frameHeight)`, which can be a non-zero value. A fixed `Y < 1.0` check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value. **Why plain int storage (not NSIndexPath):** `NSIndexPath` is created inside a `using` block in `ScrollToRequested` and is disposed before the KVO callback fires. Storing `section` and `item` as `int` fields avoids this lifetime issue. **Why `ScrollToItem` instead of `SetContentOffset`:** `ScrollToItem` recalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position. `SetContentOffset` uses a raw pixel value that becomes stale after layout changes. ### Issues Fixed Fixes #34271 **Files Changed** | File | Change | |------|--------| | `src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs` | KVO observer infrastructure, scroll restore state, `SetPendingScrollRestore`, `ClearPendingScrollRestore`, `MovedToWindow` lifecycle | | `src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs` | Call `SetPendingScrollRestore` after non-animated `ScrollToItem`; capture `section`/`item` as ints inside `using` block | | `src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs` | `DraggingStarted` override to clear restore target on user drag | | `src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt` | New public override: `ItemsViewDelegator2.DraggingStarted` | **Total**: fix spans 4 production files ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/ab398886-d90c-43d1-bd88-270a8e3925c1"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/e0b01b70-4959-406b-b94f-afd5601fa7d0">) |
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!
Note: This PR was created because the previous PR (#34356) was closed.
Root Cause
On Mac Catalyst, certain window state changes (opening/dismissing a Picker, minimizing/restoring, or resizing the window) cause UIKit to silently adjust the
UICollectionViewcontentOffsetwhen the view is programmatically scrolled near or to the last item.During these transitions, UIKit recalculates scroll bounds using estimated item sizes (
NSCollectionLayoutDimension.CreateEstimated(30f)). Because the estimatedcontentSizeis smaller than the actualcontentSize, UIKit calculates a lower maximum validcontentOffsetand silently clamps the current offset down to that value. The scroll position does not always jump to the very top — it shifts to whatever UIKit calculates as the maximum valid offset:max(0, estimatedContentSize - frameHeight). No delegate, layout, or view controller callbacks fire — the change happens entirely inside UIKit's property mutation machinery.Why items near the bottom are affected: Items near the bottom require a larger offset than UIKit's estimated bounds permit. Intermediate items sit within the estimated range and are unaffected.
Why only Mac Catalyst: The same window transitions on iOS do not trigger this recalculation behavior.
Why conventional fixes fail: Standard callbacks (
LayoutSubviews,ViewDidLayoutSubviews,Scrolled) are not triggered because this is not a layout pass or a user scroll. KVO oncontentOffsetis the only mechanism that fires synchronously at the moment UIKit mutates the property.Description of Change
KVO-Based Scroll Restore
The fix uses
NSObject.AddObserver("contentOffset", ...)(Key-Value Observing) to detect the silent reset and restore the scroll position.Flow:
ScrollTo,SetPendingScrollRestore(section, item, position)is called, storing the target index path as plainintfields and enabling tracking.OnContentOffsetChangedmonitors everycontentOffsetchange:ScrollToItemto restore position.DraggingStartedclears the restore target when the user manually scrolls (respects user intent).MovedToWindow— started when view attaches, stopped when it detaches.Why relative threshold (not
Y < 1.0): UIKit does not always reset to Y=0. It clamps tomax(0, estimatedContentSize - frameHeight), which can be a non-zero value. A fixedY < 1.0check would miss non-zero resets entirely. Comparing against the last known Y detects any sudden downward shift regardless of the absolute value.Why plain int storage (not NSIndexPath):
NSIndexPathis created inside ausingblock inScrollToRequestedand is disposed before the KVO callback fires. Storingsectionanditemasintfields avoids this lifetime issue.Why
ScrollToIteminstead ofSetContentOffset:ScrollToItemrecalculates the pixel offset using UIKit's current layout (including actual item heights), yielding the exact visual position.SetContentOffsetuses a raw pixel value that becomes stale after layout changes.Issues Fixed
Fixes #34271
Files Changed
src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.csSetPendingScrollRestore,ClearPendingScrollRestore,MovedToWindowlifecyclesrc/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.csSetPendingScrollRestoreafter non-animatedScrollToItem; capturesection/itemas ints insideusingblocksrc/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.csDraggingStartedoverride to clear restore target on user dragsrc/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txtItemsViewDelegator2.DraggingStartedTotal: fix spans 4 production files
Screenshots
34271BeforeFix.mov
34271AfterFix.mov