[Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction - #34356
[Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction#34356Vignesh-SF3580 wants to merge 434 commits into
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34356Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34356" |
57cd402 to
3d1c76f
Compare
There was a problem hiding this comment.
Pull request overview
Fixes a Mac Catalyst-specific CollectionView scroll position jump that can occur after programmatic scrolling near the end of the list and then interacting with window/UI state (e.g., dismissing a Picker). The change introduces a KVO-based mechanism on contentOffset to detect UIKit’s silent clamp/reset and restore the intended scroll position.
Changes:
- Add a Mac Catalyst-only KVO observer in
MauiCollectionViewto detect silentcontentOffsetdrops and re-ScrollToItemthe pending target. - Arm/clear the pending “restore target” from
ItemsViewHandler2(after non-animatedScrollTo) andItemsViewDelegator2(on user drag). - Add a HostApp repro page and an Appium UITest for issue #34271, plus a PublicAPI entry for the new Mac Catalyst override.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs | Adds Mac Catalyst-only KVO observer + pending scroll restore state and lifecycle wiring in MovedToWindow. |
| src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs | Captures index path parts and arms pending restore after non-animated ScrollToItem (Mac Catalyst-only). |
| src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs | Clears pending restore on DraggingStarted (Mac Catalyst-only). |
| src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | Declares the new Mac Catalyst public override API surface. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue34271.cs | Adds a repro page with a CollectionView + Picker interaction flow. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34271.cs | Adds an Appium UI test verifying scroll position remains stable after picker dismiss. |
a205a03 to
47f6b72
Compare
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the ai's summary?
@kubaflo Based on the AI summary, I have addressed the valid concerns.
|
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the ai's summary?
@kubaflo The AI summary suggested an alternative fix. I tried implementing it, but it did not resolve the reported issue. Therefore, no changes were made to the existing PR. |
…ot update dynamically at runtime (dotnet#35893) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> This pull request improves the runtime update behavior of `SearchHandler` icons (`QueryIcon`, `ClearIcon`, and `ClearPlaceholderIcon`) across Android, iOS, and Windows platforms. It ensures that changes to these icon properties are reflected visually without requiring a restart or navigation. The update also adds new test cases and a sample page to verify and demonstrate this functionality. ### Description of Change **Platform-specific icon update support:** - **Android:** - Added logic in `ShellSearchView.cs` to listen for changes to `QueryIcon`, `ClearIcon`, and `ClearPlaceholderIcon` properties and update the corresponding buttons immediately. Refactored image loading into a reusable `ApplyImageSource` method. [[1]](diffhunk://#diff-cd10009f7e4bc472ed235e594c3d3b7a87df79b413c1a0acf6edee9e73840176R243-R284) [[2]](diffhunk://#diff-cd10009f7e4bc472ed235e594c3d3b7a87df79b413c1a0acf6edee9e73840176L319-R361) - **iOS:** - Enhanced `ShellPageRendererTracker.cs` to update search bar icons at runtime for `QueryIcon`, `ClearIcon`, and `ClearPlaceholderIcon`. Added a workaround to force-refresh the clear button image, addressing iOS caching behavior. [[1]](diffhunk://#diff-7649316957d61ad7ad533c7a27d303e022ef35c1fe79b8568ca34c85ce81a719R920-R931) [[2]](diffhunk://#diff-7649316957d61ad7ad533c7a27d303e022ef35c1fe79b8568ca34c85ce81a719R1163-R1215) - **Windows:** - Updated `ShellItemHandler.Windows.cs` to support dynamic updates for `QueryIcon`. Added comments and tracking for unsupported icons (`ClearIcon`, `ClearPlaceholderIcon`). **Testing and sample coverage:** - **New sample page:** - Introduced `Issue35736` sample page to manually test toggling of all three icon properties and resetting them to defaults at runtime. - **Automated UI tests:** - Added UITest cases for all icon properties to ensure they update visually at runtime and after reset, with platform-specific handling for unsupported features. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes dotnet#35736 ### Tested the behavior in the following platforms - [x] Windows - [x] Android - [x] iOS - [x] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/7a105556-4c64-4012-8668-a5829a3c3a13"> | <video src="https://github.com/user-attachments/assets/008e86ec-d97d-46fe-a6c0-04104f7a794e"> | | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/d010995c-b244-4a1a-bbca-b4b33f0f166a"> | <video src="https://github.com/user-attachments/assets/9acb925f-e4b9-46ff-903d-f34432f22367"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ---------
…board dismiss (dotnet#35960) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details On iOS, when Shell.BackgroundColor="Transparent", navigating to a secondary page correctly shows the nav bar as transparent. After tapping an Entry and dismissing the keyboard, the navigation bar incorrectly appears with a solid opaque background, hiding the page color beneath it. ### Root Cause PR dotnet#33958 added an OnKeyboardWillHide handler in ShellPageRendererTracker to fix issue dotnet#33547, which repositions the page frame from Y=0 to Y=navBarBottom whenever the keyboard closes. For transparent Shell, Y=0 is intentional (page content extends behind the bar), but the handler moved the frame unconditionally, pulling content out from under the transparent nav bar and making it appear opaque. ### Description of Change A guard was added inside OnKeyboardWillHide to skip the frame adjustment when the navigation bar is transparent, since the page content at Y=0 is intentional in that case. The guard checks navBar.Translucent, which MAUI already sets to true for transparent Shell making it a reliable signal. The original dotnet#33547 fix is fully preserved for opaque navigation bars Validated the behavior in the following platforms - [ ] Android - [ ] Windows - [x] iOS - [ ] Mac ### Issues Fixed Fixes dotnet#35902 ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/98d6eb82-3fc9-4328-b871-4a4762efcdb3" >| <video src="https://github.com/user-attachments/assets/cb958d3b-8551-4b9e-904d-395123d2d8bb">|
…33192) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details: Unable to scrolling vertically / horizontally when scrollview placed in Shell.FlyoutHeader in Android platform. The issue doesn't occur in iOS,Mac and windows platforms. ### Root Cause: The issue occurs because Android's DrawerLayout (which powers the Shell Flyout) uses touch event interception to handle the pan gesture that opens and closes the flyout menu. When a ScrollView is placed inside the Shell.FlyoutHeader, the DrawerLayout intercepts touch events before they can reach the ScrollView. This interception happens in the DrawerLayout.OnInterceptTouchEvent() method, which is designed to detect horizontal swipe gestures. However, when a user attempts to scroll vertically within the ScrollView, the DrawerLayout interprets any touch movement as a potential flyout gesture and intercepts the event, preventing the ScrollView from receiving it. As a result, the ScrollView never processes the scroll gesture, and instead, the touch movement is consumed by the DrawerLayout's flyout pan gesture handler. This creates a conflict where vertical scrolling becomes impossible because the parent container's gesture recognition takes priority over the child ScrollView's scroll handling. The user reported that attempting to scroll the content in the flyout header would close the flyout instead of scrolling, which confirms that the DrawerLayout is intercepting and handling the touch events rather than allowing them to reach the ScrollView. ### Description of Change: The fix implements the standard Android pattern for resolving nested scrolling conflicts by using the RequestDisallowInterceptTouchEvent() API. This method allows a child view to request that its parent not intercept touch events during active touch handling. The fix was applied to both MauiScrollView.OnTouchEvent() and MauiHorizontalScrollView.OnTouchEvent() methods in the src/Core/src/Platform/Android/MauiScrollView.cs file. When a touch begins (MotionEventActions.Down), the ScrollView calls Parent?.RequestDisallowInterceptTouchEvent(true), which tells the parent DrawerLayout to stop intercepting touch events for this gesture sequence. This allows the ScrollView to receive and process all subsequent touch events in the gesture, enabling normal scrolling behavior. When the touch ends (MotionEventActions.Up) or is cancelled (MotionEventActions.Cancel), the ScrollView calls Parent?.RequestDisallowInterceptTouchEvent(false) to restore the parent's ability to intercept touch events for future gestures. This ensures that after scrolling completes, the flyout's horizontal swipe-to-close gesture continues to work normally. **Tested the behavior in the following platforms.** - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Reference: N/A ### Issues Fixed: Fixes dotnet#11764 ### Screenshots | Before | After | |---------|--------| | <Video width="380" height="1000" alt="image (6)" src="https://github.com/user-attachments/assets/3ea26fee-4aa5-43f8-be95-2ceac2ab231e" /> | <Video width="380" height="1000" alt="image (6)" src="https://github.com/user-attachments/assets/612a25c4-872f-42ca-a181-cd0e5d8358f7" /> |
…resources (dotnet#36097) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you!<!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Path.Data and Path.RenderTransform can cause memory leaks when they use shared long-lived resources (PathGeometry / ScaleTransform). The shared resource stays alive for app lifetime, and its event subscriptions can keep old Path controls alive after a page is popped. Because those Path controls stay alive, related page objects and binding context data may also be retained.This is reproducible across platforms. ### Description of Change <!-- Enter description of the fix in this section --> Added weak-event subscription handling so Path no longer holds strong references through shared geometry/transform change events. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes dotnet#35860 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> **Tested the behavior in the following platforms.** - [x] Android - [x] Windows - [x] iOS - [x] Mac | Before | After | |---------|--------| | **iOS**<br> <video src="https://github.com/user-attachments/assets/5bbfab0e-1978-46f9-91e2-a93b5b037ced" width="300" height="600"> | **iOS**<br> <video src="https://github.com/user-attachments/assets/98c362a0-a10e-411b-91b6-d612105958a2" width="300" height="600"> |
…r handler restore when property changes (dotnet#36073) <!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> This pull request addresses a regression issue where `CollectionViewHandler2` could throw a `NullReferenceException` if a layout property changed after the handler was disconnected and then reconnected. The fix ensures the internal cache is properly reinitialized, and new tests are added to verify the handler's stability in these scenarios. ### Description of Change **Bug fix:** - Ensured `_layoutPropertyCache` is reinitialized when subscribing to a new items layout in `CollectionViewHandler2`, preventing `NullReferenceException` after handler disconnect/reconnect cycles. **Testing improvements:** - Added regression tests to confirm that changing properties on `GridItemsLayout` and `LinearItemsLayout` after a disconnect/restore cycle does not cause crashes in `CollectionViewHandler2`. These tests cover various property changes and validate the fix <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes dotnet#36010 ### Tested the behavior in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <img width="954" height="693" alt="BeforeFix36010" src="https://github.com/user-attachments/assets/fb32509b-ce7c-497c-9fa4-3170181e9475" /> | <img width="947" height="689" alt="AfterFix36010" src="https://github.com/user-attachments/assets/188f6e3b-6b72-4ead-9af0-342d5d8d7c20" /> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…s MSB4019) (dotnet#36089) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change Fixes the **nightly official signed build** (`dotnet-maui`, definition 1095, dnceng/internal), which has failed **8 consecutive nights** on `refs/heads/inflight/current`. The Windows `Pack` job → step **"Build Workloads, Sign & Publish"** fails at restore/evaluation: ``` src\Workload\workloads.csproj(30,3): error MSB4019: The imported project "...\artifacts\packages\Release\Shipping\vs-workload.props" was not found. ``` ### Root cause The workload packs — `Microsoft.NET.Sdk.Maui.Manifest` and `Microsoft.Maui.Sdk` — both target **`netstandard2.0`** (via `src/Workload/Shared/Common.targets`) and both **explicitly** set `<IsPackable>true</IsPackable>` because they *are* the shipping workload packages. PR dotnet#32203 ("Don't pack .NET Standard") added a blanket rule to `Directory.Build.targets`: ```xml <IsPackable Condition="$(TargetFramework.Contains('netstandard'))">false</IsPackable> ``` `Directory.Build.targets` is auto-imported at the **end** of every project, so this assignment runs *after* the project body and **overrides** the explicit `<IsPackable>true</IsPackable>` on the workload packs. The net effect: the manifest project evaluates to `IsPackable=false`. The chain that breaks from there: 1. With `IsPackable=false`, NuGet's `Pack` target no-ops. 2. The official **"Pack, Sign"** step (`eng/pipelines/arcade/stage-pack.yml`) runs `-restore -pack -sign` **without `-build`**, so the `Build` target never runs for the manifest project. 3. `_GenerateVSWorkloadProps` (in `Microsoft.NET.Sdk.Maui.Manifest.csproj`) is hooked `AfterTargets="Build"`, so it never runs → `vs-workload.props` is never written to `$(ArtifactsShippingPackagesDir)`. 4. The next step (`-build … -projects src/Workload/workloads.csproj`) hits `workloads.csproj` line 30, whose `<Import Project="$(WorkloadMsiGenProps)" />` is unconditional and fails at evaluation → **MSB4019**. The blanket rule is also **over-broad**: it clobbers the explicit opt-in on *every* netstandard-only project, not just the workload packs (see **Scope** below). The workload/MSI leg simply failed loudest because its missing-file import is fatal at evaluation (MSB4019); the others would silently stop shipping. **Why `main` is green:** PR dotnet#32203 is on `inflight/current` only — it is *not* an ancestor of `main` or `net10.0` (verified with `git merge-base --is-ancestor`). `main` and `inflight/current` otherwise share identical `global.json` (dotnet `10.0.108`, arcade `25555.106`), identical `src/Workload`, and identical `Microsoft.Build.NoTargets 3.7.0`. The only relevant difference is the dotnet#32203 block, which is why this break is specific to `inflight/current`. ### Fix Guard the dotnet#32203 rule so it only applies when a project has **not** explicitly opted into packing: ```xml <IsPackable Condition="$(TargetFramework.Contains('netstandard')) and '$(IsPackable)' == ''">false</IsPackable> ``` - Projects that explicitly set `<IsPackable>true</IsPackable>` (the workload packs) are respected and pack again — restoring `vs-workload.props` generation. - Unmarked netstandard projects still default to not-packable, **preserving dotnet#32203's intent**. This restores the exact condition (`IsPackable=true` for the manifest project) that the passing `main` build exhibits, where `_GenerateVSWorkloadProps` runs and `vs-workload.props` is produced. ### Scope / blast radius Because dotnet#32203's blanket rule was over-broad, this guard restores packability for **6** netstandard-only projects that explicitly opt in — not just the 2 workload packs: | Project | TFM | |---|---| | `Microsoft.NET.Sdk.Maui.Manifest` | `netstandard2.0` | | `Microsoft.Maui.Sdk` | `netstandard2.0` | | `Resizetizer` | `netstandard2.0` | | `Controls.Build.Tasks` | `netstandard2.0` | | `Controls.SourceGen` | `netstandard2.0` | | `Graphics.Text.Markdig` | `netstandard2.0` | All 6 pack on `main` (which never had the dotnet#32203 rule), so this is a **`main`-parity restore**, not new shipping behavior. Multi-target libraries (e.g. `Core`, `Controls.Core`) are unaffected: `dotnet pack` evaluates `IsPackable` at the outer level where `TargetFramework=""`, so the netstandard rule never fires for them. ### How validated Root cause confirmed from the actual failing/passing build binlogs (not guesswork): | | failing `inflight/current` build `3006161` | passing `main` build `3006214` | |---|---|---| | `Microsoft.NET.Sdk.Maui.Manifest` `IsPackable` | **`false`** | **`true`** | | `_GenerateVSWorkloadProps` ran? | no (Build never ran) | yes → `vs-workload.props` produced | Verified locally on the `inflight/current` tree with `dotnet msbuild …Manifest.csproj -getProperty:IsPackable`: - **Before** fix: manifest `IsPackable=false` (reproduces the break). - **After** fix: manifest `IsPackable=true`, `Microsoft.Maui.Sdk` `IsPackable=true`. - `Microsoft.Maui.Core` (multi-target) at pack time is unchanged (`IsPackable=true`, `TargetFramework=""`; the netstandard rule only fires for single-TFM netstandard evaluations, so multi-target library packaging is unaffected). - A netstandard project with no explicit opt-in (`Controls.CustomAttributes`) still resolves `IsPackable=false` — dotnet#32203's intent preserved. The full signed Windows MSI leg only runs in the official pipeline; this change restores the property state that gates the entire pack → `vs-workload.props` → workloads handoff. **PR CI confirms the fix end-to-end:** on base `inflight/current` the `Pack Windows` leg fails at the root-cause spot; on this PR the same leg is green. The remaining red `maui-pr` legs are pre-existing on `inflight/current` (Windows Helix unit-test flakiness identical on base, and integration legs that were *skipped on base* because Pack was blocked and only run now that the fix unblocked Pack). ### Follow-up note When PR dotnet#32203's `Directory.Build.targets` change is forward-ported to `net10.0`/`main`, it must carry this `'$(IsPackable)' == ''` guard, otherwise the same break will reappear on those branches. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This pull request improves the integration test for the Aspire Service Defaults template by adding more thorough validation of the generated project. The changes focus on ensuring the template produces a buildable project and that the project file contains all required properties. Enhancements to integration test coverage: * Added a build verification step to ensure the generated project from the `maui-aspire-servicedefaults` template can be successfully built, catching issues early in the test process. * Added an assertion to verify that the generated project file includes the `<UseMauiCore>true</UseMauiCore>` property, in addition to the existing Aspire-specific properties check.
…nsions to IDrawable.Draw (dotnet#34564) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details - In a simple layout where a parent ContentView sets fixed size (Height=50, Width=100) and hosts a custom GraphicsView with an IDrawable implementation, the Draw(ICanvas canvas, RectF dirtyRect) method receives fractional dimensions (e.g., 50.28 x 100.19) instead of the expected 50 x 100. - This leads to improper drawing, such as thinner lines, off-by-one strokes, and visible border breaks due to the sub-pixel canvas size. ### Root Cause - **Android**: PlatformGraphicsView passes raw fractional dp values as dirtyRect to IDrawable.Draw(). These fractions arise because Android layouts are calculated in whole pixels, and converting back to dp results in non-integer values (e.g., 263px ÷ 2.625 = 100.19 dp). - **Windows:** PlatformGraphicsView passes raw fractional DIP values as dirtyRect to IDrawable.Draw(). This occurs because WinUI aligns layout to whole physical pixels, and converting back to DIPs produces non-integer values (e.g., 63px ÷ 1.25 = 50.4 DIPs). ### Description of Change - **Android**: The logic in PlatformGraphicsView now precomputes logical (dp) dimensions by rounding to the nearest integer and adjusts scaling factors so that the logical dimensions, when scaled, exactly match the pixel allocation. This prevents fractional dp values and sub-pixel gaps. (src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs). - **Windows**: The OnDraw method in PlatformGraphicsView now rounds the actual size to integer logical dimensions, adjusts the scale accordingly, and applies this scale via the platform canvas. This ensures the drawable area always matches the view's pixel size exactly, with no fractional or sub-pixel rendering. (src/Graphics/src/Graphics/Platforms/Windows/PlatformGraphicsView.cs). ### Issues Fixed Fixes dotnet#33110 ### Validated the behaviour in the following platforms - [x] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Platform | Before | After | |----------|----------|----------| | Android | <video src="https://github.com/user-attachments/assets/53231732-37e4-4452-b6eb-90fbb15cc6fe"> | <video src="https://github.com/user-attachments/assets/de782018-5b51-473c-9f3b-805f319cf4cf"> | | Windows | <video src="https://github.com/user-attachments/assets/19051640-33b0-436f-82bf-f770ff604310"> | <video src="https://github.com/user-attachments/assets/df1f97b9-6c32-413c-bc81-4ad917ef8d9d"> | --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…rm (dotnet#36079) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details - When the cursor is at a mid-text position in an Entry with TextTransform set, typing a character resets the cursor to the end of the text instead of advancing it by one position. ### Root Cause - On iOS/macCatalyst, each access to UITextField.AttributedText returns a new C# wrapper object around the same native ObjC instance, so the != guard in UpdateMaxLength was always true, causing an unconditional AttributedText assignment on every keystroke — which UIKit uses as a signal to reset the cursor to the end of the text. ### Description of Change - Cache textField.AttributedText in a local variable and use ReferenceEquals as the guard, so the assignment is skipped when TrimToMaxLength returns the same instance (i.e., no trimming was needed), preserving the cursor position. ### Issues Fixed Fixes dotnet#36018 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | |----------|----------| | <video src="https://github.com/user-attachments/assets/fdeccb91-df21-4fd9-a0c3-2d331f00e056"> | <video src="https://github.com/user-attachments/assets/f927a51d-bfc3-4f48-bfab-45ab9aae93d8">
81295ab to
34a8f02
Compare
…net#34791) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Summary Updates Windows App SDK and related Windows dependencies to the latest versions whose **full restore closure is available on the `dotnet-public` ADO feed** — keeping this a clean, single-file change. ### Package Updates | Package | Old Version | New Version | |---------|------------|-------------| | **Microsoft.WindowsAppSDK** | 1.8.251106002 | **1.8.260529003** | | **Microsoft.Windows.SDK.BuildTools** | 10.0.26100.4654 | **10.0.26100.8249** | | **Microsoft.Graphics.Win2D** | 1.3.2 | **1.4.0** | `Microsoft.Web.WebView2` stays at `1.0.3179.45` — the version WindowsAppSDK `1.8.x` pins via its `.WinUI` sub-package. ### Why 1.8.x and not 2.0.1 WindowsAppSDK **2.0** externalized its ML component into a separate `Microsoft.Windows.AI.MachineLearning` package (pulled transitively via `Microsoft.WindowsAppSDK.ML` → `[2.0.300, 3.0.0)`). That package is published to **nuget.org but is not mirrored to the dnceng `dotnet-public` feed**, so targeting `2.0.1` required two extra workarounds: - a `nuget.org` package source in `NuGet.config` (otherwise restore fails with `NU1101`), and - an `NU1603` suppression in `Directory.Build.props`. The latest **1.8.x** servicing release (`1.8.260529003`, ~7 months newer than the previous `1.8.251106002`) keeps a **self-contained** `.ML` sub-package — it depends on `System.Numerics.Tensors`, **not** `Microsoft.Windows.AI.MachineLearning` — and **every package in its closure is already on `dotnet-public`** (verified package-by-package). Both workarounds have been removed, so this is now a clean **`eng/Versions.props`-only** change. > Adopting WindowsAppSDK **2.0** later will require `Microsoft.Windows.AI.MachineLearning` (≥ `2.0.300`) to be mirrored into the dnceng `dotnet-public` feed first; until then, 2.0 can't be restored without a `nuget.org` source. ### Verification Every package in the `1.8.260529003` restore closure was confirmed present on `dotnet-public`. The actual Windows restore/build is validated by CI on this PR (the host used to prepare it is macOS). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: kubaflo <kubaflo@users.noreply.github.com> Co-authored-by: Jakub Florkowski <42434498+kubaflo@users.noreply.github.com>
…et#30378) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue details in mac when the app enters in full screen mode, titlebar content is not aligned to the left. ### Description of Change <!-- Enter description of the fix in this section --> Added logic to adjust the TitleBar content margin based on full-screen state (Mac Catalyst 16+). When in full-screen, left margin is removed; otherwise, a margin of 80 is applied to align with traffic light spacing. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes dotnet#30248 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> **Tested the behavior in the following platforms.** - [ ] Android - [ ] Windows - [ ] iOS - [x] Mac | Before | After | |---------|--------| | **Mac**<br> <video src="https://github.com/user-attachments/assets/1224b070-4913-46ce-929d-bfd97f20ad47" width="600" height="300"> | **Mac**<br> <video src="https://github.com/user-attachments/assets/dd7de8c9-667a-486f-86c9-fcd9e450150b" width="600" height="300"> |
# Conflicts: # Directory.Build.targets # eng/Versions.props # src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSearchView.cs # src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs # src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt # src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt # src/Core/src/Handlers/WebView/WebViewHandler.Android.cs # src/Core/src/Platform/Android/MauiWebView.cs # src/Core/src/Platform/Android/MauiWebViewClient.cs # src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt
…fact) inflight/current already defines MauiScrollView.OnOrientationChanged; the PR branch re-added an identical copy, producing a CS0111 duplicate-member error after merge. Take inflight's version since the method is unchanged and the PR does not call it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Conflicts resolved & rebased onto current
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Could you please create a new PR with the necessary files? |
…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!
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