Skip to content
7 changes: 5 additions & 2 deletions src/Core/src/Platform/iOS/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ internal IBorderStroke? Clip

void RemoveContentMask()
{
_contentMask?.RemoveFromSuperLayer();
if (_contentMask is not null && _contentMask.Handle != IntPtr.Zero)
Comment thread
PureWeen marked this conversation as resolved.
{
_contentMask.RemoveFromSuperLayer();
Comment thread
PureWeen marked this conversation as resolved.
}
_contentMask = null;
}

Expand Down Expand Up @@ -139,4 +142,4 @@ public override void WillRemoveSubview(UIView uiview)
base.WillRemoveSubview(uiview);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using CoreAnimation;
using CoreGraphics;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using UIKit;
using Xunit;

Expand Down Expand Up @@ -105,5 +110,61 @@ public async Task DoesNotPropagateToContentWithExplicitFlowDirection()

Assert.Equal(UIUserInterfaceLayoutDirection.LeftToRight, labelFlowDirection);
}

[Fact]
public async Task RemoveContentMaskDoesNotThrowWhenDisposed()
{
// Verify that removing a subview with an active clip mask does not throw
// ObjectDisposedException when the underlying CAShapeLayer is already disposed.
// Regression test for https://github.com/FFIDX-Success/ATP-Support/issues/561
await InvokeOnMainThreadAsync(() =>
{
var contentView = new Microsoft.Maui.Platform.ContentView();
contentView.Frame = new CGRect(0, 0, 200, 200);

var content = new UIView { Tag = Microsoft.Maui.Platform.ContentView.ContentTag };
content.Frame = new CGRect(0, 0, 200, 200);
contentView.AddSubview(content);

// Set a clip to trigger _contentMask creation via UpdateClip
contentView.Clip = new BorderStrokeStub();
contentView.LayoutSubviews();

// Dispose the content mask's native handle (simulating iOS deallocating the layer)
if (content.Layer.Mask is CAShapeLayer mask)
{
mask.Dispose();
}
Comment thread
Oxymoron290 marked this conversation as resolved.
Outdated

// This should not throw ObjectDisposedException
var ex = Record.Exception(() => content.RemoveFromSuperview());
Assert.Null(ex);
});
}

/// <summary>
/// Minimal IBorderStroke stub for testing clip mask creation.
/// </summary>
class BorderStrokeStub : IBorderStroke
{
public IShape Shape { get; set; } = new RectangleShape();
public Paint Stroke { get; set; }
public double StrokeThickness { get; set; } = 1;
public LineCap StrokeLineCap { get; set; }
public LineJoin StrokeLineJoin { get; set; }
public float[] StrokeDashPattern { get; set; }
Comment on lines +168 to +172

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

This stub introduces non-nullable reference-type properties (Paint Stroke and float[] StrokeDashPattern) without initialization, which can produce nullable-analysis warnings (and may fail builds if warnings are treated as errors). Initialize them to safe defaults or mark them nullable (e.g., Paint? / float[]?) to keep the test project warning-clean.

Suggested change
public Paint Stroke { get; set; }
public double StrokeThickness { get; set; } = 1;
public LineCap StrokeLineCap { get; set; }
public LineJoin StrokeLineJoin { get; set; }
public float[] StrokeDashPattern { get; set; }
public Paint? Stroke { get; set; }
public double StrokeThickness { get; set; } = 1;
public LineCap StrokeLineCap { get; set; }
public LineJoin StrokeLineJoin { get; set; }
public float[]? StrokeDashPattern { get; set; }

Copilot uses AI. Check for mistakes.
public float StrokeDashOffset { get; set; }
public float StrokeMiterLimit { get; set; }
}

class RectangleShape : IShape
{
public PathF PathForBounds(Rect bounds)
{
var path = new PathF();
path.AppendRectangle(bounds);
return path;
}
}
}
}
Loading