Skip to content

Commit 84e2c8b

Browse files
committed
new post draft
1 parent 3a4738c commit 84e2c8b

File tree

6 files changed

+259
-1
lines changed

6 files changed

+259
-1
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
+++
2+
title = "iOS Deep-Linking with Bevy"
3+
date = 2025-05-20
4+
[extra]
5+
tags=["rust","bevy","mobile"]
6+
custom_summary = "Deep-Linking unlocked on iOS with Bevy"
7+
hidden=true
8+
+++
9+
10+
Until very recently Bevy iOS apps had a hard time reading deep linking information. Bevy uses [winit](https://github.com/rust-windowing/winit) by default for its platform integrations like window lifecycle management. On iOS winit used to implement and register its own `AppDelegate` to receive app life cycle hooks/calls.
11+
12+
Users of Bevy therefore had only one inconvenient options to receive these hooks: Ditch winit and roll this themselves.
13+
14+
As of `winit 0.30.10` ([see release](https://github.com/rust-windowing/winit/releases/tag/v0.30.10)) we can now do much better.
15+
16+
# What is the use case?
17+
18+
"Deep linking" means that a link leads to our app being opened or foregrounded and the app knowing *that* and *what* url triggered it. The iOS jargon for this is [URL Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). Some example use cases for this are:
19+
20+
* Sharing specific content inside the app (imagine a user sharing their game profile with a friend)
21+
* File-Sharing (more on that later)
22+
* Advertisement attribution tracking (not covered here)
23+
24+
> You might wonder why this is relevant for File-Sharing on iOS. It turns out that the code that triggers your app to open after sharing a file with it is actually just using such a URL scheme to do that.
25+
26+
# High level steps
27+
28+
1. Configure your url schema in xcode
29+
2. Add a handler for the `application:(_:open:options:)` call. ([see docs](https://developer.apple.com/documentation/UIKit/UIApplicationDelegate/application(_:open:options:)))
30+
3. Handle deep link properly
31+
32+
## Configure your url schema
33+
34+
For iOS to open your app on a click on a specifically crafted URL we have to adapt our `Info.plist` file like so:
35+
36+
```xml
37+
...
38+
<key>CFBundleURLTypes</key>
39+
<array>
40+
<dict>
41+
<key>CFBundleURLName</key>
42+
<string>com.rustunit.tinytakeoff</string>
43+
<key>CFBundleURLSchemes</key>
44+
<array>
45+
<string>tinytakeoff</string>
46+
</array>
47+
</dict>
48+
</array>
49+
```
50+
51+
With this our game [tinytakeoff](https://tinytakeoff.com) will be opened when clicking on URL looking this: `tinytakeoff://fooobar?param=value`
52+
53+
Alternatively you can also make that change via the XCode UI, see the [docs to register your URL scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme).
54+
55+
So far these steps were possible already before and lead to your app being opened or foregrounded based on a click on such a URL.
56+
57+
What was not possible was step 2 and 3 to actually make use of that information inside your Bevy app.
58+
59+
## Receive app open options
60+
61+
Thanks to the [objc2](https://github.com/madsmtm/objc2) crate we can use native objc APIs without having to write objc but pure rust instead. The underlying code looks like magic and uses a bunch of macros to accomplish this. The result is that we can abstract this away into a conventient rust only crate: [bevy_ios_app_delegate](https://github.com/rustunit/bevy_ios_app_delegate).
62+
63+
Using this crate hooking into this open url life cycle call of the `AppDelegate` becomes as easy as this snippet of bevy setup code:
64+
65+
```rust
66+
pub fn plugin(app: &mut App) {
67+
// bevy setup skipped here...
68+
69+
// register our crates plugin (this is a noop on non ios platforms)
70+
app.add_plugins(IosAppDelegatePlugin);
71+
72+
// register observer that triggers if open url app delegate was called (either by app opening or forgrounding after a click on a URL scheme)
73+
app.add_observer(|trigger: Trigger<AppDelegateCall>| {
74+
info!("app delegate call: {:?}", trigger.event());
75+
});
76+
}
77+
```
78+
79+
## Handle deep link
80+
81+
This is of course very application dependant. Here are a few example of what to do:
82+
83+
| Link action | App behaviour |
84+
| --- | --- |
85+
| game user profile link | App opens the user profile of the user that created the link |
86+
| a players new record game run | The app shows this players replay after clicking their link |
87+
| file sharing | A `ShareExtension` receives a file that it wants to share with your app and opens your app using a URL schema and the app can identify what file to open using the deep linking context |
88+
89+
# Further steps
90+
91+
**universal links**
92+
93+
Aside from the custom URL schema we described above a regular web domain can be associated with your app and trigger opening it, this is called *universal linking* ([see apple docs](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)) and requires yet another `AppDelegate` [call implementation](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/application(_:continue:restorationhandler:)).
94+
95+
**push notification token**
96+
97+
Being able to make a custom `AppDelegate` implementation has more advantage than just for deep linking. We can now vastly simplify also the way we currently receive a push notification token in the [bevy_ios_notifications](https://github.com/rustunit/bevy_ios_notifications) crate. Receiving this token also happens via `AppDelegate` call and will be provided via the new `bevy_ios_app_delegate` crate soon.
98+
99+
Exciting times to build iOS apps using Bevy.
100+
101+
---
102+
103+
Do you need support building your Bevy or Rust project? Our team of experts can support you! [Contact us.](@/contact.md)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
8+
<title>rustunit</title>
9+
<meta name="description" content="Rustunit offers software development consulting with a focus on rust, game-development and large scale distributed backend services.">
10+
11+
<link rel="stylesheet" href="https://rustunit.com/fonts.css">
12+
<link rel="stylesheet" href="https://rustunit.com/style.css">
13+
14+
<script src="https://cdn.usefathom.com/script.js" data-site="TSUIPVAW" defer></script>
15+
</head>
16+
17+
<body>
18+
<header>
19+
<a href="https://rustunit.com/">HOME</a>
20+
<a href="https://rustunit.com/#games">GAMES</a>
21+
<a href="https://rustunit.com/blog/">BLOG</a>
22+
<a href="https://rustunit.com/contact/">CONTACT</a>
23+
</header>
24+
25+
<div id="blogpage">
26+
<div class="date">2025-05-20</div>
27+
28+
<div class="hidden">hidden</div>
29+
30+
<h1 class="title">
31+
iOS Deep-Linking with Bevy
32+
</h1>
33+
<div class="content">
34+
<p>Until very recently Bevy iOS apps had a hard time reading deep linking information. Bevy uses <a href="https://github.com/rust-windowing/winit">winit</a> by default for its platform integrations like window lifecycle management. On iOS winit used to implement and register its own <code>AppDelegate</code> to receive app life cycle hooks/calls.</p>
35+
<p>Users of Bevy therefore had only one inconvenient options to receive these hooks: Ditch winit and roll this themselves.</p>
36+
<p>As of <code>winit 0.30.10</code> (<a href="https://github.com/rust-windowing/winit/releases/tag/v0.30.10">see release</a>) we can now do much better.</p>
37+
<h1 id="what-is-the-use-case">What is the use case?</h1>
38+
<p>"Deep linking" means that a link leads to our app being opened or foregrounded and the app knowing <em>that</em> and <em>what</em> url triggered it. The iOS jargon for this is <a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app">URL Scheme</a>. Some example use cases for this are:</p>
39+
<ul>
40+
<li>Sharing specific content inside the app (imagine a user sharing their game profile with a friend)</li>
41+
<li>File-Sharing (more on that later)</li>
42+
<li>Advertisement attribution tracking (not covered here)</li>
43+
</ul>
44+
<blockquote>
45+
<p>You might wonder why this is relevant for File-Sharing on iOS. It turns out that the code that triggers your app to open after sharing a file with it is actually just using such a URL scheme to do that.</p>
46+
</blockquote>
47+
<h1 id="high-level-steps">High level steps</h1>
48+
<ol>
49+
<li>Configure your url schema in xcode</li>
50+
<li>Add a handler for the <code>application:(_:open:options:)</code> call. (<a href="https://developer.apple.com/documentation/UIKit/UIApplicationDelegate/application(_:open:options:)">see docs</a>)</li>
51+
<li>Handle deep link properly</li>
52+
</ol>
53+
<h2 id="configure-your-url-schema">Configure your url schema</h2>
54+
<p>For iOS to open your app on a click on a specifically crafted URL we have to adapt our <code>Info.plist</code> file like so:</p>
55+
<pre data-lang="xml" style="background-color:#212121;color:#eeffff;" class="language-xml "><code class="language-xml" data-lang="xml"><span>...
56+
</span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;</span><span>CFBundleURLTypes</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;
57+
</span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">array</span><span style="color:#89ddff;">&gt;
58+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">dict</span><span style="color:#89ddff;">&gt;
59+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;</span><span>CFBundleURLName</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;
60+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">string</span><span style="color:#89ddff;">&gt;</span><span>com.rustunit.tinytakeoff</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">string</span><span style="color:#89ddff;">&gt;
61+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;</span><span>CFBundleURLSchemes</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">key</span><span style="color:#89ddff;">&gt;
62+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">array</span><span style="color:#89ddff;">&gt;
63+
</span><span> </span><span style="color:#89ddff;">&lt;</span><span style="color:#f07178;">string</span><span style="color:#89ddff;">&gt;</span><span>tinytakeoff</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">string</span><span style="color:#89ddff;">&gt;
64+
</span><span> </span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">array</span><span style="color:#89ddff;">&gt;
65+
</span><span> </span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">dict</span><span style="color:#89ddff;">&gt;
66+
</span><span style="color:#89ddff;">&lt;/</span><span style="color:#f07178;">array</span><span style="color:#89ddff;">&gt;
67+
</span></code></pre>
68+
<p>With this our game <a href="https://tinytakeoff.com">tinytakeoff</a> will be opened when clicking on URL looking this: <code>tinytakeoff://fooobar?param=value</code></p>
69+
<p>Alternatively you can also make that change via the XCode UI, see the <a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app#Register-your-URL-scheme">docs to register your URL scheme</a>.</p>
70+
<p>So far these steps were possible already before and lead to your app being opened or foregrounded based on a click on such a URL.</p>
71+
<p>What was not possible was step 2 and 3 to actually make use of that information inside your Bevy app.</p>
72+
<h2 id="receive-app-open-options">Receive app open options</h2>
73+
<p>Thanks to the <a href="https://github.com/madsmtm/objc2">objc2</a> crate we can use native objc APIs without having to write objc but pure rust instead. The underlying code looks like magic and uses a bunch of macros to accomplish this. The result is that we can abstract this away into a conventient rust only crate: <a href="https://github.com/rustunit/bevy_ios_app_delegate">bevy_ios_app_delegate</a>.</p>
74+
<p>Using this crate hooking into this open url life cycle call of the <code>AppDelegate</code> becomes as easy as this snippet of bevy setup code:</p>
75+
<pre data-lang="rust" style="background-color:#212121;color:#eeffff;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#c792ea;">pub </span><span style="font-style:italic;color:#c792ea;">fn </span><span style="color:#82aaff;">plugin</span><span style="color:#89ddff;">(</span><span style="color:#f78c6c;">app</span><span style="color:#89ddff;">: &amp;</span><span style="color:#c792ea;">mut</span><span> App</span><span style="color:#89ddff;">) {
76+
</span><span> </span><span style="font-style:italic;color:#4a4a4a;">// bevy setup skipped here...
77+
</span><span>
78+
</span><span> </span><span style="font-style:italic;color:#4a4a4a;">// register our crates plugin (this is a noop on non ios platforms)
79+
</span><span> app</span><span style="color:#89ddff;">.</span><span style="color:#82aaff;">add_plugins</span><span style="color:#89ddff;">(</span><span>IosAppDelegatePlugin</span><span style="color:#89ddff;">);
80+
</span><span>
81+
</span><span> </span><span style="font-style:italic;color:#4a4a4a;">// register observer that triggers if open url app delegate was called (either by app opening or forgrounding after a click on a URL scheme)
82+
</span><span> app</span><span style="color:#89ddff;">.</span><span style="color:#82aaff;">add_observer</span><span style="color:#89ddff;">(|</span><span style="color:#f78c6c;">trigger</span><span style="color:#89ddff;">: </span><span>Trigger</span><span style="color:#89ddff;">&lt;</span><span>AppDelegateCall</span><span style="color:#89ddff;">&gt;| {
83+
</span><span> info!</span><span style="color:#89ddff;">(&quot;</span><span style="color:#c3e88d;">app delegate call: {:?}</span><span style="color:#89ddff;">&quot;,</span><span> trigger</span><span style="color:#89ddff;">.</span><span style="color:#82aaff;">event</span><span style="color:#89ddff;">());
84+
</span><span> </span><span style="color:#89ddff;">});
85+
</span><span style="color:#89ddff;">}
86+
</span></code></pre>
87+
<h2 id="handle-deep-link">Handle deep link</h2>
88+
<p>This is of course very application dependant. Here are a few example of what to do:</p>
89+
<table><thead><tr><th>Link action</th><th>App behaviour</th></tr></thead><tbody>
90+
<tr><td>game user profile link</td><td>App opens the user profile of the user that created the link</td></tr>
91+
<tr><td>a players new record game run</td><td>The app shows this players replay after clicking their link</td></tr>
92+
<tr><td>file sharing</td><td>A <code>ShareExtension</code> receives a file that it wants to share with your app and opens your app using a URL schema and the app can identify what file to open using the deep linking context</td></tr>
93+
</tbody></table>
94+
<h1 id="further-steps">Further steps</h1>
95+
<p><strong>universal links</strong></p>
96+
<p>Aside from the custom URL schema we described above a regular web domain can be associated with your app and trigger opening it, this is called <em>universal linking</em> (<a href="https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app">see apple docs</a>) and requires yet another <code>AppDelegate</code> <a href="https://developer.apple.com/documentation/appkit/nsapplicationdelegate/application(_:continue:restorationhandler:)">call implementation</a>.</p>
97+
<p><strong>push notification token</strong></p>
98+
<p>Being able to make a custom <code>AppDelegate</code> implementation has more advantage than just for deep linking. We can now vastly simplify also the way we currently receive a push notification token in the <a href="https://github.com/rustunit/bevy_ios_notifications">bevy_ios_notifications</a> crate. Receiving this token also happens via <code>AppDelegate</code> call and will be provided via the new <code>bevy_ios_app_delegate</code> crate soon.</p>
99+
<p>Exciting times to build iOS apps using Bevy.</p>
100+
<hr />
101+
<p>Do you need support building your Bevy or Rust project? Our team of experts can support you! <a href="https://rustunit.com/contact/">Contact us.</a></p>
102+
103+
</div>
104+
</div>
105+
106+
<footer>
107+
108+
<div class="links">
109+
110+
<a rel="me" href="https:&#x2F;&#x2F;www.linkedin.com&#x2F;company&#x2F;rustunit&#x2F;" title="LinkedIn">
111+
<img alt="LinkedIn" class="icon" src="https://rustunit.com/icons/linkedin.svg" />
112+
</a>
113+
114+
<a rel="me" href="&#x2F;contact" title="Contact">
115+
<img alt="Contact" class="icon" src="https://rustunit.com/icons/mail.svg" />
116+
</a>
117+
118+
<a rel="me" href="https:&#x2F;&#x2F;github.com&#x2F;rustunit" title="GitHub">
119+
<img alt="GitHub" class="icon" src="https://rustunit.com/icons/github.svg" />
120+
</a>
121+
122+
<a rel="me" href="https:&#x2F;&#x2F;www.youtube.com&#x2F;@rustunit_com" title="YouTube">
123+
<img alt="YouTube" class="icon" src="https://rustunit.com/icons/youtube.svg" />
124+
</a>
125+
126+
<a rel="me" href="https:&#x2F;&#x2F;mastodon.social&#x2F;@rustunit" title="Mastodon">
127+
<img alt="Mastodon" class="icon" src="https://rustunit.com/icons/mastodon.svg" />
128+
</a>
129+
130+
<a rel="me" href="https:&#x2F;&#x2F;bsky.app&#x2F;profile&#x2F;rustunit.com" title="Bsky">
131+
<img alt="Bsky" class="icon" src="https://rustunit.com/icons/bsky.svg" />
132+
</a>
133+
134+
<a rel="me" href="https:&#x2F;&#x2F;discord.gg&#x2F;MHzmYHnnsE" title="Discord">
135+
<img alt="Discord" class="icon" src="https://rustunit.com/icons/discord.svg" />
136+
</a>
137+
138+
</div>
139+
140+
141+
<div>Copyright © 2025 Rustunit B.V.</div>
142+
</footer>
143+
</body>
144+
145+
</html>

docs/blog/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ <h1>Blog</h1>
2727
<div class="posts">
2828

2929

30+
31+
3032
<div class="post">
3133
<div class="date">2025-01-02</div>
3234
<a href="https://rustunit.com/blog/2025/01-02-bevy-mobile-framerate/">

docs/sitemap.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
<loc>https://rustunit.com/blog/2025/01-02-bevy-mobile-framerate/</loc>
3535
<lastmod>2025-01-02</lastmod>
3636
</url>
37+
<url>
38+
<loc>https://rustunit.com/blog/2025/05-20-bevy-ios-deep-linking/</loc>
39+
<lastmod>2025-05-20</lastmod>
40+
</url>
3741
<url>
3842
<loc>https://rustunit.com/blog/drafts/</loc>
3943
</url>

0 commit comments

Comments
 (0)