- Fix RemoveAsync race condition: close popup before disconnecting handler - Add null-safe VirtualView access with try-catch on all platforms - Properly unsubscribe SizeChanged event in PopupPageHandler - Fix Android Window type ambiguity with fully qualified name - Fix README path in csproj The root cause was layout events firing during popup removal while the handler was being disconnected. Now we close the popup first to stop events, then clean up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
241 lines
7.8 KiB
C#
241 lines
7.8 KiB
C#
using CoreGraphics;
|
|
|
|
using Foundation;
|
|
using Mopups.Pages;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
using UIKit;
|
|
|
|
namespace Mopups.Platforms.MacCatalyst
|
|
{
|
|
internal class PopupPageRenderer : UIViewController
|
|
{
|
|
private PopupPageHandler? _renderer;
|
|
private readonly UIGestureRecognizer _tapGestureRecognizer;
|
|
private NSObject? _willChangeFrameNotificationObserver;
|
|
private NSObject? _willHideNotificationObserver;
|
|
private bool _isDisposed;
|
|
|
|
internal CGRect KeyboardBounds { get; private set; } = CGRect.Empty;
|
|
|
|
public PopupPageHandler? Handler => _renderer;
|
|
|
|
public PopupPageRenderer(PopupPageHandler handler)
|
|
{
|
|
_renderer = handler;
|
|
|
|
_tapGestureRecognizer = new UITapGestureRecognizer(OnTap)
|
|
{
|
|
CancelsTouchesInView = false
|
|
};
|
|
}
|
|
|
|
public PopupPageRenderer(IntPtr handle) : base(handle)
|
|
{
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_renderer = null;
|
|
View?.RemoveGestureRecognizer(_tapGestureRecognizer);
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
_isDisposed = true;
|
|
}
|
|
|
|
|
|
private void OnTap(UITapGestureRecognizer e)
|
|
{
|
|
var view = e.View;
|
|
var location = e.LocationInView(view);
|
|
var subview = view.HitTest(location, null);
|
|
|
|
if (Equals(subview, view))
|
|
{
|
|
try
|
|
{
|
|
(Handler.VirtualView as PopupPage)?.SendBackgroundClick();
|
|
}
|
|
catch (InvalidOperationException) { }
|
|
}
|
|
}
|
|
|
|
|
|
public override bool ShouldAutomaticallyForwardRotationMethods => true;
|
|
|
|
public override void ViewDidLayoutSubviews()
|
|
{
|
|
base.ViewDidLayoutSubviews();
|
|
UpdateSize(this);
|
|
PresentedViewController?.ViewDidLayoutSubviews();
|
|
|
|
void UpdateSize(PopupPageRenderer handler)
|
|
{
|
|
try
|
|
{
|
|
var currentElement = Handler.VirtualView as PopupPage;
|
|
|
|
if (handler.Handler.PlatformView?.Superview?.Frame == null || currentElement == null)
|
|
return;
|
|
|
|
var superviewFrame = handler.Handler.PlatformView.Superview.Frame;
|
|
var applicationFrame = UIScreen.MainScreen.ApplicationFrame;
|
|
|
|
var systemPadding = new Thickness
|
|
{
|
|
Left = applicationFrame.Left,
|
|
Top = applicationFrame.Top,
|
|
Right = applicationFrame.Right - applicationFrame.Width - applicationFrame.Left,
|
|
Bottom = applicationFrame.Bottom - applicationFrame.Height - applicationFrame.Top + handler.KeyboardBounds.Height
|
|
};
|
|
|
|
var virtualView = handler.Handler.VirtualView;
|
|
if (virtualView != null &&
|
|
((virtualView.Width != superviewFrame.Width && virtualView.Height != superviewFrame.Height)
|
|
|| currentElement.SystemPadding.Bottom != systemPadding.Bottom))
|
|
{
|
|
currentElement.BatchBegin();
|
|
currentElement.SystemPadding = systemPadding;
|
|
currentElement.Layout(new Rect(currentElement.X, currentElement.Y, superviewFrame.Width, superviewFrame.Height));
|
|
currentElement.BatchCommit();
|
|
}
|
|
}
|
|
catch (InvalidOperationException) { }
|
|
}
|
|
}
|
|
|
|
public override void ViewDidLoad()
|
|
{
|
|
base.ViewDidLoad();
|
|
|
|
ModalPresentationStyle = UIModalPresentationStyle.OverCurrentContext;
|
|
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical;
|
|
|
|
View?.AddGestureRecognizer(_tapGestureRecognizer);
|
|
}
|
|
|
|
public override void ViewDidUnload()
|
|
{
|
|
base.ViewDidUnload();
|
|
|
|
View?.RemoveGestureRecognizer(_tapGestureRecognizer);
|
|
}
|
|
|
|
public override void ViewWillAppear(bool animated)
|
|
{
|
|
base.ViewWillAppear(animated);
|
|
|
|
UnregisterAllObservers();
|
|
|
|
_willChangeFrameNotificationObserver = UIKeyboard.Notifications.ObserveWillShow((sender, args) =>
|
|
{
|
|
KeyboardBounds = args.FrameBegin;
|
|
ViewDidLayoutSubviews();
|
|
});
|
|
|
|
_willHideNotificationObserver = UIKeyboard.Notifications.ObserveWillHide(async (sender, args) =>
|
|
{
|
|
KeyboardBounds = CGRect.Empty;
|
|
|
|
if (args.AnimationDuration > 0.01)
|
|
{
|
|
if (!_isDisposed)
|
|
await UIView.AnimateAsync(args.AnimationDuration, OnKeyboardAnimated);
|
|
}
|
|
else
|
|
{
|
|
ViewDidLayoutSubviews();
|
|
}
|
|
|
|
void OnKeyboardAnimated()
|
|
{
|
|
if (_isDisposed)
|
|
return;
|
|
|
|
ViewDidLayoutSubviews();
|
|
}
|
|
});
|
|
}
|
|
|
|
public override void ViewWillDisappear(bool animated)
|
|
{
|
|
base.ViewWillDisappear(animated);
|
|
|
|
UnregisterAllObservers();
|
|
}
|
|
|
|
private void UnregisterAllObservers()
|
|
{
|
|
|
|
_willChangeFrameNotificationObserver?.Dispose();
|
|
_willHideNotificationObserver?.Dispose();
|
|
|
|
_willChangeFrameNotificationObserver = null;
|
|
_willHideNotificationObserver = null;
|
|
}
|
|
|
|
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations()
|
|
{
|
|
if ((ChildViewControllers != null) && (ChildViewControllers.Length > 0))
|
|
{
|
|
return ChildViewControllers[0].GetSupportedInterfaceOrientations();
|
|
}
|
|
return base.GetSupportedInterfaceOrientations();
|
|
}
|
|
|
|
public override UIInterfaceOrientation PreferredInterfaceOrientationForPresentation()
|
|
{
|
|
if ((ChildViewControllers != null) && (ChildViewControllers.Length > 0))
|
|
{
|
|
return ChildViewControllers[0].PreferredInterfaceOrientationForPresentation();
|
|
}
|
|
return base.PreferredInterfaceOrientationForPresentation();
|
|
}
|
|
|
|
public override UIViewController ChildViewControllerForStatusBarHidden()
|
|
{
|
|
return _renderer?.ViewController!;
|
|
}
|
|
|
|
public override bool PrefersStatusBarHidden()
|
|
{
|
|
return _renderer?.ViewController.PrefersStatusBarHidden() ?? false;
|
|
}
|
|
|
|
public override UIViewController ChildViewControllerForStatusBarStyle()
|
|
{
|
|
return _renderer?.ViewController!;
|
|
}
|
|
|
|
public override UIStatusBarStyle PreferredStatusBarStyle()
|
|
{
|
|
return (UIStatusBarStyle)(_renderer?.ViewController.PreferredStatusBarStyle())!;
|
|
}
|
|
|
|
public override bool ShouldAutorotate()
|
|
{
|
|
if ((ChildViewControllers != null) && (ChildViewControllers.Length > 0))
|
|
{
|
|
return ChildViewControllers[0].ShouldAutorotate();
|
|
}
|
|
return base.ShouldAutorotate();
|
|
}
|
|
|
|
public override bool ShouldAutorotateToInterfaceOrientation(UIInterfaceOrientation toInterfaceOrientation)
|
|
{
|
|
if ((ChildViewControllers != null) && (ChildViewControllers.Length > 0))
|
|
{
|
|
return ChildViewControllers[0].ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation);
|
|
}
|
|
return base.ShouldAutorotateToInterfaceOrientation(toInterfaceOrientation);
|
|
}
|
|
}
|
|
}
|