dotnet / dotnet/maui

[iOS] `CollectionView` first item draws **under** the navigation bar — `ContentInsetAdjustmentBehavior` overrides via handler customization have no effect

Open
#35,378 1 comment 2 reactions 0 assignees View on GitHub
s/triaged
Dominant language
C#
Stars
23.3k
Forks
2k
Avg merge
1d 15h
Merged PRs (30d)
290

Description

## Description

When a `CollectionView` is placed inside a `Layout` that uses `SafeAreaEdges` for edge-to-edge vertical bounds (so the scroll view extends behind a `UINavigationBar`), the underlying `UICollectionView` does **not** receive a top content inset matching the nav bar height. The first item is rendered at `contentOffset.y = 0` (visually under the nav bar / large title), instead of starting below the bar and scrolling under it on user gesture.

Manually setting `UIScrollView.ContentInsetAdjustmentBehavior` to either `.Automatic` or `.Always` via the handler's `PlatformView` (in `HandlerChanged`) **has no observable effect** — the first item still renders under the nav bar.

This is a regression from the equivalent behaviour that `Microsoft.Maui.Controls.VirtualListView` (community implementation) achieved on top of `UICollectionView` in the same .NET 10 environment.

## Steps to reproduce

> **Reproduction Project** https://github.com/Redth/maui-issue-repros/tree/main/dotnet/maui/issue-35378

1. Create a .NET MAUI 10 app whose `MainPage` is wrapped in a `NavigationPage` (or any UI that produces a `UINavigationController` on iOS — `ContentPage` with `ToolbarItems`, `Shell`, etc.).
2. On the page, place a `Layout` (e.g. `Grid`) with `SafeAreaEdges="Container, None"` (horizontal=`Container`, **vertical=`None`** — meaning extend behind top status/nav bar and bottom home indicator).
3. Inside that layout, place a `CollectionView` with at least 20 items.
4. Run on an iOS 26.4 iPhone simulator (or any device with a translucent nav bar / large title).

### Expected

The `UICollectionView` extends behind the nav bar (correct, observable). Because the standard UIKit `UIScrollView.contentInsetAdjustmentBehavior` is `.automatic` by default and the scroll view is inside a `UINavigationController`, UIKit should add `safeAreaInsets.top == navBarHeight` as a content inset. The **first item should render BELOW the nav bar** at first paint, and the user should be able to scroll items **under** the translucent bar.

### Actual

The first item renders at `y = 0` of the scroll view's bounds — i.e. **under** the nav bar from the moment the view appears. Scrolling still moves items under the bar correctly, but the initial offset is wrong: there is no auto top inset.

### Workaround attempted (also broken)

The following handler customization, applied on `HandlerChanged`, was expected to force UIKit to apply safe-area-derived content insets. Both `.Automatic` and `.Always` were tried — **neither has any observable effect on the rendering**:

```csharp
public TimelineView()
{
InitializeComponent();

#if IOS
collectionTimeline.HandlerChanged += OnHandlerChanged;
#endif
}

#if IOS
void OnHandlerChanged(object sender, EventArgs e)
{
if (collectionTimeline.Handler?.PlatformView is UIKit.UIScrollView sv)
{
sv.ContentInsetAdjustmentBehavior =
UIKit.UIScrollViewContentInsetAdjustmentBehavior.Always; // or .Automatic
}
}
#endif
```

The cast succeeds at runtime (`PlatformView` is the `UICollectionView`, which inherits from `UIScrollView`), but the assignment appears to be either:

- ignored by MAUI's `CollectionViewHandler`, **or**
- overridden later in a layout / mapping pass by the handler resetting it to `.Never`, **or**
- shadowed by MAUI explicitly writing `ContentInset` to `UIEdgeInsets.Zero` (which prevents the safe-area-derived inset from ever taking effect even when `.Always` is set).

Without source diving into the iOS handler I can't tell which of those it is, but **all three indicate that MAUI's `CollectionView` iOS handler is incompatible with consumers that need the classic UIKit auto-inset-under-nav-bar pattern**.

## Minimal XAML repro

```xml











```

