Skip to content

[Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction - #34356

Closed
Vignesh-SF3580 wants to merge 434 commits into
dotnet:inflight/currentfrom
Vignesh-SF3580:fix-34271
Closed

[Mac] Fix Prevent CollectionView scroll position reset on Mac Catalyst after Picker interaction#34356
Vignesh-SF3580 wants to merge 434 commits into
dotnet:inflight/currentfrom
Vignesh-SF3580:fix-34271

Conversation

@Vignesh-SF3580

@Vignesh-SF3580 Vignesh-SF3580 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

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 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
34271BeforeFix.mov
34271AfterFix.mov
@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34356

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34356"
@Vignesh-SF3580 Vignesh-SF3580 added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Mar 6, 2026
@Tamilarasan-Paranthaman Tamilarasan-Paranthaman added platform/macos macOS / Mac Catalyst area-controls-collectionview CollectionView, CarouselView, IndicatorView labels Mar 6, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review March 23, 2026 12:01
Copilot AI review requested due to automatic review settings March 23, 2026 12:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MauiCollectionView to detect silent contentOffset drops and re-ScrollToItem the pending target.
  • Arm/clear the pending “restore target” from ItemsViewHandler2 (after non-animated ScrollTo) and ItemsViewDelegator2 (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.
Comment thread src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs Outdated
Comment thread src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewDelegator2.cs
Comment thread src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs Outdated
Comment thread src/Controls/src/Core/Handlers/Items/iOS/MauiCollectionView.cs
Comment thread src/Controls/tests/TestCases.HostApp/Issues/Issue34271.cs Outdated
Comment thread src/Controls/tests/TestCases.HostApp/Issues/Issue34271.cs Outdated
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 28, 2026
@dotnet dotnet deleted a comment from Vignesh-SF3580 Apr 12, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 12, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 12, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 17, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 17, 2026
@dotnet dotnet deleted a comment from Vignesh-SF3580 Apr 17, 2026
@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Apr 17, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please review the ai's summary?

@Vignesh-SF3580

Copy link
Copy Markdown
Contributor Author

Could you please review the ai's summary?

@kubaflo Based on the AI summary, I have addressed the valid concerns.

  • Added a note clarifying that scroll restore tracking applies only to vertical offset changes, and horizontal CollectionView layouts are not covered.
  • Added a guard at the start of the async closure to ensure the native view is still valid and attached to a window before performing scroll restoration, preventing potential crashes on detached or disposed views.
  • Added a helper method to validate that the pending section and item indices are within the current data source bounds before restoring scroll position, preventing native inconsistencies if the data source changes after scheduling.
@dotnet dotnet deleted a comment from MauiBot Apr 20, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please review the ai's summary?

@Vignesh-SF3580

Copy link
Copy Markdown
Contributor Author

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.

@dotnet dotnet deleted a comment from MauiBot Apr 21, 2026
SubhikshaSf4851 and others added 9 commits June 25, 2026 21:59
…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">
@kubaflo
kubaflo force-pushed the inflight/current branch from 81295ab to 34a8f02 Compare June 25, 2026 20:02
mattleibow and others added 4 commits June 26, 2026 13:58
…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>
@kubaflo

kubaflo commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

🔍 AI-generated — automated conflict resolution & adversarial review by GitHub Copilot CLI, on behalf of @kubaflo. Please verify before merging.

Conflicts resolved & rebased onto current inflight/current

This branch had diverged badly from inflight/current (a ~1074-file diff caused by inflight/current's frequent rebases dragging in unrelated WebView/Shell/Carousel history). I merged the current tip and collapsed it back down to the real fix — 7 files:

  • MauiCollectionView.cs, ItemsViewHandler2.iOS.cs, CarouselViewDelegator2.cs, ItemsViewDelegator2.cs
  • PublicAPI.Unshipped.txt (net‑maccatalyst), Issue34271 HostApp + shared tests

Noise conflicts (WebView Android, Shell, Directory.Build.targets, eng/Versions.props, net‑android/net‑windows PublicAPI) were taken from inflight/current.

● Fixed a merge‑breaking issue

  • MauiScrollView.OnOrientationChanged() was duplicated after the merge (inflight/current already defines it; the branch re‑added an identical copy) → CS0111 duplicate‑member compile error. Removed the redundant copy (the method is unchanged and unused by this PR).

ℹ Adversarial review notes (for the author — not blocking the merge mechanics)

  • PublicAPI parity verified correct: MauiCollectionView.Dispose(bool) and ItemsViewDelegator2.DraggingStarted are inside #if MACCATALYST, so they belong only in net‑maccatalyst (not net‑ios). ✓
  • Animated scroll path: the restore appears armed only for !args.IsAnimated (ItemsViewHandler2.iOS.cs). The picker/window‑clamp reset isn't animation‑specific — please confirm ScrollTo(..., animate: true) is also covered.
  • Test coverage: Issue34271 opens/closes the Picker but doesn't hover/change the ScrollToPosition selection the way the original repro does; consider strengthening it. It also pulls images over the network — a fixed HeightRequest would make it hermetic.

Lifecycle cleanup (observer stopped on detach/dispose, base.Dispose called) looks correct, and the core behavior is correctly Mac Catalyst‑gated with no iOS regression.

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 26, 2026
@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 26, 2026
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 30, 2026
@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 30, 2026
@kubaflo
kubaflo force-pushed the inflight/current branch from 3136313 to 2542716 Compare July 3, 2026 13:46
@kubaflo

kubaflo commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Could you please create a new PR with the necessary files?

@Vignesh-SF3580

Copy link
Copy Markdown
Contributor Author

Could you please create a new PR with the necessary files?

@kubaflo Created a new PR with the necessary files.#36431

kubaflo pushed a commit that referenced this pull request Jul 17, 2026
…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">)
|
kubaflo pushed a commit that referenced this pull request Jul 22, 2026
…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">)
|
kubaflo pushed a commit that referenced this pull request Jul 28, 2026
…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">)
|
kubaflo pushed a commit that referenced this pull request Jul 29, 2026
…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">)
|
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/macos macOS / Mac Catalyst s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)