#![deny(unused_results)] use std::{ cell::{RefCell, RefMut}, collections::HashSet, mem, os::raw::c_void, ptr, time::Instant, }; use objc::runtime::{BOOL, YES}; use crate::{ event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, platform_impl::platform::{ event_loop::{EventHandler, Never}, ffi::{ id, kCFRunLoopCommonModes, CFAbsoluteTimeGetCurrent, CFRelease, CFRunLoopAddTimer, CFRunLoopGetMain, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate, CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, NSInteger, NSOperatingSystemVersion, NSUInteger, }, }, window::WindowId as RootWindowId, }; macro_rules! bug { ($($msg:tt)*) => { panic!("winit iOS bug, file an issue: {}", format!($($msg)*)) }; } macro_rules! bug_assert { ($test:expr, $($msg:tt)*) => { assert!($test, "winit iOS bug, file an issue: {}", format!($($msg)*)) }; } enum UserCallbackTransitionResult<'a> { Success { event_handler: Box, active_control_flow: ControlFlow, processing_redraws: bool, }, ReentrancyPrevented { queued_events: &'a mut Vec>, }, } impl Event { fn is_redraw(&self) -> bool { if let Event::WindowEvent { window_id: _, event: WindowEvent::RedrawRequested, } = self { true } else { false } } } // this is the state machine for the app lifecycle #[derive(Debug)] #[must_use = "dropping `AppStateImpl` without inspecting it is probably a bug"] enum AppStateImpl { NotLaunched { queued_windows: Vec, queued_events: Vec>, queued_gpu_redraws: HashSet, }, Launching { queued_windows: Vec, queued_events: Vec>, queued_event_handler: Box, queued_gpu_redraws: HashSet, }, ProcessingEvents { event_handler: Box, queued_gpu_redraws: HashSet, active_control_flow: ControlFlow, }, // special state to deal with reentrancy and prevent mutable aliasing. InUserCallback { queued_events: Vec>, queued_gpu_redraws: HashSet, }, ProcessingRedraws { event_handler: Box, active_control_flow: ControlFlow, }, Waiting { waiting_event_handler: Box, start: Instant, }, PollFinished { waiting_event_handler: Box, }, Terminated, } struct AppState { // This should never be `None`, except for briefly during a state transition. app_state: Option, control_flow: ControlFlow, waker: EventLoopWaker, } impl Drop for AppState { fn drop(&mut self) { match self.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } | &mut AppStateImpl::Launching { ref mut queued_windows, .. } => { for &mut window in queued_windows { unsafe { let () = msg_send![window, release]; } } } _ => {} } } } impl AppState { // requires main thread unsafe fn get_mut() -> RefMut<'static, AppState> { // basically everything in UIKit requires the main thread, so it's pointless to use the // std::sync APIs. // must be mut because plain `static` requires `Sync` static mut APP_STATE: RefCell> = RefCell::new(None); if cfg!(debug_assertions) { assert_main_thread!( "bug in winit: `AppState::get_mut()` can only be called on the main thread" ); } let mut guard = APP_STATE.borrow_mut(); if guard.is_none() { #[inline(never)] #[cold] unsafe fn init_guard(guard: &mut RefMut<'static, Option>) { let waker = EventLoopWaker::new(CFRunLoopGetMain()); **guard = Some(AppState { app_state: Some(AppStateImpl::NotLaunched { queued_windows: Vec::new(), queued_events: Vec::new(), queued_gpu_redraws: HashSet::new(), }), control_flow: ControlFlow::default(), waker, }); } init_guard(&mut guard) } RefMut::map(guard, |state| state.as_mut().unwrap()) } fn state(&self) -> &AppStateImpl { match &self.app_state { Some(ref state) => state, None => bug!("`AppState` previously failed a state transition"), } } fn state_mut(&mut self) -> &mut AppStateImpl { match &mut self.app_state { Some(ref mut state) => state, None => bug!("`AppState` previously failed a state transition"), } } fn take_state(&mut self) -> AppStateImpl { match self.app_state.take() { Some(state) => state, None => bug!("`AppState` previously failed a state transition"), } } fn set_state(&mut self, new_state: AppStateImpl) { bug_assert!( self.app_state.is_none(), "attempted to set an `AppState` without calling `take_state` first {:?}", self.app_state ); self.app_state = Some(new_state) } fn replace_state(&mut self, new_state: AppStateImpl) -> AppStateImpl { match &mut self.app_state { Some(ref mut state) => mem::replace(state, new_state), None => bug!("`AppState` previously failed a state transition"), } } fn has_launched(&self) -> bool { match self.state() { &AppStateImpl::NotLaunched { .. } | &AppStateImpl::Launching { .. } => false, _ => true, } } fn will_launch_transition(&mut self, queued_event_handler: Box) { let (queued_windows, queued_events, queued_gpu_redraws) = match self.take_state() { AppStateImpl::NotLaunched { queued_windows, queued_events, queued_gpu_redraws, } => (queued_windows, queued_events, queued_gpu_redraws), s => bug!("unexpected state {:?}", s), }; self.set_state(AppStateImpl::Launching { queued_windows, queued_events, queued_event_handler, queued_gpu_redraws, }); } fn did_finish_launching_transition(&mut self) -> (Vec, Vec>) { let (windows, events, event_handler, queued_gpu_redraws) = match self.take_state() { AppStateImpl::Launching { queued_windows, queued_events, queued_event_handler, queued_gpu_redraws, } => ( queued_windows, queued_events, queued_event_handler, queued_gpu_redraws, ), s => bug!("unexpected state {:?}", s), }; self.set_state(AppStateImpl::ProcessingEvents { event_handler, active_control_flow: ControlFlow::Poll, queued_gpu_redraws, }); (windows, events) } fn wakeup_transition(&mut self) -> Option> { // before `AppState::did_finish_launching` is called, pretend there is no running // event loop. if !self.has_launched() { return None; } let (event_handler, event) = match (self.control_flow, self.take_state()) { ( ControlFlow::Poll, AppStateImpl::PollFinished { waiting_event_handler, }, ) => (waiting_event_handler, Event::NewEvents(StartCause::Poll)), ( ControlFlow::Wait, AppStateImpl::Waiting { waiting_event_handler, start, }, ) => ( waiting_event_handler, Event::NewEvents(StartCause::WaitCancelled { start, requested_resume: None, }), ), ( ControlFlow::WaitUntil(requested_resume), AppStateImpl::Waiting { waiting_event_handler, start, }, ) => { let event = if Instant::now() >= requested_resume { Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, }) } else { Event::NewEvents(StartCause::WaitCancelled { start, requested_resume: Some(requested_resume), }) }; (waiting_event_handler, event) } (ControlFlow::Exit, _) => bug!("unexpected `ControlFlow` `Exit`"), s => bug!("`EventHandler` unexpectedly woke up {:?}", s), }; self.set_state(AppStateImpl::ProcessingEvents { event_handler, queued_gpu_redraws: Default::default(), active_control_flow: self.control_flow, }); Some(event) } fn try_user_callback_transition(&mut self) -> UserCallbackTransitionResult<'_> { // If we're not able to process an event due to recursion or `Init` not having been sent out // yet, then queue the events up. match self.state_mut() { &mut AppStateImpl::Launching { ref mut queued_events, .. } | &mut AppStateImpl::NotLaunched { ref mut queued_events, .. } | &mut AppStateImpl::InUserCallback { ref mut queued_events, .. } => { // A lifetime cast: early returns are not currently handled well with NLL, but // polonius handles them well. This transmute is a safe workaround. return unsafe { mem::transmute::< UserCallbackTransitionResult<'_>, UserCallbackTransitionResult<'_>, >(UserCallbackTransitionResult::ReentrancyPrevented { queued_events, }) }; } &mut AppStateImpl::ProcessingEvents { .. } | &mut AppStateImpl::ProcessingRedraws { .. } => {} s @ &mut AppStateImpl::PollFinished { .. } | s @ &mut AppStateImpl::Waiting { .. } | s @ &mut AppStateImpl::Terminated => { bug!("unexpected attempted to process an event {:?}", s) } } let (event_handler, queued_gpu_redraws, active_control_flow, processing_redraws) = match self.take_state() { AppStateImpl::Launching { .. } | AppStateImpl::NotLaunched { .. } | AppStateImpl::InUserCallback { .. } => unreachable!(), AppStateImpl::ProcessingEvents { event_handler, queued_gpu_redraws, active_control_flow, } => ( event_handler, queued_gpu_redraws, active_control_flow, false, ), AppStateImpl::ProcessingRedraws { event_handler, active_control_flow, } => (event_handler, Default::default(), active_control_flow, true), AppStateImpl::PollFinished { .. } | AppStateImpl::Waiting { .. } | AppStateImpl::Terminated => unreachable!(), }; self.set_state(AppStateImpl::InUserCallback { queued_events: Vec::new(), queued_gpu_redraws, }); UserCallbackTransitionResult::Success { event_handler, active_control_flow, processing_redraws, } } fn main_events_cleared_transition(&mut self) -> HashSet { let (event_handler, queued_gpu_redraws, active_control_flow) = match self.take_state() { AppStateImpl::ProcessingEvents { event_handler, queued_gpu_redraws, active_control_flow, } => (event_handler, queued_gpu_redraws, active_control_flow), s => bug!("unexpected state {:?}", s), }; self.set_state(AppStateImpl::ProcessingRedraws { event_handler, active_control_flow, }); queued_gpu_redraws } fn events_cleared_transition(&mut self) { if !self.has_launched() { return; } let (waiting_event_handler, old) = match self.take_state() { AppStateImpl::ProcessingRedraws { event_handler, active_control_flow, } => (event_handler, active_control_flow), s => bug!("unexpected state {:?}", s), }; let new = self.control_flow; match (old, new) { (ControlFlow::Poll, ControlFlow::Poll) => self.set_state(AppStateImpl::PollFinished { waiting_event_handler, }), (ControlFlow::Wait, ControlFlow::Wait) => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { waiting_event_handler, start, }); } (ControlFlow::WaitUntil(old_instant), ControlFlow::WaitUntil(new_instant)) if old_instant == new_instant => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { waiting_event_handler, start, }); } (_, ControlFlow::Wait) => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { waiting_event_handler, start, }); self.waker.stop() } (_, ControlFlow::WaitUntil(new_instant)) => { let start = Instant::now(); self.set_state(AppStateImpl::Waiting { waiting_event_handler, start, }); self.waker.start_at(new_instant) } (_, ControlFlow::Poll) => { self.set_state(AppStateImpl::PollFinished { waiting_event_handler, }); self.waker.start() } (_, ControlFlow::Exit) => { // https://developer.apple.com/library/archive/qa/qa1561/_index.html // it is not possible to quit an iOS app gracefully and programatically warn!("`ControlFlow::Exit` ignored on iOS"); self.control_flow = old } } } fn terminated_transition(&mut self) -> Box { match self.replace_state(AppStateImpl::Terminated) { AppStateImpl::ProcessingEvents { event_handler, .. } => event_handler, s => bug!( "`LoopDestroyed` happened while not processing events {:?}", s ), } } } // requires main thread and window is a UIWindow // retains window pub unsafe fn set_key_window(window: id) { bug_assert!( { let is_window: BOOL = msg_send![window, isKindOfClass: class!(UIWindow)]; is_window == YES }, "set_key_window called with an incorrect type" ); let mut this = AppState::get_mut(); match this.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_windows, .. } => return queued_windows.push(msg_send![window, retain]), &mut AppStateImpl::ProcessingEvents { .. } | &mut AppStateImpl::InUserCallback { .. } | &mut AppStateImpl::ProcessingRedraws { .. } => {} s @ &mut AppStateImpl::Launching { .. } | s @ &mut AppStateImpl::Waiting { .. } | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), &mut AppStateImpl::Terminated => { panic!("Attempt to create a `Window` after the app has terminated") } } drop(this); msg_send![window, makeKeyAndVisible] } // requires main thread and window is a UIWindow // retains window pub unsafe fn queue_gl_or_metal_redraw(window: id) { bug_assert!( { let is_window: BOOL = msg_send![window, isKindOfClass: class!(UIWindow)]; is_window == YES }, "set_key_window called with an incorrect type" ); let mut this = AppState::get_mut(); match this.state_mut() { &mut AppStateImpl::NotLaunched { ref mut queued_gpu_redraws, .. } | &mut AppStateImpl::Launching { ref mut queued_gpu_redraws, .. } | &mut AppStateImpl::ProcessingEvents { ref mut queued_gpu_redraws, .. } | &mut AppStateImpl::InUserCallback { ref mut queued_gpu_redraws, .. } => drop(queued_gpu_redraws.insert(window)), s @ &mut AppStateImpl::ProcessingRedraws { .. } | s @ &mut AppStateImpl::Waiting { .. } | s @ &mut AppStateImpl::PollFinished { .. } => bug!("unexpected state {:?}", s), &mut AppStateImpl::Terminated => { panic!("Attempt to create a `Window` after the app has terminated") } } drop(this); } // requires main thread pub unsafe fn will_launch(queued_event_handler: Box) { AppState::get_mut().will_launch_transition(queued_event_handler) } // requires main thread pub unsafe fn did_finish_launching() { let mut this = AppState::get_mut(); let windows = match this.state_mut() { AppStateImpl::Launching { queued_windows, .. } => mem::replace(queued_windows, Vec::new()), s => bug!("unexpected state {:?}", s), }; // start waking up the event loop now! bug_assert!( this.control_flow == ControlFlow::Poll, "unexpectedly not setup to `Poll` on launch!" ); this.waker.start(); // have to drop RefMut because the window setup code below can trigger new events drop(this); for window in windows { let count: NSUInteger = msg_send![window, retainCount]; // make sure the window is still referenced if count > 1 { // Do a little screen dance here to account for windows being created before // `UIApplicationMain` is called. This fixes visual issues such as being // offcenter and sized incorrectly. Additionally, to fix orientation issues, we // gotta reset the `rootViewController`. // // relevant iOS log: // ``` // [ApplicationLifecycle] Windows were created before application initialzation // completed. This may result in incorrect visual appearance. // ``` let screen: id = msg_send![window, screen]; let _: id = msg_send![screen, retain]; let () = msg_send![window, setScreen:0 as id]; let () = msg_send![window, setScreen: screen]; let () = msg_send![screen, release]; let controller: id = msg_send![window, rootViewController]; let () = msg_send![window, setRootViewController:ptr::null::<()>()]; let () = msg_send![window, setRootViewController: controller]; let () = msg_send![window, makeKeyAndVisible]; } let () = msg_send![window, release]; } let (windows, events) = AppState::get_mut().did_finish_launching_transition(); let events = std::iter::once(Event::NewEvents(StartCause::Init)).chain(events); handle_nonuser_events(events); // the above window dance hack, could possibly trigger new windows to be created. // we can just set those windows up normally, as they were created after didFinishLaunching for window in windows { let count: NSUInteger = msg_send![window, retainCount]; // make sure the window is still referenced if count > 1 { let () = msg_send![window, makeKeyAndVisible]; } let () = msg_send![window, release]; } } // requires main thread // AppState::did_finish_launching handles the special transition `Init` pub unsafe fn handle_wakeup_transition() { let mut this = AppState::get_mut(); let wakeup_event = match this.wakeup_transition() { None => return, Some(wakeup_event) => wakeup_event, }; drop(this); handle_nonuser_event(wakeup_event) } // requires main thread pub unsafe fn handle_nonuser_event(event: Event) { handle_nonuser_events(std::iter::once(event)) } // requires main thread pub unsafe fn handle_nonuser_events>>(events: I) { let mut this = AppState::get_mut(); let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { UserCallbackTransitionResult::ReentrancyPrevented { queued_events } => { queued_events.extend(events); return; } UserCallbackTransitionResult::Success { event_handler, active_control_flow, processing_redraws, } => (event_handler, active_control_flow, processing_redraws), }; let mut control_flow = this.control_flow; drop(this); for event in events { if !processing_redraws && event.is_redraw() { log::info!("processing `RedrawRequested` during the main event loop"); } else if processing_redraws && !event.is_redraw() { log::warn!( "processing non `RedrawRequested` event after the main event loop: {:#?}", event ); } event_handler.handle_nonuser_event(event, &mut control_flow) } loop { let mut this = AppState::get_mut(); let queued_events = match this.state_mut() { &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _, } => mem::replace(queued_events, Vec::new()), s => bug!("unexpected state {:?}", s), }; if queued_events.is_empty() { let queued_gpu_redraws = match this.take_state() { AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws, } => queued_gpu_redraws, _ => unreachable!(), }; this.app_state = Some(if processing_redraws { bug_assert!( queued_gpu_redraws.is_empty(), "redraw queued while processing redraws" ); AppStateImpl::ProcessingRedraws { event_handler, active_control_flow, } } else { AppStateImpl::ProcessingEvents { event_handler, queued_gpu_redraws, active_control_flow, } }); this.control_flow = control_flow; break; } drop(this); for event in queued_events { if !processing_redraws && event.is_redraw() { log::info!("processing `RedrawRequested` during the main event loop"); } else if processing_redraws && !event.is_redraw() { log::warn!( "processing non-`RedrawRequested` event after the main event loop: {:#?}", event ); } event_handler.handle_nonuser_event(event, &mut control_flow) } } } // requires main thread unsafe fn handle_user_events() { let mut this = AppState::get_mut(); let mut control_flow = this.control_flow; let (mut event_handler, active_control_flow, processing_redraws) = match this.try_user_callback_transition() { UserCallbackTransitionResult::ReentrancyPrevented { .. } => { bug!("unexpected attempted to process an event") } UserCallbackTransitionResult::Success { event_handler, active_control_flow, processing_redraws, } => (event_handler, active_control_flow, processing_redraws), }; if processing_redraws { bug!("user events attempted to be sent out while `ProcessingRedraws`"); } drop(this); event_handler.handle_user_events(&mut control_flow); loop { let mut this = AppState::get_mut(); let queued_events = match this.state_mut() { &mut AppStateImpl::InUserCallback { ref mut queued_events, queued_gpu_redraws: _, } => mem::replace(queued_events, Vec::new()), s => bug!("unexpected state {:?}", s), }; if queued_events.is_empty() { let queued_gpu_redraws = match this.take_state() { AppStateImpl::InUserCallback { queued_events: _, queued_gpu_redraws, } => queued_gpu_redraws, _ => unreachable!(), }; this.app_state = Some(AppStateImpl::ProcessingEvents { event_handler, queued_gpu_redraws, active_control_flow, }); this.control_flow = control_flow; break; } drop(this); for event in queued_events { event_handler.handle_nonuser_event(event, &mut control_flow) } event_handler.handle_user_events(&mut control_flow); } } // requires main thread pub unsafe fn handle_main_events_cleared() { let mut this = AppState::get_mut(); if !this.has_launched() { return; } match this.state_mut() { &mut AppStateImpl::ProcessingEvents { .. } => {} _ => bug!("`ProcessingRedraws` happened unexpectedly"), }; drop(this); // User events are always sent out at the end of the "MainEventLoop" handle_user_events(); handle_nonuser_event(Event::EventsCleared); let mut this = AppState::get_mut(); let redraw_events = this .main_events_cleared_transition() .into_iter() .map(|window| Event::WindowEvent { window_id: RootWindowId(window.into()), event: WindowEvent::RedrawRequested, }); drop(this); handle_nonuser_events(redraw_events); } // requires main thread pub unsafe fn handle_events_cleared() { AppState::get_mut().events_cleared_transition(); } // requires main thread pub unsafe fn terminated() { let mut this = AppState::get_mut(); let mut event_handler = this.terminated_transition(); let mut control_flow = this.control_flow; drop(this); event_handler.handle_nonuser_event(Event::LoopDestroyed, &mut control_flow) } struct EventLoopWaker { timer: CFRunLoopTimerRef, } impl Drop for EventLoopWaker { fn drop(&mut self) { unsafe { CFRunLoopTimerInvalidate(self.timer); CFRelease(self.timer as _); } } } impl EventLoopWaker { fn new(rl: CFRunLoopRef) -> EventLoopWaker { extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} unsafe { // Create a timer with a 0.1µs interval (1ns does not work) to mimic polling. // It is initially setup with a first fire time really far into the // future, but that gets changed to fire immediately in did_finish_launching let timer = CFRunLoopTimerCreate( ptr::null_mut(), std::f64::MAX, 0.000_000_1, 0, 0, wakeup_main_loop, ptr::null_mut(), ); CFRunLoopAddTimer(rl, timer, kCFRunLoopCommonModes); EventLoopWaker { timer } } } fn stop(&mut self) { unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } } fn start(&mut self) { unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } } fn start_at(&mut self, instant: Instant) { let now = Instant::now(); if now >= instant { self.start(); } else { unsafe { let current = CFAbsoluteTimeGetCurrent(); let duration = instant - now; let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + duration.as_secs() as f64; CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) } } } } macro_rules! os_capabilities { ( $( $(#[$attr:meta])* $error_name:ident: $objc_call:literal, $name:ident: $major:literal-$minor:literal ),* $(,)* ) => { #[derive(Clone, Debug)] pub struct OSCapabilities { $( pub $name: bool, )* os_version: NSOperatingSystemVersion, } impl From for OSCapabilities { fn from(os_version: NSOperatingSystemVersion) -> OSCapabilities { $(let $name = os_version.meets_requirements($major, $minor);)* OSCapabilities { $($name,)* os_version, } } } impl OSCapabilities {$( $(#[$attr])* pub fn $error_name(&self, extra_msg: &str) { log::warn!( concat!("`", $objc_call, "` requires iOS {}.{}+. This device is running iOS {}.{}.{}. {}"), $major, $minor, self.os_version.major, self.os_version.minor, self.os_version.patch, extra_msg ) } )*} }; } os_capabilities! { /// https://developer.apple.com/documentation/uikit/uiview/2891103-safeareainsets?language=objc #[allow(unused)] // error message unused safe_area_err_msg: "-[UIView safeAreaInsets]", safe_area: 11-0, /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887509-setneedsupdateofhomeindicatoraut?language=objc home_indicator_hidden_err_msg: "-[UIViewController setNeedsUpdateOfHomeIndicatorAutoHidden]", home_indicator_hidden: 11-0, /// https://developer.apple.com/documentation/uikit/uiviewcontroller/2887507-setneedsupdateofscreenedgesdefer?language=objc defer_system_gestures_err_msg: "-[UIViewController setNeedsUpdateOfScreenEdgesDeferringSystem]", defer_system_gestures: 11-0, /// https://developer.apple.com/documentation/uikit/uiscreen/2806814-maximumframespersecond?language=objc maximum_frames_per_second_err_msg: "-[UIScreen maximumFramesPerSecond]", maximum_frames_per_second: 10-3, /// https://developer.apple.com/documentation/uikit/uitouch/1618110-force?language=objc #[allow(unused)] // error message unused force_touch_err_msg: "-[UITouch force]", force_touch: 9-0, } impl NSOperatingSystemVersion { fn meets_requirements(&self, required_major: NSInteger, required_minor: NSInteger) -> bool { (self.major, self.minor) >= (required_major, required_minor) } } pub fn os_capabilities() -> OSCapabilities { lazy_static! { static ref OS_CAPABILITIES: OSCapabilities = { let version: NSOperatingSystemVersion = unsafe { let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; let atleast_ios_8: BOOL = msg_send![ process_info, respondsToSelector: sel!(operatingSystemVersion) ]; // winit requires atleast iOS 8 because no one has put the time into supporting earlier os versions. // Older iOS versions are increasingly difficult to test. For example, Xcode 11 does not support // debugging on devices with an iOS version of less than 8. Another example, in order to use an iOS // simulator older than iOS 8, you must download an older version of Xcode (<9), and at least Xcode 7 // has been tested to not even run on macOS 10.15 - Xcode 8 might? // // The minimum required iOS version is likely to grow in the future. assert!( atleast_ios_8 == YES, "`winit` requires iOS version 8 or greater" ); msg_send![process_info, operatingSystemVersion] }; version.into() }; } OS_CAPABILITIES.clone() }