#![expect(non_snake_case, reason = "UIKit does not use Rust naming conventions")]
use std::cell::{Cell, RefCell, RefMut};
use std::ptr::NonNull;
use bevy_app::{App, AppExit, PluginsState};
use bevy_ecs::entity::Entity;
use bevy_ecs::event::{Event, EventReader};
use bevy_ecs::query::{QuerySingleError, With};
use bevy_tasks::tick_global_task_pools_on_main_thread;
use bevy_window::{PrimaryWindow, Window, WindowCreated, WindowEvent};
use dispatch2::MainThreadBound;
use objc2::rc::{Allocated, Retained};
use objc2::runtime::AnyObject;
use objc2::{available, define_class, msg_send, ClassType, MainThreadMarker, MainThreadOnly};
use objc2_core_foundation::{kCFRunLoopDefaultMode, CFRunLoopGetMain, CFRunLoopPerformBlock};
use objc2_foundation::{
ns_string, NSDictionary, NSObject, NSObjectProtocol, NSSet, NSStringFromClass, NSURL,
};
use objc2_ui_kit::{
UIApplication, UIApplicationDelegate, UIApplicationLaunchOptionsKey, UIApplicationMain,
UIApplicationOpenURLOptionsKey, UISceneConfiguration, UISceneConnectionOptions, UISceneSession,
UIWindow,
};
use tracing::{error, trace, warn};
use crate::scene_delegate::SceneDelegate;
use crate::windows::{setup_window, WorldHelper};
use crate::UIKitWindows;
pub fn uikit_runner(mut app: App) -> AppExit {
let mtm = MainThreadMarker::new().expect("UIKit applications must be run on the main thread");
trace!("polling plugins until they're ready");
while app.plugins_state() == PluginsState::Adding {
tick_global_task_pools_on_main_thread(); }
if app.plugins_state() == PluginsState::Ready {
app.finish();
app.cleanup();
}
assert_eq!(app.plugins_state(), PluginsState::Cleaned);
trace!("starting UIApplicationMain");
let previous_app = APP_STATE.get(mtm).replace(Some(app));
if previous_app.is_some() {
panic!("tried to run `uikit_runner` twice");
}
let res = unsafe {
let _ = mtm; UIApplicationMain(
*libc::_NSGetArgc(),
NonNull::new(*libc::_NSGetArgv()).unwrap(),
None, Some(&NSStringFromClass(ApplicationDelegate::class())),
)
};
AppExit::from_code(res.try_into().unwrap_or(1))
}
pub fn disallow_app_exit(mut exit_events: EventReader<AppExit>) {
for event in exit_events.read() {
if cfg!(debug_assertions) {
panic!("`AppExit::{event:?}` is not supported on iOS");
}
}
}
type AppState = RefCell<Option<App>>;
static APP_STATE: MainThreadBound<AppState> = {
let mtm = unsafe { MainThreadMarker::new_unchecked() };
MainThreadBound::new(RefCell::new(None), mtm)
};
#[track_caller]
pub(crate) fn access_app(mtm: MainThreadMarker) -> RefMut<'static, App> {
RefMut::map(APP_STATE.get(mtm).borrow_mut(), |app| {
app.as_mut().expect("application was not initialized")
})
}
fn queue_closure(_mtm: MainThreadMarker, closure: impl FnOnce() + 'static) {
let run_loop = unsafe { CFRunLoopGetMain() }.unwrap();
let closure = Cell::new(Some(closure));
let block = block2::RcBlock::new(move || {
if let Some(closure) = closure.take() {
closure()
} else {
error!("tried to execute queued closure on main thread twice");
}
});
let mode = unsafe { kCFRunLoopDefaultMode.unwrap() };
unsafe { CFRunLoopPerformBlock(&run_loop, Some(mode), Some(&block)) }
}
pub(crate) fn send_event(mtm: MainThreadMarker, event: impl Event) {
if let Ok(mut app) = APP_STATE.get(mtm).try_borrow_mut() {
let app = app.as_mut().expect("application was not initialized");
app.world_mut().send_event(event);
app.update();
} else {
trace!("re-entrant access of App, scheduling event for later");
queue_closure(mtm, move || {
let mut app = access_app(mtm);
app.world_mut().send_event(event);
app.update();
});
}
}
pub(crate) fn send_window_event(
mtm: MainThreadMarker,
event: impl Into<WindowEvent> + Event + Clone,
) {
if let Ok(mut app) = APP_STATE.get(mtm).try_borrow_mut() {
let app = app.as_mut().expect("application was not initialized");
app.world_mut().send_window_event(event);
app.update();
} else {
trace!("re-entrant access of App, scheduling event for later");
queue_closure(mtm, move || {
let mut app = access_app(mtm);
app.world_mut().send_window_event(event);
app.update();
});
}
}
#[derive(Debug)]
pub(crate) struct Ivars {}
define_class!(
#[unsafe(super(NSObject))]
#[name = "BevyApplicationDelegate"]
#[thread_kind = MainThreadOnly]
#[ivars = Ivars]
#[derive(Debug)]
pub(crate) struct ApplicationDelegate;
unsafe impl NSObjectProtocol for ApplicationDelegate {}
impl ApplicationDelegate {
#[unsafe(method_id(init))]
fn init(this: Allocated<Self>) -> Retained<Self> {
let this = this.set_ivars(Ivars {});
unsafe { msg_send![super(this), init] }
}
}
unsafe impl UIApplicationDelegate for ApplicationDelegate {
#[unsafe(method(application:willFinishLaunchingWithOptions:))]
fn application_willFinishLaunchingWithOptions(
&self,
_application: &UIApplication,
launch_options: Option<&NSDictionary<UIApplicationLaunchOptionsKey, AnyObject>>,
) -> bool {
trace!(
?launch_options,
"application:willFinishLaunchingWithOptions:"
);
let mut app = access_app(self.mtm());
app.update();
true
}
#[unsafe(method(application:didFinishLaunchingWithOptions:))]
fn application_didFinishLaunchingWithOptions(
&self,
_application: &UIApplication,
launch_options: Option<&NSDictionary<UIApplicationLaunchOptionsKey, AnyObject>>,
) -> bool {
trace!(
?launch_options,
"application:didFinishLaunchingWithOptions:"
);
let mut app = access_app(self.mtm());
if cfg!(feature = "no-scene")
|| !available!(ios = 13.0, tvos = 13.0, visionos = 1.0, ..)
{
let world = app.world_mut();
let query = world
.query_filtered::<(Entity, &Window), With<PrimaryWindow>>()
.get_single(&world);
let (entity, uikit_window) = match query {
Ok((entity, window)) => {
trace!("initializing primary window");
let uikit_window = setup_window(None, entity, window, self.mtm());
(entity, uikit_window)
}
Err(QuerySingleError::NoEntities(_)) => {
trace!("creating primary window");
let entity = world.spawn((Window::default(), PrimaryWindow));
let window = entity.get::<Window>().unwrap();
let uikit_window = setup_window(None, entity.id(), window, self.mtm());
(entity.id(), uikit_window)
}
Err(e) => panic!("failed fetching primary window: {e}"),
};
world
.non_send_resource_mut::<UIKitWindows>()
.insert(entity, uikit_window);
world.send_window_event(WindowCreated { window: entity });
app.update();
}
true
}
#[unsafe(method(applicationWillEnterForeground:))]
fn applicationWillEnterForeground(&self, _application: &UIApplication) {
trace!("applicationWillEnterForeground:");
}
#[unsafe(method(applicationDidBecomeActive:))]
fn applicationDidBecomeActive(&self, _application: &UIApplication) {
trace!("applicationDidBecomeActive:");
}
#[unsafe(method(applicationWillResignActive:))]
fn applicationWillResignActive(&self, _application: &UIApplication) {
trace!("applicationWillResignActive:");
}
#[unsafe(method(applicationDidEnterBackground:))]
fn applicationDidEnterBackground(&self, _application: &UIApplication) {
trace!("applicationDidEnterBackground:");
}
#[unsafe(method(applicationWillTerminate:))]
fn applicationWillTerminate(&self, _application: &UIApplication) {
trace!("applicationWillTerminate:");
let app = APP_STATE
.get(self.mtm())
.borrow_mut()
.take()
.expect("application was not initialized");
let _: App = app;
}
#[unsafe(method(applicationDidReceiveMemoryWarning:))]
fn applicationDidReceiveMemoryWarning(&self, _application: &UIApplication) {
trace!("applicationDidReceiveMemoryWarning:");
}
#[unsafe(method(application:openURL:options:))]
fn application_openURL_options(
&self,
_application: &UIApplication,
url: &NSURL,
options: &NSDictionary<UIApplicationOpenURLOptionsKey, AnyObject>,
) -> bool {
trace!(?url, ?options, "application:openURL:options:");
false
}
#[cfg(not(feature = "no-scene"))]
#[unsafe(method_id(application:configurationForConnectingSceneSession:options:))]
fn application_configurationForConnectingSceneSession_options(
&self,
_application: &UIApplication,
connecting_scene_session: &UISceneSession,
options: &UISceneConnectionOptions,
) -> Retained<UISceneConfiguration> {
trace!(
scene = ?unsafe { connecting_scene_session.persistentIdentifier() },
user_info = ?unsafe { connecting_scene_session.userInfo() },
configuration = ?unsafe { connecting_scene_session.configuration() },
?options,
"application:configurationForConnectingSceneSession:options:"
);
let config = unsafe {
UISceneConfiguration::configurationWithName_sessionRole(
Some(ns_string!("Bevy Configuration")),
&connecting_scene_session.role(),
self.mtm(),
)
};
unsafe { config.setDelegateClass(Some(SceneDelegate::class())) };
config
}
#[cfg(not(feature = "no-scene"))]
#[unsafe(method(application:didDiscardSceneSessions:))]
fn application_didDiscardSceneSessions(
&self,
_application: &UIApplication,
scene_sessions: &NSSet<UISceneSession>,
) {
trace!(?scene_sessions, "application:didDiscardSceneSessions:");
}
#[unsafe(method_id(window))]
fn window(&self) -> Option<Retained<UIWindow>> {
None
}
#[unsafe(method(setWindow:))]
fn setWindow(&self, _window: Option<&UIWindow>) {
warn!("setting a story board is not supported in Bevy, remove `UIMainStoryboardFile` key from `Info.plist`");
}
}
);