|
| 1 | +using Microsoft.Maui.Handlers; |
| 2 | +using UIKit; |
| 3 | +using Foundation; |
| 4 | +using Airship; |
| 5 | + |
| 6 | +namespace AirshipDotNet.Embedded.Controls |
| 7 | +{ |
| 8 | + public partial class AirshipEmbeddedViewHandler : ViewHandler<AirshipEmbeddedView, UIView> |
| 9 | + { |
| 10 | + private UIView _containerView = null!; |
| 11 | + private UIViewController? _embeddedVC; |
| 12 | + private IDisposable? _sizeObservation; |
| 13 | + private bool _embedded; |
| 14 | + |
| 15 | + public AirshipEmbeddedViewHandler() : base(PropertyMapper, CommandMapper) |
| 16 | + { |
| 17 | + } |
| 18 | + |
| 19 | + public AirshipEmbeddedViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) |
| 20 | + : base(mapper ?? PropertyMapper, commandMapper ?? CommandMapper) |
| 21 | + { |
| 22 | + } |
| 23 | + |
| 24 | + protected override UIView CreatePlatformView() |
| 25 | + { |
| 26 | + _containerView = new UIView(); |
| 27 | + return _containerView; |
| 28 | + } |
| 29 | + |
| 30 | + protected override void ConnectHandler(UIView platformView) |
| 31 | + { |
| 32 | + base.ConnectHandler(platformView); |
| 33 | + EmbedIfNeeded(); |
| 34 | + } |
| 35 | + |
| 36 | + protected override void DisconnectHandler(UIView platformView) |
| 37 | + { |
| 38 | + _sizeObservation?.Dispose(); |
| 39 | + _sizeObservation = null; |
| 40 | + _embeddedVC?.RemoveFromParentViewController(); |
| 41 | + _embeddedVC?.View?.RemoveFromSuperview(); |
| 42 | + _embeddedVC = null; |
| 43 | + _embedded = false; |
| 44 | + base.DisconnectHandler(platformView); |
| 45 | + } |
| 46 | + |
| 47 | + static partial void MapEmbeddedId(IViewHandler handler, AirshipEmbeddedView view) |
| 48 | + { |
| 49 | + if (handler is AirshipEmbeddedViewHandler h) |
| 50 | + h.EmbedIfNeeded(); |
| 51 | + } |
| 52 | + |
| 53 | + // Walk up the responder chain from a view to find its nearest containing view controller. |
| 54 | + // This is more reliable than Platform.GetCurrentUIViewController(), which returns the |
| 55 | + // topmost presented VC (e.g. ShellFlyoutRenderer) rather than the page VC that hosts our view. |
| 56 | + private static UIViewController? FindContainerViewController(UIView view) |
| 57 | + { |
| 58 | + UIResponder? responder = view.NextResponder; |
| 59 | + while (responder != null) |
| 60 | + { |
| 61 | + if (responder is UIViewController vc) |
| 62 | + return vc; |
| 63 | + responder = responder.NextResponder; |
| 64 | + } |
| 65 | + return null; |
| 66 | + } |
| 67 | + |
| 68 | + private void EmbedIfNeeded() |
| 69 | + { |
| 70 | + if (_embedded || string.IsNullOrEmpty(VirtualView?.EmbeddedId)) return; |
| 71 | + |
| 72 | + NSRunLoop.Main.BeginInvokeOnMainThread(() => |
| 73 | + { |
| 74 | + if (_embedded || string.IsNullOrEmpty(VirtualView?.EmbeddedId)) return; |
| 75 | + |
| 76 | + var parentVC = FindContainerViewController(_containerView); |
| 77 | + if (parentVC == null) return; |
| 78 | + |
| 79 | + _embedded = true; |
| 80 | + var embeddedId = VirtualView!.EmbeddedId!; |
| 81 | + |
| 82 | + _embeddedVC = UAEmbeddedViewControllerFactory.MakeViewControllerWithEmbeddedID(embeddedId); |
| 83 | + parentVC.AddChildViewController(_embeddedVC); |
| 84 | + var embeddedView = _embeddedVC.View!; |
| 85 | + embeddedView.TranslatesAutoresizingMaskIntoConstraints = false; |
| 86 | + _containerView.AddSubview(embeddedView); |
| 87 | + |
| 88 | + // Pin top/leading/trailing only — not bottom. |
| 89 | + // UIHostingController with sizingOptions = .intrinsicContentSize reports the |
| 90 | + // SwiftUI content's natural height as its intrinsicContentSize. Pinning bottom |
| 91 | + // in addition would override that and cause content to be clipped. |
| 92 | + NSLayoutConstraint.ActivateConstraints([ |
| 93 | + embeddedView.TopAnchor.ConstraintEqualTo(_containerView.TopAnchor), |
| 94 | + embeddedView.LeadingAnchor.ConstraintEqualTo(_containerView.LeadingAnchor), |
| 95 | + embeddedView.TrailingAnchor.ConstraintEqualTo(_containerView.TrailingAnchor), |
| 96 | + ]); |
| 97 | + |
| 98 | + _embeddedVC.DidMoveToParentViewController(parentVC); |
| 99 | + |
| 100 | + // Observe preferredContentSize so height stays in sync whenever Airship content |
| 101 | + // appears, disappears, or changes size — not just at the 300 ms snapshot. |
| 102 | + _sizeObservation = _embeddedVC.AddObserver( |
| 103 | + "preferredContentSize", |
| 104 | + NSKeyValueObservingOptions.New, |
| 105 | + _ => MainThread.BeginInvokeOnMainThread(FitToContent)); |
| 106 | + |
| 107 | + // Initial fit after SwiftUI has had a chance to render. |
| 108 | + System.Threading.Tasks.Task.Delay(300).ContinueWith(_ => |
| 109 | + MainThread.BeginInvokeOnMainThread(FitToContent)); |
| 110 | + }); |
| 111 | + } |
| 112 | + |
| 113 | + private void FitToContent() |
| 114 | + { |
| 115 | + var h = _embeddedVC?.PreferredContentSize.Height ?? 0; |
| 116 | + if (VirtualView == null) return; |
| 117 | + |
| 118 | + // Collapse to zero when empty so the blank area doesn't take up space. |
| 119 | + VirtualView.HeightRequest = h > 0 ? h : 0; |
| 120 | + } |
| 121 | + } |
| 122 | +} |
0 commit comments