[iOS] `CollectionView` first item draws **under** the navigation bar — `ContentInsetAdjustmentBehavior` overrides via handler customization have no effect
- 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
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