Summary
Under the scene lifecycle, an iOS app launched by a URL or document open (custom scheme, universal link, or a CFBundleDocumentTypes "Open with" file) never receives the URL. tao emits Event::Opened only for warm opens (app already running); cold-start opens are silently dropped.
Environment
Root cause
iOS delivers the launch URL two different ways under the scene lifecycle:
- App already running →
scene(_:openURLContexts:). tao handles this: TaoSceneDelegate::scene_openURLContexts parses the URLs and emits Event::Opened ✅ (src/platform_impl/ios/scene.rs).
- App not running (cold start) → the URLs arrive in
scene(_:willConnectTo:options:) via connectionOptions.URLContexts. tao's connect_scene (src/platform_impl/ios/app_state.rs) sets up the window/scene but never reads options.URLContexts(), so no Event::Opened is emitted ❌.
There's no app-delegate fallback either: for scene-based apps the launch URL is not placed in application(_:didFinishLaunchingWithOptions:)'s launchOptions, so once the scene callback ignores it, it's unrecoverable.
Expected vs actual
- Expected: a cold-start open emits
Event::Opened { urls }, matching the warm path.
- Actual: nothing is emitted; the app launches with no indication a URL/file triggered it.
Impact
tauri-plugin-deep-link's getCurrent() returns null on cold start and onOpenUrl never fires — deep links / universal links / document opens only work if the app is already running. Affects every Tauri iOS app using the plugin.
- Document-based apps (
CFBundleDocumentTypes / "Open with") can't receive a file when launched by it.
Repro
- Scene-lifecycle iOS app that logs
Event::Opened.
- Register a custom scheme (or a document type).
- Fully quit the app; trigger the scheme / "Open with" from another app.
- App launches;
Event::Opened never fires. (Warm — app already open — works.)
Possible fix
Extract the URL-parsing/emit from scene:openURLContexts: into a shared helper, and call it from connect_scene on options.URLContexts() so cold and warm opens are symmetric. If the event loop isn't ready yet the event queues via try_user_callback_transition. (Could also be extended to connectionOptions.userActivities for universal links; this covers URLContexts — custom schemes + document opens.)
diff --git a/src/platform_impl/ios/app_state.rs b/src/platform_impl/ios/app_state.rs
@@ use crate::{
- scene::multiple_scenes_enabled,
+ scene::{emit_opened_from_url_contexts, multiple_scenes_enabled},
@@ pub unsafe fn connect_scene(scene: &UIScene, options: &UISceneConnectionOptions)
}
}
+
+ // A cold-start URL/document open (the app was launched *by* it) is delivered
+ // here in `connectionOptions`, not via `scene:openURLContexts:` (which only
+ // fires while the app is already running). Without this, such opens are
+ // silently dropped under the scene lifecycle. Deliver them through the same
+ // `Event::Opened` path as the warm case; if the event loop isn't ready yet
+ // the event is queued (see `try_user_callback_transition`).
+ emit_opened_from_url_contexts(&options.URLContexts());
}
diff --git a/src/platform_impl/ios/scene.rs b/src/platform_impl/ios/scene.rs
@@ pub unsafe fn multiple_scenes_enabled() -> bool {
(*num).as_bool()
}
+/// Parse the URLs out of a set of open-URL contexts and, if any, deliver them
+/// as a single `Event::Opened`. Shared by the two ways iOS surfaces a URL/
+/// document open to a scene-based app: `scene:openURLContexts:` (app already
+/// running) and the `connectionOptions` of `scene:willConnectToSession:`
+/// (app launched *by* the open — see `app_state::connect_scene`). Keeping
+/// both on this one path makes warm and cold opens symmetric.
+pub(crate) unsafe fn emit_opened_from_url_contexts(url_contexts: &NSSet<UIOpenURLContext>) {
+ let urls: Vec<url::Url> = url_contexts
+ .iter()
+ .filter_map(|ctx| {
+ ctx.URL().absoluteString().and_then(|url| {
+ let url = url.to_string();
+ url.parse().map_err(|e| {
+ log::error!("failed to parse URL {url} from open URL context: {e}");
+ e
+ }).ok()
+ })
+ })
+ .collect();
+ if !urls.is_empty() {
+ app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls }));
+ }
+}
+
define_class!(
@@ #[unsafe(method(scene:openURLContexts:))]
fn scene_openURLContexts(&self, _scene: &UIScene, url_contexts: &NSSet<UIOpenURLContext>) {
unsafe {
- let urls: Vec<url::Url> = url_contexts
- .iter()
- .filter_map(|ctx| { /* …same parsing… */ })
- .collect();
- if !urls.is_empty() {
- app_state::handle_nonuser_event(EventWrapper::StaticEvent(Event::Opened { urls }));
- }
+ emit_opened_from_url_contexts(url_contexts);
}
}
Verified on device (iPhone 15 Pro, tao 0.35.2): with this change a cold-start document open ("Open with") now fires Event::Opened exactly like the warm case.
Summary
Under the scene lifecycle, an iOS app launched by a URL or document open (custom scheme, universal link, or a
CFBundleDocumentTypes"Open with" file) never receives the URL. tao emitsEvent::Openedonly for warm opens (app already running); cold-start opens are silently dropped.Environment
dev)Root cause
iOS delivers the launch URL two different ways under the scene lifecycle:
scene(_:openURLContexts:). tao handles this:TaoSceneDelegate::scene_openURLContextsparses the URLs and emitsEvent::Opened✅ (src/platform_impl/ios/scene.rs).scene(_:willConnectTo:options:)viaconnectionOptions.URLContexts. tao'sconnect_scene(src/platform_impl/ios/app_state.rs) sets up the window/scene but never readsoptions.URLContexts(), so noEvent::Openedis emitted ❌.There's no app-delegate fallback either: for scene-based apps the launch URL is not placed in
application(_:didFinishLaunchingWithOptions:)'slaunchOptions, so once the scene callback ignores it, it's unrecoverable.Expected vs actual
Event::Opened { urls }, matching the warm path.Impact
tauri-plugin-deep-link'sgetCurrent()returnsnullon cold start andonOpenUrlnever fires — deep links / universal links / document opens only work if the app is already running. Affects every Tauri iOS app using the plugin.CFBundleDocumentTypes/ "Open with") can't receive a file when launched by it.Repro
Event::Opened.Event::Openednever fires. (Warm — app already open — works.)Possible fix
Extract the URL-parsing/emit from
scene:openURLContexts:into a shared helper, and call it fromconnect_sceneonoptions.URLContexts()so cold and warm opens are symmetric. If the event loop isn't ready yet the event queues viatry_user_callback_transition. (Could also be extended toconnectionOptions.userActivitiesfor universal links; this coversURLContexts— custom schemes + document opens.)Verified on device (iPhone 15 Pro, tao 0.35.2): with this change a cold-start document open ("Open with") now fires
Event::Openedexactly like the warm case.