With `Items = Enumerable.Range(1, 50).Select(i => $"Item {i}").ToList()` and the page hosted inside a `NavigationPage`, item 1 will be drawn under the nav bar at first paint.

## Expected vs actual visual

| Scenario | Expected | Actual |
|---|---|---|
| Initial render | Item 1 below the nav bar, with `contentInset.top == navBarHeight` so it is fully visible | Item 1 at `y=0` of the scroll view — top half clipped under the nav bar |
| User scrolls down | Items scroll under the translucent nav bar | Works correctly |
| User scrolls back to top | Item 1 visible below the nav bar again | Top of Item 1 is hidden under the nav bar (because there is no top inset) |

Translation: scrolling *under* the bar works (good); the initial *content offset* doesn't account for the nav bar (bad). UIKit's default `.Automatic` `contentInsetAdjustmentBehavior` would fix both, but MAUI's handler appears to suppress that.

## Suggested fix

`CollectionViewHandler` (iOS) should either:

1. **Not** overwrite `ContentInsetAdjustmentBehavior` once a consumer has set it via the handler's `PlatformView`. The current behaviour silently undoes user customizations, which is a foot-gun for anyone trying to interop with UIKit safe-area patterns.
2. **Respect `SafeAreaEdges`** in a way that allows `Vertical=None` to mean *"let UIKit's `.automatic` inset adjustment handle the top"*, not *"set `ContentInsetAdjustmentBehavior=.Never` and `ContentInset=Zero`"*. With the new `.NET 10` cross-platform safe-area API, the docs imply (per `redth:maui-safe-area`) that `ScrollView` with `SafeAreaEdges="Default"` maps to UIKit's `.automatic` — `CollectionView` should follow the same model.
3. Alternatively, expose a MAUI-level property (e.g. `CollectionView.ContentInsetAdjustmentBehavior`) so consumers don't have to reach through the handler at all.

## Environment

| Item | Version |
|---|---|
| .NET SDK | `10.0.203` (`global.json` pins to this) |
| MAUI workload | `10.0.20/10.0.100` (manifest source: SDK 10.0.200) |
| Workload version | `10.0.203.1` |
| `Microsoft.Maui.Controls` package | `10.0.60` |
| iOS workload | `26.4.10259/10.0.100` |
| Target framework | `net10.0-ios` |
| `SupportedOSPlatformVersion` | `15.0` |
| Simulator | iPhone 17 Pro, iOS 26.4 (`23E244`) |
| Host | macOS Darwin 25.4 |

## Repro project context

The relevant XAML in that project is essentially:

```xml











```

## Related observations

- `SafeAreaEdges="Container, None"` correctly insets the **horizontal** edges in landscape on a notched iPhone (content stays clear of the notch), so the new safe-area API itself is wired up correctly for the layout — the issue is specifically the `CollectionView` iOS handler's relationship with `UIScrollView.ContentInsetAdjustmentBehavior` / `ContentInset.top`.
- The bottom edge works as expected: with `Vertical=None`, content correctly scrolls behind the translucent tab bar / home indicator, including the `CollectionView.Footer` spacer.
- `RefreshView` (parent of `CollectionView`) does **not** appear to surface `SafeAreaEdges` (XAML parse error `MAUIX2002: No accessible property … "SafeAreaEdges"` when set on `RefreshView`). That itself may be intentional, but it removes one possible workaround layer.

## Severity

Annoying-but-not-blocking visual regression. Affects every `CollectionView` consumer that wants the iOS-native "scroll under the translucent nav bar" pattern. Will be highly visible on every iOS app moving from `VirtualListView` / `ListView` / `UICollectionView` to MAUI's `CollectionView` on iOS 16+ (large titles) and iOS 26+ (translucent / liquid-glass bars).

Contributor guide

Open the contributing guide

Research direction

Start with the iOS CollectionViewHandler and the HandlerChanged PlatformView customization described in the issue, then run the linked issue-35378 reproduction project on an iOS simulator. Trace when ContentInsetAdjustmentBehavior and ContentInset are set; done means the first CollectionView item appears below the navigation bar while scrolling under it still works, with a regression test or reproduction verification covering the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, ios
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.