dotnet / dotnet/maui

[iOS] GestureManager.SetupGestureManager crashes with InvalidOperationException when handler.PlatformView is null after disconnect (→ SIGABRT)

Open Beginner friendly
#35,628 5 comments 1 reaction 0 assignees View on GitHub
area-gestures platform/ios s/needs-info s/needs-repro
Dominant language
C#
Stars
23.3k
Forks
2k
Avg merge
1d 15h
Merged PRs (30d)
290

Description

## Description

`GestureManager.SetupGestureManager()` guards against a null handler, but not against a handler whose `PlatformView` is null. This produces an `InvalidOperationException` inside `GesturePlatformManager..ctor` that escapes the managed→ObjC boundary and causes a **SIGABRT** on iOS.

## Steps to Reproduce

The crash occurs when a `Border` control is re-attached to a window while its handler has already been disconnected (`DisconnectHandler` called, `PlatformView = null`). This happens in two common scenarios:
- **Navigation cache re-entry**: MAUI Shell caches a page; on re-navigation the `Window` property changes before handlers are fully reconnected.
- **CollectionView cell recycling**: A `Border` inside a `DataTemplate` is reused after its handler was disconnected during virtualisation.

Exact sequence:
1. `Border` handler connects → `GestureManager` subscribes to `VirtualView.WindowChanged`.
2. Handler is disconnected → `PlatformView = null`. Depending on timing, `HandlerChanging` may not fire (navigation cache path), so the gesture manager subscription survives.
3. `VirtualView.Window` changes → `GestureManager.OnWindowChanged` → `SetupGestureManager()`.
4. Guard `handler == null` passes (handler reference is non-null).
5. `_platformView != handler.PlatformView` (null) → `DisconnectGestures()` → `GesturePlatformManager = null`.
6. `new GesturePlatformManager(handler)` is called.
7. Inside constructor: `_handler?.ToPlatform()` → `handler.PlatformView` is null → `InvalidOperationException: Unable to convert Microsoft.Maui.Controls.Border to UIKit.UIView`.
8. Exception escapes `MovedToWindow()` through the managed→ObjC boundary → **SIGABRT**.

## The Bug in the Source

**`GestureManager.cs` — `SetupGestureManager()` (current `main`):**

```csharp
var handler = _view.Handler;

if (handler == null ||
(_didHaveWindow && _view.Window == null))
{
DisconnectGestures();
return;
}

// ...reaches here with handler.PlatformView == null
GesturePlatformManager = new GesturePlatformManager(handler); // ← crashes
```

**`GesturePlatformManager.iOS.cs` — constructor:**

```csharp
if (_handler?.ToPlatform() is not PlatformView target)
throw new ArgumentNullException(nameof(handler.PlatformView));
// ^ This is reached when PlatformView is null, but ToPlatform() actually
// throws InvalidOperationException before the null check here fires.
```

## Expected Behaviour

`SetupGestureManager()` should return early (or call `DisconnectGestures()`) when `handler.PlatformView` is null, rather than proceeding to construct a `GesturePlatformManager` that will immediately crash.

## Proposed Fix

Add a single null guard in `SetupGestureManager()`:

```csharp
if (handler == null ||
handler.PlatformView == null || // ← add this line
(_didHaveWindow && _view.Window == null))
{
DisconnectGestures();
return;
}
```

## Confirmed Stack Trace (from iOS breadcrumb log)

```
[Managed->ObjC]: InvalidOperationException: PlatformView cannot be null here
at ImageHandler.OnWindowChanged() → MauiImageView.MovedToWindow()

[Managed->ObjC]: InvalidOperationException: Unable to convert Border to UIKit.UIView
at ElementExtensions.ToPlatform(IElement view)
at ElementHandlerExtensions.ToPlatform(IElementHandler elementHandler)
at GesturePlatformManager..ctor(IViewHandler handler)
at GestureManager.SetupGestureManager()
at GestureManager.OnWindowChanged(Object sender, EventArgs e)
at VisualElement.OnWindowChanged(BindableObject bindable, Object oldValue, Object newValue)
at BindableObject.OnBindablePropertySet(...)

Unhandled(AppDomain): InvalidOperationException: Unable to convert Border to UIKit.UIView
UnhandledException: same → SIGABRT
```

Confirmed on a production pilot fleet (iPad12,1, iOS 26.2.1, build targeting net10.0-ios, MAUI 10.0.60). The crash reproduces across multiple stores and devices and is the top-crashing issue in our fleet.

## Version

- .NET: 10.0 (net10.0-ios)
- MAUI: 10.0.60
- iOS: 26.2.1 (also reproduced on 26.4.x and 26.5 beta)
- Device: iPad12,1 (iPad Pro 12.9")

## Workaround

Override `CreatePlatformView()` in a custom `BorderHandler` to return a subclass that catches the exception in `MovedToWindow()` before it escapes to UIKit:

```csharp
public class CustomBorderHandler : Microsoft.Maui.Handlers.BorderHandler
{
protected override ContentView CreatePlatformView() => new SafeContentView();

sealed class SafeContentView : ContentView
{
public override void MovedToWindow()
{
try { base.MovedToWindow(); }
catch (InvalidOperationException ex)
when (ex.Message.Contains("Unable to convert") ||
ex.Message.Contains("PlatformView cannot be null"))
{ }
}
}
}
```

This absorbs the exception before it crosses the managed→ObjC boundary. MAUI re-establishes gesture recognizers on the next proper `ConnectHandler` cycle.

Note: `CustomContentViewHandler` likely has the same vulnerability (same `ContentView` platform type, same `GestureManager` subscription path) but we have not confirmed crashes from it.

Contributor guide

Open the contributing guide

Research direction

Start in GestureManager.cs at SetupGestureManager() and compare its handler checks with the constructor in GesturePlatformManager.iOS.cs. Add coverage for a disconnected handler whose PlatformView is null, then verify the window-change path disconnects safely without reaching the constructor or causing SIGABRT.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, ios
Domain
mobile-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.