diff --git a/Avalonia.sln b/Avalonia.sln index 0adc29ffe8..0a33bc4150 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -221,6 +221,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample.Desktop", "samples\interop\NativeEmbedSample.Desktop\NativeEmbedSample.Desktop.csproj", "{F2389463-DDB4-4317-B894-D4DF9FF6B763}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample.iOS", "samples\interop\NativeEmbedSample.iOS\NativeEmbedSample.iOS.csproj", "{28DB5AD1-656D-4619-BE0B-5B475E138DF8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -2015,6 +2017,30 @@ Global {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Release|iPhone.Build.0 = Release|Any CPU {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {F2389463-DDB4-4317-B894-D4DF9FF6B763}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|iPhone.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|iPhone.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|Any CPU.Build.0 = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|iPhone.ActiveCfg = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|iPhone.Build.0 = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {28DB5AD1-656D-4619-BE0B-5B475E138DF8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2075,6 +2101,7 @@ Global {CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F2389463-DDB4-4317-B894-D4DF9FF6B763} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} + {28DB5AD1-656D-4619-BE0B-5B475E138DF8} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/interop/NativeEmbedSample.iOS/AppDelegate.cs b/samples/interop/NativeEmbedSample.iOS/AppDelegate.cs new file mode 100644 index 0000000000..9ac8ebab2e --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Avalonia.iOS; + +namespace NativeEmbedSample.iOS; + +[Register("AppDelegate")] +public partial class AppDelegate : AvaloniaAppDelegate +{ + +} diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..98f4d035c8 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,117 @@ +{ + "images": [ + { + "scale": "2x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon40.png" + }, + { + "scale": "3x", + "size": "20x20", + "idiom": "iphone", + "filename": "Icon60.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon58.png" + }, + { + "scale": "3x", + "size": "29x29", + "idiom": "iphone", + "filename": "Icon87.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon80.png" + }, + { + "scale": "3x", + "size": "40x40", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "2x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon120.png" + }, + { + "scale": "3x", + "size": "60x60", + "idiom": "iphone", + "filename": "Icon180.png" + }, + { + "scale": "1x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon20.png" + }, + { + "scale": "2x", + "size": "20x20", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "1x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon29.png" + }, + { + "scale": "2x", + "size": "29x29", + "idiom": "ipad", + "filename": "Icon58.png" + }, + { + "scale": "1x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon40.png" + }, + { + "scale": "2x", + "size": "40x40", + "idiom": "ipad", + "filename": "Icon80.png" + }, + { + "scale": "1x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon76.png" + }, + { + "scale": "2x", + "size": "76x76", + "idiom": "ipad", + "filename": "Icon152.png" + }, + { + "scale": "2x", + "size": "83.5x83.5", + "idiom": "ipad", + "filename": "Icon167.png" + }, + { + "scale": "1x", + "size": "1024x1024", + "idiom": "ios-marketing", + "filename": "Icon1024.png" + } + ], + "properties": {}, + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png new file mode 100644 index 0000000000..9174c989a9 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png new file mode 100644 index 0000000000..9c60a1761d Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png new file mode 100644 index 0000000000..448d6efb57 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png new file mode 100644 index 0000000000..8524768f8d Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png new file mode 100644 index 0000000000..60a64703c0 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png new file mode 100644 index 0000000000..45268a641c Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png new file mode 100644 index 0000000000..6a6c77a8b4 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png new file mode 100644 index 0000000000..cc7edcf5cb Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png new file mode 100644 index 0000000000..1ad04f004b Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png new file mode 100644 index 0000000000..2dd52620a8 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png new file mode 100644 index 0000000000..b058cae2f4 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png new file mode 100644 index 0000000000..02e47a2611 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png new file mode 100644 index 0000000000..4954a4bd33 Binary files /dev/null and b/samples/interop/NativeEmbedSample.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png differ diff --git a/samples/interop/NativeEmbedSample.iOS/Entitlements.plist b/samples/interop/NativeEmbedSample.iOS/Entitlements.plist new file mode 100644 index 0000000000..36a8706706 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/Entitlements.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/interop/NativeEmbedSample.iOS/Info.plist b/samples/interop/NativeEmbedSample.iOS/Info.plist new file mode 100644 index 0000000000..b9656e1c20 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDisplayName + AvaloniaNative + CFBundleIdentifier + Avalonia.Native.Sample + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/AppIcon.appiconset + + diff --git a/samples/interop/NativeEmbedSample.iOS/Main.cs b/samples/interop/NativeEmbedSample.iOS/Main.cs new file mode 100644 index 0000000000..a1d1502084 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/Main.cs @@ -0,0 +1,6 @@ +using NativeEmbedSample.iOS; + +// This is the main entry point of the application. +// If you want to use a different Application Delegate class from "AppDelegate" +// you can specify it here. +UIApplication.Main(args, null, typeof(AppDelegate)); diff --git a/samples/interop/NativeEmbedSample.iOS/NativeEmbedSample.iOS.csproj b/samples/interop/NativeEmbedSample.iOS/NativeEmbedSample.iOS.csproj new file mode 100644 index 0000000000..83611c90a1 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/NativeEmbedSample.iOS.csproj @@ -0,0 +1,16 @@ + + + net6.0-ios + manual + Exe + enable + true + 11.2 + iossimulator-x64 + + + + + + + diff --git a/samples/interop/NativeEmbedSample.iOS/Resources/LaunchScreen.xib b/samples/interop/NativeEmbedSample.iOS/Resources/LaunchScreen.xib new file mode 100644 index 0000000000..8190201742 --- /dev/null +++ b/samples/interop/NativeEmbedSample.iOS/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index e8108dd3de..0a47b152ed 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -43,7 +43,7 @@ namespace Avalonia.iOS MultipleTouchEnabled = true; } - internal class TopLevelImpl : ITopLevelImplWithTextInputMethod + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost { private readonly AvaloniaView _view; public AvaloniaView View => _view; @@ -51,6 +51,7 @@ namespace Avalonia.iOS public TopLevelImpl(AvaloniaView view) { _view = view; + NativeControlHost = new NativeControlHostImpl(_view); } public void Dispose() @@ -112,6 +113,7 @@ namespace Avalonia.iOS new AcrylicPlatformCompensationLevels(); public ITextInputMethodImpl? TextInputMethod => _view; + public INativeControlHostImpl NativeControlHost { get; } } [Export("layerClass")] diff --git a/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs new file mode 100644 index 0000000000..c5c632e7f7 --- /dev/null +++ b/src/iOS/Avalonia.iOS/NativeControlHostImpl.cs @@ -0,0 +1,183 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using CoreGraphics; +using ObjCRuntime; +using UIKit; + +namespace Avalonia.iOS +{ + internal class NativeControlHostImpl : INativeControlHostImpl + { + private readonly AvaloniaView _avaloniaView; + + public NativeControlHostImpl(AvaloniaView avaloniaView) + { + _avaloniaView = avaloniaView; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + return new UIViewControlHandle(new UIView()); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + var holder = new UIViewControlHandle(_avaloniaView); + NativeControlAttachment? attachment = null; + try + { + var child = create(holder); + // It has to be assigned to the variable before property setter is called so we dispose it on exception +#pragma warning disable IDE0017 // Simplify object initialization + attachment = new NativeControlAttachment(holder, child); +#pragma warning restore IDE0017 // Simplify object initialization + attachment.AttachedTo = this; + return attachment; + } + catch + { + attachment?.Dispose(); + holder?.Destroy(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + return new NativeControlAttachment(new UIViewControlHandle(_avaloniaView), handle) + { + AttachedTo = this + }; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle.HandleDescriptor == UIViewControlHandle.UIViewDescriptor; + + private class ViewHolder : UIView + { + public ViewHolder(IntPtr handle) : base(new NativeHandle(handle)) + { + + } + } + + private class NativeControlAttachment : INativeControlHostControlTopLevelAttachment + { + // ReSharper disable once NotAccessedField.Local (keep GC reference) + private IPlatformHandle? _child; + private UIViewControlHandle? _holder; + private UIView? _view; + private NativeControlHostImpl? _attachedTo; + + public NativeControlAttachment(UIViewControlHandle holder, IPlatformHandle child) + { + _holder = holder; + _child = child; + + _view = (child as UIViewControlHandle)?.View ?? new ViewHolder(child.Handle); + _holder.View.AddSubview(_view); + } + + [MemberNotNull(nameof(_view))] + private void CheckDisposed() + { + if (_view == null) + throw new ObjectDisposedException(nameof(NativeControlAttachment)); + } + + public void Dispose() + { + _view?.RemoveFromSuperview(); + _holder = null; + _child = null; + _attachedTo = null; + _view?.Dispose(); + _view = null; + } + + public INativeControlHostImpl? AttachedTo + { + get => _attachedTo; + set + { + CheckDisposed(); + + _attachedTo = (NativeControlHostImpl?)value; + if (_attachedTo == null) + { + _view.RemoveFromSuperview(); + } + else + { + _holder!.View.AddSubview(_view); + } + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl; + + public void HideWithSize(Size size) + { + CheckDisposed(); + _view.Hidden = true; + } + + public void ShowInBounds(Rect bounds) + { + CheckDisposed(); + if (_attachedTo == null) + throw new InvalidOperationException("The control isn't currently attached to a toplevel"); + + //bounds = _attachedTo._avaloniaView.ContentScaleFactor; + _view.Frame = new CGRect(bounds.X, bounds.Y, bounds.Width, bounds.Height); + _view.Hidden = false; + } + } + } + + public class UIViewControlHandle : INativeControlHostDestroyableControlHandle, IDisposable + { + internal const string UIViewDescriptor = "UIView"; + + private UIView? _view; + + public UIViewControlHandle(UIView view) + { + _view = view; + } + + public UIView View => _view ?? throw new ObjectDisposedException(nameof(UIViewControlHandle)); + + public string HandleDescriptor => UIViewDescriptor; + + IntPtr IPlatformHandle.Handle => _view?.Handle.Handle ?? default; + + public void Destroy() + { + Dispose(true); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + ~UIViewControlHandle() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + _view?.Dispose(); + _view = null; + if (disposing) + { + GC.SuppressFinalize(this); + } + } + } +}