{"article":{"slug":"dropping-swift-entirely-from-our-bevy-ios-crates","title":"Dropping Swift entirely from our Bevy iOS crates","subtitle":null,"summary":"Rustunit explains dropping Swift from Bevy iOS crates in favor of objc2—why the Swift glue went away, what the new crates look like, and what it means for Bevy-on-iOS maintainers.","content_type":"changelog","language":"en","canonical_url":"https://rustunit.com/blog/2026/09-04-bevy-ios-crates-objc2/","author":{"name":"Rustunit","url":"https://rustunit.com/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Rustunit","url":"https://rustunit.com/","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Mobile","slug":"mobile","url":"https://listedarticles.com/topics/mobile"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Engineering","slug":"engineering","url":"https://listedarticles.com/topics/engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":988,"reading_minutes":4,"published_at":"2026-09-04T00:00:00.000Z","added_at":"2026-09-28T09:14:31.873Z","updated_at":"2026-09-28T09:14:31.873Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":false},"profile_url":"https://listedarticles.com/articles/dropping-swift-entirely-from-our-bevy-ios-crates","markdown_url":"https://listedarticles.com/articles/dropping-swift-entirely-from-our-bevy-ios-crates.md","example":false,"citation":"Rustunit, Rustunit. \"Dropping Swift entirely from our Bevy iOS crates.\" 4 Sept 2026. https://rustunit.com/blog/2026/09-04-bevy-ios-crates-objc2/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://rustunit.com/blog/2026/09-04-bevy-ios-crates-objc2/"},"body_markdown":"In this short post we look at how we got rid of the Swift packages in our bevy_ios_* crates.\n\nMost of them we used to ship in a tandem: a Rust crate on crates.io and a Swift package that you had to add to your xcode project via SPM. Over the last weeks we released them again, this time without that second half.\n\nInstalling them is now just `cargo add`.\n\n# Why?\n\nThe language barrier itself did not go anywhere, we still cross it on every call. What changed is that we do not have to build and ship that crossing ourselves anymore.\n\nThe platform side lived in that package, written in Swift or Objective-C, and rust could only reach into it through C symbols. So every crate came with its own hand-written bridge and over the years we ended up with four different ones:\n\n- `bevy_ios_safearea` : Swift functions exported as C symbols via`@_cdecl`\n- `bevy_ios_alerts` and`bevy_ios_review` : hand-written Objective-C behind a C header\n- `bevy_ios_notifications` : protobuf messages serialized across the C ABI\n- `bevy_ios_gamecenter` and`bevy_ios_iap` : swift-bridge plus a prebuilt`.xcframework`\n\nThey all had the same downsides:\n\n- every one of our READMEs started with the xcode step of adding the SPM package, screenshot included\n- the crate version and the package version had to match, that is why our install instructions said things like `bevy_ios_gamecenter = { version = \"=0.6.0\" }` . Get it wrong and you find out at link time.\n- the swift-bridge crates also needed CI to build an `.xcframework` , zip it, hash it and attach it to a GitHub release on every version\n- no simulator builds, `swift-bridge` pulls in`rust-bindgen` which broke`aarch64-apple-ios-sim` for us\n\n# What replaced it\n\nAll of it is now objc2 and its generated framework bindings: `objc2-ui-kit`, `objc2-user-notifications`, `objc2-game-kit` and `objc2-store-kit`. Apple's frameworks are Objective-C anyway, and unlike Swift, Objective-C has a dynamic runtime you can bind against generically: selectors, type encodings, `objc_msgSend`. That is what objc2 does for us under the hood, once, for every framework. The bridge is a dependency now instead of a second thing we ship.\n\nMads Marquart, who maintains objc2, gave a talk about exactly this at our Bevy Meetup #13: *Bevy on iOS - in pure Rust*. Well worth a watch.\n\nOur existing bevy_ios_app_delegate crate was built that way already. Our previous post on iOS deep-linking goes into detail here.\n\nLets look at the smallest crate first. `bevy_ios_safearea` used to be four Swift functions like this one:\n\n```\n@_cdecl(\"swift_safearea_top\")\npublic func safeareaTop(view: UnsafeRawPointer) -> Float32 {\n    let view = Unmanaged<UIView>.fromOpaque(view).takeUnretainedValue()\n    return Float32(view.safeAreaInsets.top);\n}\n// ... and three more for bottom, left, right\n```\nplus their `extern \"C\"` declarations on the rust side, plus a `Package.swift`, plus the SPM step in your project. Today this is all it takes:\n\n```\nuse objc2_ui_kit::UIView;\nlet view: &UIView = unsafe { &*ui_view.cast::<UIView>() };\nlet raw = view.safeAreaInsets();\n```\n# How does the platform call us back?\n\nSo far this is just rust calling into the platform. The other direction is what most of the protobuf and swift-bridge machinery was there for.\n\nWith objc2 we can define an Objective-C class in rust and hand it to UIKit as a delegate:\n\n```\ndefine_class!(\n    #[unsafe(super(NSObject))]\n    #[name = \"BevyIosNotificationsDelegate\"]\n    struct Delegate;\n    unsafe impl UNUserNotificationCenterDelegate for Delegate {\n        #[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]\n        fn will_present(\n            &self,\n            _center: &UNUserNotificationCenter,\n            notification: &UNNotification,\n            completion_handler: &DynBlock<dyn Fn(UNNotificationPresentationOptions)>,\n        ) {\n            send_event(IosNotificationEvents::NotificationTriggered(\n                notification.request().identifier().to_string(),\n            ));\n            let options = UNNotificationPresentationOptions::Banner\n                | UNNotificationPresentationOptions::List\n                | UNNotificationPresentationOptions::Badge\n                | UNNotificationPresentationOptions::Sound;\n            completion_handler.call((options,));\n        }\n        // ...\n    }\n);\n```\nThe delegate above sends the Event off and calls the completion handler. `send_event` is our own little helper on top of bevy_channel_message, it grabs the sender that our plugin put in place when it was built. So our Events end up in Bevy just like before.\n\nAll in all we deleted 2809 lines from `bevy_ios_notifications`, 1615 of them a single generated `Data.pb.swift` file. `bevy_ios_gamecenter` lost 4430 lines!\n\nRemember the push notification token from the deep-linking post? It is done now.\n\nThose tokens are only handed to the `UIApplicationDelegate`, so instead of swizzling from Swift we add those two callbacks to whatever delegate the app has and install our own if there is none. If `bevy_ios_app_delegate` already set one up we hook into that one.\n\n# What changes for you\n\nYou don't have to add an SPM package anymore, no linking `GameKit` or `StoreKit` by hand and no screenshots in the README. There is just one version left to get right instead of two.\n\nAnd `bevy_ios_gamecenter` and `bevy_ios_iap` build for the simulator again.\n\n# What about StoreKit 2?\n\n`bevy_ios_iap` is the one exception. StoreKit 2 is Swift only, there is no Objective-C runtime for objc2 to bind against, so here we are back to writing the bridge ourselves.\n\nSo this crate keeps a small Swift shim. But that shim now lives inside the crate: `build.rs` compiles it with `swiftc` and links it statically.\n\nThe two sides talk over a hand-written C ABI carrying JSON.\n\nNot pretty, but you still just `cargo add bevy_ios_iap` - no SPM package, no `.xcframework` release. This one is on `main` and not released yet.\n\n# Releases\n\n| crate | version | \n|---|---|\n| bevy_ios_alerts | `0.9` | \n| bevy_ios_gamecenter | `0.7` | \n| bevy_ios_notifications | `0.8` | \n| bevy_ios_review | `0.7` | \n| bevy_ios_safearea | `0.7` | \n| bevy_ios_iap | `0.11` (unreleased) | \n\n# Conclusion\n\nWe did not change the public APIs much, `bevy_ios_notifications` is the exception here so check its changelog. If you are using any of these crates go and delete the SPM dependency from your xcode project and bump the rust one.\n\nThanks to the objc2 crate for making all of this possible. Adding an iOS crate to your bevy game is just `cargo add` now!\n\nDo you need support building your Bevy or Rust project? Our team of experts can support you! Contact us.","body_html":"<p>In this short post we look at how we got rid of the Swift packages in our bevy_ios_* crates.</p>\n<p>Most of them we used to ship in a tandem: a Rust crate on crates.io and a Swift package that you had to add to your xcode project via SPM. Over the last weeks we released them again, this time without that second half.</p>\n<p>Installing them is now just <code>cargo add</code>.</p>\n<h1 id=\"why\">Why?</h1>\n<p>The language barrier itself did not go anywhere, we still cross it on every call. What changed is that we do not have to build and ship that crossing ourselves anymore.</p>\n<p>The platform side lived in that package, written in Swift or Objective-C, and rust could only reach into it through C symbols. So every crate came with its own hand-written bridge and over the years we ended up with four different ones:</p>\n<ul><li><code>bevy_ios_safearea</code> : Swift functions exported as C symbols via<code>@_cdecl</code></li><li><code>bevy_ios_alerts</code> and<code>bevy_ios_review</code> : hand-written Objective-C behind a C header</li><li><code>bevy_ios_notifications</code> : protobuf messages serialized across the C ABI</li><li><code>bevy_ios_gamecenter</code> and<code>bevy_ios_iap</code> : swift-bridge plus a prebuilt<code>.xcframework</code></li></ul>\n<p>They all had the same downsides:</p>\n<ul><li>every one of our READMEs started with the xcode step of adding the SPM package, screenshot included</li><li>the crate version and the package version had to match, that is why our install instructions said things like <code>bevy_ios_gamecenter = { version = &quot;=0.6.0&quot; }</code> . Get it wrong and you find out at link time.</li><li>the swift-bridge crates also needed CI to build an <code>.xcframework</code> , zip it, hash it and attach it to a GitHub release on every version</li><li>no simulator builds, <code>swift-bridge</code> pulls in<code>rust-bindgen</code> which broke<code>aarch64-apple-ios-sim</code> for us</li></ul>\n<h1 id=\"what-replaced-it\">What replaced it</h1>\n<p>All of it is now objc2 and its generated framework bindings: <code>objc2-ui-kit</code>, <code>objc2-user-notifications</code>, <code>objc2-game-kit</code> and <code>objc2-store-kit</code>. Apple&#39;s frameworks are Objective-C anyway, and unlike Swift, Objective-C has a dynamic runtime you can bind against generically: selectors, type encodings, <code>objc_msgSend</code>. That is what objc2 does for us under the hood, once, for every framework. The bridge is a dependency now instead of a second thing we ship.</p>\n<p>Mads Marquart, who maintains objc2, gave a talk about exactly this at our Bevy Meetup #13: <em>Bevy on iOS - in pure Rust</em>. Well worth a watch.</p>\n<p>Our existing bevy_ios_app_delegate crate was built that way already. Our previous post on iOS deep-linking goes into detail here.</p>\n<p>Lets look at the smallest crate first. <code>bevy_ios_safearea</code> used to be four Swift functions like this one:</p>\n<pre><code>@_cdecl(&quot;swift_safearea_top&quot;)\npublic func safeareaTop(view: UnsafeRawPointer) -&gt; Float32 {\n    let view = Unmanaged&lt;UIView&gt;.fromOpaque(view).takeUnretainedValue()\n    return Float32(view.safeAreaInsets.top);\n}\n// ... and three more for bottom, left, right</code></pre>\n<p>plus their <code>extern &quot;C&quot;</code> declarations on the rust side, plus a <code>Package.swift</code>, plus the SPM step in your project. Today this is all it takes:</p>\n<pre><code>use objc2_ui_kit::UIView;\nlet view: &amp;UIView = unsafe { &amp;*ui_view.cast::&lt;UIView&gt;() };\nlet raw = view.safeAreaInsets();</code></pre>\n<h1 id=\"how-does-the-platform-call-us-back\">How does the platform call us back?</h1>\n<p>So far this is just rust calling into the platform. The other direction is what most of the protobuf and swift-bridge machinery was there for.</p>\n<p>With objc2 we can define an Objective-C class in rust and hand it to UIKit as a delegate:</p>\n<pre><code>define_class!(\n    #[unsafe(super(NSObject))]\n    #[name = &quot;BevyIosNotificationsDelegate&quot;]\n    struct Delegate;\n    unsafe impl UNUserNotificationCenterDelegate for Delegate {\n        #[unsafe(method(userNotificationCenter:willPresentNotification:withCompletionHandler:))]\n        fn will_present(\n            &amp;self,\n            _center: &amp;UNUserNotificationCenter,\n            notification: &amp;UNNotification,\n            completion_handler: &amp;DynBlock&lt;dyn Fn(UNNotificationPresentationOptions)&gt;,\n        ) {\n            send_event(IosNotificationEvents::NotificationTriggered(\n                notification.request().identifier().to_string(),\n            ));\n            let options = UNNotificationPresentationOptions::Banner\n                | UNNotificationPresentationOptions::List\n                | UNNotificationPresentationOptions::Badge\n                | UNNotificationPresentationOptions::Sound;\n            completion_handler.call((options,));\n        }\n        // ...\n    }\n);</code></pre>\n<p>The delegate above sends the Event off and calls the completion handler. <code>send_event</code> is our own little helper on top of bevy_channel_message, it grabs the sender that our plugin put in place when it was built. So our Events end up in Bevy just like before.</p>\n<p>All in all we deleted 2809 lines from <code>bevy_ios_notifications</code>, 1615 of them a single generated <code>Data.pb.swift</code> file. <code>bevy_ios_gamecenter</code> lost 4430 lines!</p>\n<p>Remember the push notification token from the deep-linking post? It is done now.</p>\n<p>Those tokens are only handed to the <code>UIApplicationDelegate</code>, so instead of swizzling from Swift we add those two callbacks to whatever delegate the app has and install our own if there is none. If <code>bevy_ios_app_delegate</code> already set one up we hook into that one.</p>\n<h1 id=\"what-changes-for-you\">What changes for you</h1>\n<p>You don&#39;t have to add an SPM package anymore, no linking <code>GameKit</code> or <code>StoreKit</code> by hand and no screenshots in the README. There is just one version left to get right instead of two.</p>\n<p>And <code>bevy_ios_gamecenter</code> and <code>bevy_ios_iap</code> build for the simulator again.</p>\n<h1 id=\"what-about-storekit-2\">What about StoreKit 2?</h1>\n<p><code>bevy_ios_iap</code> is the one exception. StoreKit 2 is Swift only, there is no Objective-C runtime for objc2 to bind against, so here we are back to writing the bridge ourselves.</p>\n<p>So this crate keeps a small Swift shim. But that shim now lives inside the crate: <code>build.rs</code> compiles it with <code>swiftc</code> and links it statically.</p>\n<p>The two sides talk over a hand-written C ABI carrying JSON.</p>\n<p>Not pretty, but you still just <code>cargo add bevy_ios_iap</code> - no SPM package, no <code>.xcframework</code> release. This one is on <code>main</code> and not released yet.</p>\n<h1 id=\"releases\">Releases</h1>\n<div class=\"table-wrap\"><table><thead><tr><th>crate</th><th>version</th></tr></thead><tbody><tr><td>bevy_ios_alerts</td><td><code>0.9</code></td></tr><tr><td>bevy_ios_gamecenter</td><td><code>0.7</code></td></tr><tr><td>bevy_ios_notifications</td><td><code>0.8</code></td></tr><tr><td>bevy_ios_review</td><td><code>0.7</code></td></tr><tr><td>bevy_ios_safearea</td><td><code>0.7</code></td></tr><tr><td>bevy_ios_iap</td><td><code>0.11</code> (unreleased)</td></tr></tbody></table></div>\n<h1 id=\"conclusion\">Conclusion</h1>\n<p>We did not change the public APIs much, <code>bevy_ios_notifications</code> is the exception here so check its changelog. If you are using any of these crates go and delete the SPM dependency from your xcode project and bump the rust one.</p>\n<p>Thanks to the objc2 crate for making all of this possible. Adding an iOS crate to your bevy game is just <code>cargo add</code> now!</p>\n<p>Do you need support building your Bevy or Rust project? Our team of experts can support you! Contact us.</p>","headings":[{"level":1,"text":"Why?","id":"why"},{"level":1,"text":"What replaced it","id":"what-replaced-it"},{"level":1,"text":"How does the platform call us back?","id":"how-does-the-platform-call-us-back"},{"level":1,"text":"What changes for you","id":"what-changes-for-you"},{"level":1,"text":"What about StoreKit 2?","id":"what-about-storekit-2"},{"level":1,"text":"Releases","id":"releases"},{"level":1,"text":"Conclusion","id":"conclusion"}]}}