diff --git a/winit-uikit/Cargo.toml b/winit-uikit/Cargo.toml index 9fdc74bbc7..d01eb5d824 100644 --- a/winit-uikit/Cargo.toml +++ b/winit-uikit/Cargo.toml @@ -60,6 +60,7 @@ objc2-ui-kit = { workspace = true, features = [ "UIPinchGestureRecognizer", "UIResponder", "UIRotationGestureRecognizer", + "UIScene", "UIScreen", "UIScreenMode", "UITapGestureRecognizer", @@ -68,6 +69,7 @@ objc2-ui-kit = { workspace = true, features = [ "UIView", "UIViewController", "UIWindow", + "UIWindowScene", ] } winit-common = { workspace = true, features = ["core-foundation", "event-handler", "foundation"] } diff --git a/winit-uikit/src/app_state.rs b/winit-uikit/src/app_state.rs index 7876e34fe1..da6536fffa 100644 --- a/winit-uikit/src/app_state.rs +++ b/winit-uikit/src/app_state.rs @@ -9,8 +9,8 @@ use std::{fmt, ptr}; use dispatch2::MainThreadBound; use dpi::PhysicalSize; -use objc2::MainThreadMarker; use objc2::rc::Retained; +use objc2::{MainThreadMarker, available}; use objc2_core_foundation::{ CFAbsoluteTimeGetCurrent, CFRetained, CFRunLoop, CFRunLoopTimer, CGRect, CGSize, kCFRunLoopCommonModes, @@ -83,6 +83,7 @@ pub(crate) struct AppState { event_loop_proxy: Arc, queued_events: Cell>, queued_gpu_redraws: Cell>>, + awaiting_scene: Cell, } impl fmt::Debug for AppState { @@ -116,6 +117,7 @@ impl AppState { event_loop_proxy, queued_events: Cell::new(Vec::new()), queued_gpu_redraws: Cell::new(HashSet::new()), + awaiting_scene: Cell::new(false), }) .is_ok() } @@ -268,10 +270,25 @@ pub fn did_finish_launching(mtm: MainThreadMarker) { this.did_finish_launching_transition(); get_handler(mtm).handle(|app| app.new_events(&ActiveEventLoop { mtm }, StartCause::Init)); - get_handler(mtm).handle(|app| app.can_create_surfaces(&ActiveEventLoop { mtm })); + // Apps that adopt the scene life cycle get their first scene after launching, and windows + // need one to be shown. + if available!(ios = 13.0, tvos = 13.0, visionos = 1.0) + && UIApplication::sharedApplication(mtm).connectedScenes().is_empty() + { + this.awaiting_scene.set(true); + } else { + get_handler(mtm).handle(|app| app.can_create_surfaces(&ActiveEventLoop { mtm })); + } handle_nonuser_events(mtm, []); } +pub(crate) fn scene_will_connect(mtm: MainThreadMarker) { + if AppState::get(mtm).awaiting_scene.replace(false) { + get_handler(mtm).handle(|app| app.can_create_surfaces(&ActiveEventLoop { mtm })); + handle_nonuser_events(mtm, []); + } +} + // AppState::did_finish_launching handles the special transition `Init` pub fn handle_wakeup_transition(mtm: MainThreadMarker) { let this = AppState::get(mtm); diff --git a/winit-uikit/src/event_loop.rs b/winit-uikit/src/event_loop.rs index 3a5a967168..873229829b 100644 --- a/winit-uikit/src/event_loop.rs +++ b/winit-uikit/src/event_loop.rs @@ -1,10 +1,12 @@ +use std::ffi::{CStr, c_char, c_void}; +use std::ptr; use std::sync::Arc; use objc2::rc::Retained; use objc2::runtime::ProtocolObject; -use objc2::{ClassType, MainThreadMarker, msg_send}; +use objc2::{ClassType, MainThreadMarker, available, msg_send}; use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopDefaultMode}; -use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; +use objc2_foundation::{NSNotificationCenter, NSNotificationName, NSObjectProtocol}; use objc2_ui_kit::{ UIApplication, UIApplicationDidBecomeActiveNotification, UIApplicationDidEnterBackgroundNotification, UIApplicationDidFinishLaunchingNotification, @@ -134,6 +136,7 @@ pub struct EventLoop { _did_enter_background_observer: Retained>, _will_terminate_observer: Retained>, _did_receive_memory_warning_observer: Retained>, + _scene_will_connect_observer: Option>>, _tracing_observers: Option<(MainRunLoopObserver, MainRunLoopObserver)>, _wakeup_observer: MainRunLoopObserver, @@ -240,6 +243,19 @@ impl EventLoop { }, ); + let _scene_will_connect_observer = available!(ios = 13.0, tvos = 13.0, visionos = 1.0) + .then(|| { + create_observer( + ¢er, + // `scene:willConnectToSession:options:` + uikit_notification_name(c"UISceneWillConnectNotification"), + move |_| { + let _entered = debug_span!("UISceneWillConnectNotification").entered(); + app_state::scene_will_connect(mtm) + }, + ) + }); + let main_loop = MainRunLoop::get(mtm); let mode = unsafe { kCFRunLoopDefaultMode }.unwrap(); @@ -302,6 +318,7 @@ impl EventLoop { _did_enter_background_observer, _will_terminate_observer, _did_receive_memory_warning_observer, + _scene_will_connect_observer, _tracing_observers, _wakeup_observer, _main_events_cleared_observer, @@ -334,6 +351,18 @@ impl EventLoop { } } +/// Looks up a notification name at runtime. Linking to one that is newer than the app's oldest +/// supported iOS version would stop the app from launching there. +fn uikit_notification_name(symbol: &CStr) -> &'static NSNotificationName { + unsafe extern "C" { + fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; + } + const RTLD_DEFAULT: *mut c_void = ptr::without_provenance_mut(-2isize as usize); + let name: *const &'static NSNotificationName = + unsafe { dlsym(RTLD_DEFAULT, symbol.as_ptr()) }.cast(); + unsafe { name.as_ref() }.copied().unwrap_or_else(|| panic!("UIKit has no {symbol:?}")) +} + impl EventLoopProvider for EventLoop { fn run_app(self, app: A) -> Result<(), EventLoopError> { self.run_app_never_return(app) diff --git a/winit-uikit/src/lib.rs b/winit-uikit/src/lib.rs index d0a33c9211..26c8ed907d 100644 --- a/winit-uikit/src/lib.rs +++ b/winit-uikit/src/lib.rs @@ -89,6 +89,33 @@ //! //! Note that an app may not receive the `Drop` event if suspended; it might be SIGKILL'ed. //! +//! ## Scenes +//! +//! Apps built with the iOS 27 SDK must adopt [the UIScene lifecycle][scenes] by declaring a scene +//! configuration in `Info.plist`. No scene delegate is needed: +//! +//! ```xml +//! UIApplicationSceneManifest +//! +//! UIApplicationSupportsMultipleScenes +//! +//! UISceneConfigurations +//! +//! UIWindowSceneSessionRoleApplication +//! +//! +//! UISceneConfigurationName +//! Default Configuration +//! +//! +//! +//! +//! ``` +//! +//! Winit shows its windows in the first scene that connects; multiple scenes are not supported. +//! +//! [scenes]: https://developer.apple.com/documentation/uikit/specifying-the-scenes-your-app-supports +//! //! ## Custom `UIApplicationDelegate` //! //! Winit usually handles everything related to the lifecycle events of the application. Sometimes, diff --git a/winit-uikit/src/window.rs b/winit-uikit/src/window.rs index c7c50b2f38..b13ebe32df 100644 --- a/winit-uikit/src/window.rs +++ b/winit-uikit/src/window.rs @@ -14,7 +14,7 @@ use objc2_core_foundation::{CGFloat, CGPoint, CGRect, CGSize}; use objc2_foundation::{NSObject, NSObjectProtocol}; use objc2_ui_kit::{ UIApplication, UICoordinateSpace, UIEdgeInsets, UIResponder, UIScreen, - UIScreenOverscanCompensation, UIViewController, UIWindow, + UIScreenOverscanCompensation, UIViewController, UIWindow, UIWindowScene, }; use tracing::{debug, debug_span, warn}; use winit_core::cursor::Cursor; @@ -84,6 +84,15 @@ impl WinitUIWindow { this.setRootViewController(Some(view_controller)); + // Windows in apps that adopt the scene life cycle are only shown once attached to a scene. + if available!(ios = 13.0, tvos = 13.0, visionos = 1.0) && this.windowScene().is_none() { + let scene = UIApplication::sharedApplication(mtm) + .connectedScenes() + .into_iter() + .find_map(|scene| scene.downcast::().ok()); + this.setWindowScene(scene.as_deref()); + } + match window_attributes.fullscreen.clone() { Some(Fullscreen::Exclusive(monitor, ref video_mode)) => { let monitor = monitor.cast_ref::().unwrap(); diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a0e7d3a474..6425290a30 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -40,6 +40,10 @@ changelog entry. ## Unreleased +### Added + +- On iOS, support the UIScene lifecycle (required by iOS 27) + ### Fixed - On Windows, fix a crash occurring when trying to create a DXGI surface on a window that is both fullscreen and always on top.