Skip to content

Commit 79e56d8

Browse files
koblaviMrJul
authored andcommitted
fix(iOS): drain UISceneConnectionOptions on cold launch (AvaloniaUI#21362) (AvaloniaUI#21394)
When the iOS app uses scene-based lifecycle (iOS 13+), Apple delivers cold-launch user activities and URL contexts via UISceneConnectionOptions on scene:willConnectToSession:options: — NOT via the warm-path selectors scene:continueUserActivity: or scene:openURLContexts:. AvaloniaSceneDelegate ignored connectionOptions entirely, so cold-launching the app from a Universal Link or custom-scheme URL dropped the URL on the floor. The user's subscriber to IActivatableLifetime.Activated then only saw the base ActivatedEventArgs raised by the foreground transition (OnLeavingBackground), never a ProtocolActivatedEventArgs with the URL. Fix: in WillConnect, after the window is created and made key, drain both connectionOptions.UserActivities and connectionOptions.UrlContexts through the existing IAvaloniaAppInternalDelegate paths. Also add a scene:openURLContexts: handler so warm custom-scheme URLs work under scene lifecycle (previously these were silently dropped too — application:openURL: on the AppDelegate is not called when scenes are in use). Closes AvaloniaUI#21362 (cold path of AvaloniaUI#17600 / AvaloniaUI#18005). The IAvaloniaAppInternalDelegate interface gains an OpenUrl(NSUrl) method so the scene delegate can route URL contexts through the same code path the existing application:openURL:options: AppDelegate handler uses. No new tests — the Avalonia.iOS UnitTests and IntegrationTests projects don't currently exist in the repo. Manual verification steps in the issue: build any Avalonia.iOS app with Associated Domains entitlement + AASA served, force-quit, tap a Universal Link, observe the Activated event fires with ProtocolActivatedEventArgs (not base ActivatedEventArgs). Happy to add a test if maintainers can point at a scene-lifecycle test fixture to model from.
1 parent 74109cc commit 79e56d8

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public interface IAvaloniaAppDelegate
1515
internal interface IAvaloniaAppInternalDelegate
1616
{
1717
bool ContinueUserActivity(NSUserActivity userActivity);
18+
bool OpenUrl(NSUrl url);
1819
}
1920

2021
public class AvaloniaAppDelegate<TApp> : UIResponder, IUIApplicationDelegate, IAvaloniaAppDelegate, IAvaloniaAppInternalDelegate
@@ -87,6 +88,9 @@ private void CreateAndInitWindow(SingleViewLifetime lifetime)
8788

8889
[Export("application:openURL:options:")]
8990
public bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
91+
=> ((IAvaloniaAppInternalDelegate)this).OpenUrl(url);
92+
93+
bool IAvaloniaAppInternalDelegate.OpenUrl(NSUrl url)
9094
{
9195
if (Uri.TryCreate(url.ToString(), UriKind.Absolute, out var uri))
9296
{
@@ -98,7 +102,7 @@ public bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
98102
else
99103
#endif
100104
{
101-
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri));
105+
_onActivated?.Invoke(this, new ProtocolActivatedEventArgs(uri));
102106
}
103107
return true;
104108
}

src/iOS/Avalonia.iOS/AvaloniaSceneDelegate.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Foundation;
1+
using System;
2+
using Avalonia.Controls.ApplicationLifetimes;
3+
using Foundation;
24
using UIKit;
35

46
namespace Avalonia.iOS;
@@ -22,6 +24,18 @@ scene is not UIWindowScene windowScene ||
2224
InitWindow(Window, lifetime);
2325

2426
Window.MakeKeyAndVisible();
27+
28+
// Cold-launch activation: when iOS launches the app to handle a
29+
// Universal Link, a Handoff, or a custom-scheme URL, the
30+
// payload is delivered via the connectionOptions parameter —
31+
// NOT via the warm-path scene:continueUserActivity: or
32+
// scene:openURLContexts: selectors. Without draining the
33+
// options here, the URL is silently dropped and subscribers to
34+
// IActivatableLifetime.Activated only see the base
35+
// ActivatedEventArgs from the foreground transition. Fixes the
36+
// cold path of #17600 that PR #18005 only addressed for the
37+
// warm case.
38+
DispatchConnectionOptions(connectionOptions);
2539
}
2640

2741
[Export("scene:continueUserActivity:")]
@@ -31,6 +45,35 @@ public void ContinueUserActivity(UIScene scene, NSUserActivity userActivity)
3145
appDelegate?.ContinueUserActivity(userActivity);
3246
}
3347

48+
[Export("scene:openURLContexts:")]
49+
public void OpenUrlContexts(UIScene scene, NSSet<UIOpenUrlContext> urlContexts)
50+
{
51+
var appDelegate = UIApplication.SharedApplication.Delegate as IAvaloniaAppInternalDelegate;
52+
if (appDelegate is null || urlContexts is null)
53+
return;
54+
foreach (var ctx in urlContexts)
55+
appDelegate.OpenUrl(ctx.Url);
56+
}
57+
58+
private static void DispatchConnectionOptions(UISceneConnectionOptions connectionOptions)
59+
{
60+
var appDelegate = UIApplication.SharedApplication.Delegate as IAvaloniaAppInternalDelegate;
61+
if (appDelegate is null)
62+
return;
63+
64+
if (connectionOptions.UserActivities is { } activities)
65+
{
66+
foreach (NSUserActivity activity in activities)
67+
appDelegate.ContinueUserActivity(activity);
68+
}
69+
70+
if (connectionOptions.UrlContexts is { } urlContexts)
71+
{
72+
foreach (var ctx in urlContexts)
73+
appDelegate.OpenUrl(ctx.Url);
74+
}
75+
}
76+
3477
internal static void InitWindow(UIWindow window, SingleViewLifetime lifetime)
3578
{
3679
var view = new AvaloniaView();

0 commit comments

Comments
 (0)