Skip to content

Commit c59907c

Browse files
committed
Add embedded support first pass
1 parent 0ff2514 commit c59907c

File tree

13 files changed

+333
-2
lines changed

13 files changed

+333
-2
lines changed

Cartfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github "urbanairship/ios-library" == 20.3.0
1+
github "urbanairship/ios-library" == 20.3.2

Cartfile.resolved

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github "urbanairship/ios-library" "20.3.0"
1+
github "urbanairship/ios-library" "20.3.2"

MauiSample/HomePage.xaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
33
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
44
xmlns:mauisample="clr-namespace:MauiSample"
5+
xmlns:airship="clr-namespace:AirshipDotNet.Embedded.Controls;assembly=Airship.Net.Embedded"
56
x:DataType="mauisample:HomePageViewModel"
67
x:Class="MauiSample.HomePage">
78

@@ -48,6 +49,18 @@
4849
HorizontalOptions="Center"
4950
Command="{Binding OnPrefCenterButtonClicked}" />
5051

52+
<Label
53+
Text="Embedded View"
54+
FontSize="14"
55+
FontAttributes="Bold"
56+
Margin="0,24,0,4" />
57+
58+
<airship:AirshipEmbeddedView
59+
EmbeddedId="test"
60+
HeightRequest="400"
61+
HorizontalOptions="Fill"
62+
VerticalOptions="Start" />
63+
5164
</VerticalStackLayout>
5265
</ScrollView>
5366

MauiSample/MauiProgram.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using AirshipDotNet;
22
using AirshipDotNet.MessageCenter;
3+
using AirshipDotNet.Embedded;
34

45
namespace MauiSample;
56

@@ -11,6 +12,7 @@ public static MauiApp CreateMauiApp()
1112
builder
1213
.UseMauiApp<App>()
1314
.UseAirshipMessageCenter()
15+
.UseAirshipEmbedded()
1416
.ConfigureFonts(fonts =>
1517
{
1618
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");

MauiSample/MauiSample.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<ItemGroup Label="Common Package Dependencies" Condition="!$(UseProjectReferences)">
7373
<PackageReference Include="Airship.Net" Version="$(AirshipCrossPlatformNugetVersion)" />
7474
<PackageReference Include="Airship.Net.MessageCenter" Version="$(AirshipCrossPlatformNugetVersion)" />
75+
<PackageReference Include="Airship.Net.Embedded" Version="$(AirshipCrossPlatformNugetVersion)" />
7576
</ItemGroup>
7677

7778
<ItemGroup Label="iOS Package Dependencies" Condition="'$(TargetFramework)' == 'net10.0-ios' And !$(UseProjectReferences)">
@@ -98,6 +99,7 @@
9899
<ItemGroup Label="Common Project Dependencies" Condition="$(UseProjectReferences)">
99100
<ProjectReference Condition="$(UseProjectReferences)" Include="..\src\Airship.Net\Airship.Net.csproj" />
100101
<ProjectReference Condition="$(UseProjectReferences)" Include="..\src\Airship.Net.MessageCenter\Airship.Net.MessageCenter.csproj" />
102+
<ProjectReference Condition="$(UseProjectReferences)" Include="..\src\Airship.Net.Embedded\Airship.Net.Embedded.csproj" />
101103
</ItemGroup>
102104

103105
<ItemGroup Label="iOS Project Dependencies" Condition="'$(TargetFramework)' == 'net10.0-ios' And '$(UseProjectReferences)'">
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net10.0;net10.0-android;net10.0-ios</TargetFrameworks>
5+
<SingleProject>true</SingleProject>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<LangVersion>latest</LangVersion>
8+
<Nullable>enable</Nullable>
9+
<RootNamespace>AirshipDotNet.Embedded</RootNamespace>
10+
<IsTrimmable>false</IsTrimmable>
11+
<UseMaui>true</UseMaui>
12+
13+
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">16.0</SupportedOSPlatformVersion>
14+
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
15+
</PropertyGroup>
16+
17+
<PropertyGroup>
18+
<PackageId>Airship.Net.Embedded</PackageId>
19+
<Title>Airship Embedded View SDK</Title>
20+
<Summary>Airship Embedded View SDK .NET Library</Summary>
21+
<Description>Embedded view (MAUI) components for Airship SDK</Description>
22+
<AssemblyVersion>$(AirshipCrossPlatformVersion)</AssemblyVersion>
23+
<PackageVersion>$(AirshipCrossPlatformNugetVersion)</PackageVersion>
24+
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
25+
</PropertyGroup>
26+
27+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-ios|AnyCPU'">
28+
<CreatePackage>false</CreatePackage>
29+
<MtouchHttpClientHandler>HttpClientHandler</MtouchHttpClientHandler>
30+
</PropertyGroup>
31+
32+
<ItemGroup>
33+
<Compile Remove="Controls/Platforms/Android/**" Condition="'$(TargetFramework)' != 'net10.0-android'" />
34+
<Compile Remove="Controls/Platforms/iOS/**" Condition="'$(TargetFramework)' != 'net10.0-ios'" />
35+
</ItemGroup>
36+
37+
<!-- MAUI Package References -->
38+
<ItemGroup>
39+
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.0" />
40+
</ItemGroup>
41+
42+
<!-- Dependencies -->
43+
<ItemGroup>
44+
<ProjectReference Include="..\Airship.Net\Airship.Net.csproj" />
45+
</ItemGroup>
46+
47+
<!-- Android-specific dependencies -->
48+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
49+
<ProjectReference Include="..\..\binderator\generated\Airship.Net.Android.Automation\Airship.Net.Android.Automation.csproj" />
50+
</ItemGroup>
51+
52+
<!-- iOS-specific dependencies -->
53+
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-ios'">
54+
<ProjectReference Include="..\AirshipBindings.iOS.ObjectiveC\AirshipBindings.iOS.ObjectiveC.csproj" />
55+
</ItemGroup>
56+
57+
</Project>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Microsoft.Maui.Controls;
2+
3+
namespace AirshipDotNet.Embedded.Controls
4+
{
5+
/// <summary>
6+
/// A MAUI view that renders Airship embedded content for a given embedded ID.
7+
/// </summary>
8+
public class AirshipEmbeddedView : View
9+
{
10+
/// <summary>
11+
/// Bindable property for EmbeddedId.
12+
/// </summary>
13+
public static readonly BindableProperty EmbeddedIdProperty = BindableProperty.Create(
14+
nameof(EmbeddedId),
15+
typeof(string),
16+
typeof(AirshipEmbeddedView),
17+
default(string));
18+
19+
/// <summary>
20+
/// Gets or sets the Airship embedded content ID to display.
21+
/// </summary>
22+
public string? EmbeddedId
23+
{
24+
get => (string?)GetValue(EmbeddedIdProperty);
25+
set => SetValue(EmbeddedIdProperty, value);
26+
}
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.Maui;
2+
using Microsoft.Maui.Handlers;
3+
4+
namespace AirshipDotNet.Embedded.Controls
5+
{
6+
/// <summary>
7+
/// Handler for AirshipEmbeddedView control.
8+
/// </summary>
9+
public partial class AirshipEmbeddedViewHandler
10+
{
11+
/// <summary>
12+
/// Property mapper for AirshipEmbeddedView.
13+
/// </summary>
14+
public static IPropertyMapper<AirshipEmbeddedView, IViewHandler> PropertyMapper = new PropertyMapper<AirshipEmbeddedView, IViewHandler>(ViewHandler.ViewMapper)
15+
{
16+
[nameof(AirshipEmbeddedView.EmbeddedId)] = (handler, view) => MapEmbeddedId(handler, view)
17+
};
18+
19+
/// <summary>
20+
/// Command mapper for AirshipEmbeddedView.
21+
/// </summary>
22+
public static CommandMapper<AirshipEmbeddedView, IViewHandler> CommandMapper = new CommandMapper<AirshipEmbeddedView, IViewHandler>(ViewHandler.ViewCommandMapper);
23+
24+
static partial void MapEmbeddedId(IViewHandler handler, AirshipEmbeddedView view);
25+
}
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Microsoft.Maui.Handlers;
2+
using NativeEmbeddedView = Com.Urbanairship.Embedded.AirshipEmbeddedView;
3+
4+
namespace AirshipDotNet.Embedded.Controls
5+
{
6+
public partial class AirshipEmbeddedViewHandler : ViewHandler<AirshipEmbeddedView, NativeEmbeddedView>
7+
{
8+
public AirshipEmbeddedViewHandler() : base(PropertyMapper, CommandMapper)
9+
{
10+
}
11+
12+
public AirshipEmbeddedViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null)
13+
: base(mapper ?? PropertyMapper, commandMapper ?? CommandMapper)
14+
{
15+
}
16+
17+
protected override NativeEmbeddedView CreatePlatformView()
18+
{
19+
return new NativeEmbeddedView(Context, VirtualView!.EmbeddedId ?? "");
20+
}
21+
22+
protected override void ConnectHandler(NativeEmbeddedView platformView)
23+
{
24+
base.ConnectHandler(platformView);
25+
platformView.LayoutParameters = new Android.Widget.RelativeLayout.LayoutParams(
26+
Android.Widget.RelativeLayout.LayoutParams.MatchParent,
27+
Android.Widget.RelativeLayout.LayoutParams.MatchParent);
28+
}
29+
30+
static partial void MapEmbeddedId(IViewHandler handler, AirshipEmbeddedView view)
31+
{
32+
// EmbeddedId is passed at construction time; runtime changes are not supported.
33+
}
34+
}
35+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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

Comments
 (0)