// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #if PLATFORM_IOS #include "iOSPlatform.h" #include "iOSWindow.h" #include "iOSFile.h" #include "iOSFileSystem.h" #include "iOSApp.h" #include "Engine/Core/Log.h" #include "Engine/Core/Delegate.h" #include "Engine/Core/Utilities.h" #include "Engine/Core/Types/String.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Platform/Apple/AppleUtils.h" #include "Engine/Platform/StringUtils.h" #include "Engine/Platform/MessageBox.h" #include "Engine/Platform/Window.h" #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/BatteryInfo.h" #include "Engine/Threading/Threading.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Input/Input.h" #include "Engine/Input/InputDevice.h" #include "Engine/Engine/Engine.h" #include "Engine/Engine/Globals.h" #include "Engine/Content/Storage/ContentStorageManager.h" #include #include #include #include #include #include #include #include class iOSTouchScreen : public InputDevice { private: CriticalSection _locker; public: explicit iOSTouchScreen() : InputDevice(SpawnParams(Guid::New(), TypeInitializer), TEXT("iOS Touch Screen")) { } void ResetState() override { ScopeLock lock(_locker); InputDevice::ResetState(); } bool Update(EventQueue& queue) override { ScopeLock lock(_locker); return InputDevice::Update(queue); } void OnTouch(EventType type, float x, float y, int32 pointerId) { ScopeLock lock(_locker); Event& e = _queue.AddOne(); e.Type = type; e.Target = nullptr; e.TouchData.Position.X = x; e.TouchData.Position.Y = y; e.TouchData.PointerId = pointerId; } }; int32 Dpi = 96; Guid DeviceId; FlaxView* MainView = nullptr; FlaxViewController* MainViewController = nullptr; iOSWindow* MainWindow = nullptr; iOSTouchScreen* TouchScreen; bool HasFocus = true; bool IsPaused = false; struct MessagePipeline { CriticalSection Locker; Array> List; void Add(const Function& func, bool wait) { Locker.Lock(); List.Add(func); Locker.Unlock(); // TODO: use atomic counters for more optimized waiting while (wait) { Platform::Sleep(1); Locker.Lock(); wait = List.HasItems(); Locker.Unlock(); } } void Run() { Locker.Lock(); for (const auto& func : List) { func(); } List.Clear(); Locker.Unlock(); } }; MessagePipeline UIThreadPipeline; MessagePipeline MainThreadPipeline; #define PLATFORM_IOS_MAX_TOUCHES 8 @interface FlaxView() { UITouch* activeTouches[PLATFORM_IOS_MAX_TOUCHES]; } @end @implementation FlaxView + (Class)layerClass { return [CAMetalLayer class]; } - (void)setFrame:(CGRect)frame { [super setFrame:frame]; if (!MainWindow) return; float scale = [[UIScreen mainScreen] scale]; float width = (float)frame.size.width * scale; float height = (float)frame.size.height * scale; iOSPlatform::RunOnMainThread([width, height]() { MainWindow->CheckForResize(width, height); }); } - (void)Init { self.multipleTouchEnabled = YES; Platform::MemoryClear(activeTouches, sizeof(activeTouches)); } - (void)OnTouchEvent:(NSSet*)touches ofType:(InputDevice::EventType)eventType { float scale = [[UIScreen mainScreen] scale]; for (UITouch* touch in touches) { // Get touch location CGPoint location = [touch locationInView:self]; location.x *= scale; location.y *= scale; // Get touch index (use list of activly tracked touches) int32 touchIndex = 0; for (; touchIndex < PLATFORM_IOS_MAX_TOUCHES; touchIndex++) { if (activeTouches[touchIndex] == touch) break; } if (touchIndex == PLATFORM_IOS_MAX_TOUCHES && eventType == InputDevice::EventType::TouchDown) { // Find free slot for a new touches touchIndex = 0; for (; touchIndex < PLATFORM_IOS_MAX_TOUCHES; touchIndex++) { if (activeTouches[touchIndex] == nil) { activeTouches[touchIndex] = touch; break; } } } if (touchIndex == PLATFORM_IOS_MAX_TOUCHES) continue; // Send event to the input device TouchScreen->OnTouch(eventType, location.x, location.y, touchIndex); // Remove ended touches if (eventType == InputDevice::EventType::TouchUp) activeTouches[touchIndex] = nil; } } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [self OnTouchEvent:touches ofType:InputDevice::EventType::TouchDown]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { [self OnTouchEvent:touches ofType:InputDevice::EventType::TouchMove]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { [self OnTouchEvent:touches ofType:InputDevice::EventType::TouchUp]; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [self OnTouchEvent:touches ofType:InputDevice::EventType::TouchUp]; } @end @implementation FlaxViewController - (BOOL)prefersHomeIndicatorAutoHidden { return YES; } - (BOOL)prefersStatusBarHidden { return YES; } - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation { return UIStatusBarAnimationSlide; } @end @interface FlaxAppDelegate() @property(strong, nonatomic) CADisplayLink* displayLink; @end @implementation FlaxAppDelegate - (void)MainThreadMain:(NSDictionary*)launchOptions { // Run engine on a separate game thread Engine::Main(TEXT("")); } - (void)UIThreadMain { // Invoke callbacks UIThreadPipeline.Run(); } - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { // Create window CGRect frame = [[UIScreen mainScreen] bounds]; self.window = [[UIWindow alloc] initWithFrame:frame]; // Create view self.view = [[FlaxView alloc] initWithFrame:frame]; [self.view resignFirstResponder]; [self.view setNeedsDisplay]; [self.view setHidden:NO]; [self.view setOpaque:YES]; [self.view Init]; self.view.backgroundColor = [UIColor clearColor]; MainView = self.view; // Create view controller self.viewController = [[FlaxViewController alloc] init]; [self.viewController setView:self.view]; [self.viewController setNeedsUpdateOfHomeIndicatorAutoHidden]; [self.viewController setNeedsStatusBarAppearanceUpdate]; MainViewController = self.viewController; // Create navigation controller UINavigationController* navController = [[UINavigationController alloc] initWithRootViewController:self.viewController]; [self.window setRootViewController:navController]; [self.window makeKeyAndVisible]; // Create UI thread update callback self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIThreadMain)]; self.displayLink.preferredFramesPerSecond = 60; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // Run engine on a separate main thread NSThread* mainThread = [[NSThread alloc] initWithTarget:self selector:@selector(MainThreadMain:) object:launchOptions]; #if BUILD_DEBUG const int32 mainThreadStackSize = 4 * 1024 * 1024; // 4 MB #else const int32 mainThreadStackSize = 2 * 1024 * 1024; // 2 MB #endif [mainThread setStackSize:mainThreadStackSize]; [mainThread start]; return YES; } - (void)applicationWillResignActive:(UIApplication*)application { LOG(Info, "[iOS] applicationWillResignActive"); // Defocus app HasFocus = false; if (MainWindow) MainWindow->OnLostFocus(); } - (void)applicationDidEnterBackground:(UIApplication*)application { LOG(Info, "[iOS] applicationDidEnterBackground"); // Pause IsPaused = true; } - (void)applicationWillEnterForeground:(UIApplication*)application { LOG(Info, "[iOS] applicationWillEnterForeground"); // Resume IsPaused = false; } - (void)applicationDidBecomeActive:(UIApplication*)application { LOG(Info, "[iOS] applicationDidBecomeActive"); // Focus app HasFocus = true; if (MainWindow) MainWindow->OnGotFocus(); } - (void)applicationDidReceiveMemoryWarning:(UIApplication*)application { LOG(Warning, "[iOS] applicationDidReceiveMemoryWarning"); LOG(Warning, "os_proc_available_memory: {}", Utilities::BytesToText(os_proc_available_memory())); } @end DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon) { NSString* title = (NSString*)AppleUtils::ToString(caption); NSString* message = (NSString*)AppleUtils::ToString(text); Function func = [&title, &message]() { UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* button = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(id){ }]; [alert addAction:button]; [MainViewController presentViewController:alert animated:YES completion:nil]; }; iOSPlatform::RunOnUIThread(func, true); return DialogResult::OK; } iOSWindow::iOSWindow(const CreateWindowSettings& settings) : WindowBase(settings) { CGRect frame = [[UIScreen mainScreen] bounds]; float scale = [[UIScreen mainScreen] scale]; _clientSize = Float2((float)frame.size.width * scale, (float)frame.size.height * scale); MainWindow = this; } iOSWindow::~iOSWindow() { MainWindow = nullptr; } void iOSWindow::CheckForResize(float width, float height) { const Float2 clientSize(width, height); if (clientSize != _clientSize) { _clientSize = clientSize; OnResize(width, height); } } void* iOSWindow::GetNativePtr() const { return MainView; } void iOSWindow::Show() { if (!_visible) { InitSwapChain(); if (_showAfterFirstPaint) { if (RenderTask) RenderTask->Enabled = true; return; } // Show _focused = true; // Base WindowBase::Show(); } } bool iOSWindow::IsClosed() const { return false; } bool iOSWindow::IsForegroundWindow() const { return IsFocused() && Platform::GetHasFocus(); } void iOSWindow::BringToFront(bool force) { Focus(); } void iOSWindow::SetIsFullscreen(bool isFullscreen) { } // Fallback to file placed side-by-side with application #define IOS_FALLBACK_PATH(path) (Globals::ProjectFolder / StringUtils::GetFileName(path)) iOSFile* iOSFile::Open(const StringView& path, FileMode mode, FileAccess access, FileShare share) { iOSFile* file; if (mode == FileMode::OpenExisting && !AppleFileSystem::FileExists(path)) file = (iOSFile*)UnixFile::Open(IOS_FALLBACK_PATH(path), mode, access, share); else file = (iOSFile*)UnixFile::Open(path, mode, access, share); return file; } bool iOSFileSystem::FileExists(const StringView& path) { return AppleFileSystem::FileExists(path) || AppleFileSystem::FileExists(IOS_FALLBACK_PATH(path)); } uint64 iOSFileSystem::GetFileSize(const StringView& path) { if (AppleFileSystem::FileExists(path)) return AppleFileSystem::GetFileSize(path); return AppleFileSystem::GetFileSize(IOS_FALLBACK_PATH(path)); } bool iOSFileSystem::IsReadOnly(const StringView& path) { if (AppleFileSystem::FileExists(path)) return AppleFileSystem::IsReadOnly(path); return AppleFileSystem::IsReadOnly(IOS_FALLBACK_PATH(path)); } #undef IOS_FALLBACK_PATH void iOSPlatform::RunOnUIThread(const Function& func, bool wait) { UIThreadPipeline.Add(func, wait); } void iOSPlatform::RunOnMainThread(const Function& func, bool wait) { MainThreadPipeline.Add(func, wait); } bool iOSPlatform::Init() { if (ApplePlatform::Init()) return true; // Setup screen scaling ScreenScale = [[UIScreen mainScreen] scale]; CustomDpiScale *= ScreenScale; Dpi = Math::TruncToInt(163 * ScreenScale); // Get device identifier NSString* uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; String uuidStr = AppleUtils::ToString((CFStringRef)uuid); Guid::Parse(uuidStr, DeviceId); // Setup native platform input devices // TODO: add Gamepad for vibrations usability Input::CustomDevices.Add(TouchScreen = New()); // Use more aggressive content buffers freeing to reduce peek memory ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromMilliseconds(30); return false; } void iOSPlatform::LogInfo() { ApplePlatform::LogInfo(); struct utsname systemInfo; uname(&systemInfo); NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; LOG(Info, "{3}, iOS {0}.{1}.{2}", version.majorVersion, version.minorVersion, version.patchVersion, String(systemInfo.machine)); LOG(Info, "os_proc_available_memory: {}", Utilities::BytesToText(os_proc_available_memory())); } void iOSPlatform::Tick() { // Invoke callbacks MainThreadPipeline.Run(); ApplePlatform::Tick(); } BatteryInfo iOSPlatform::GetBatteryInfo() { BatteryInfo result; UIDevice* uiDevice = [UIDevice currentDevice]; if (uiDevice) { uiDevice.batteryMonitoringEnabled = YES; result.BatteryLifePercent = Math::Saturate([uiDevice batteryLevel]); switch ([uiDevice batteryState]) { case UIDeviceBatteryStateUnknown: result.BatteryLifePercent = 1.0f; break; case UIDeviceBatteryStateUnplugged: result.State = BatteryInfo::States::BatteryDischarging; break; case UIDeviceBatteryStateCharging: result.State = BatteryInfo::States::BatteryCharging; break; case UIDeviceBatteryStateFull: result.State = BatteryInfo::States::Connected; break; } } return result; } int32 iOSPlatform::GetDpi() { return Dpi; } NetworkConnectionType iOSPlatform::GetNetworkConnectionType() { struct sockaddr_in emptyAddr; Platform::MemoryClear(&emptyAddr, sizeof(emptyAddr)); emptyAddr.sin_len = sizeof(emptyAddr); emptyAddr.sin_family = AF_INET; SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)&emptyAddr); SCNetworkReachabilityFlags reachabilityFlags; bool connected = SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags); CFRelease(reachability); if (connected) { if (reachabilityFlags == 0) return NetworkConnectionType::AirplaneMode; if ((reachabilityFlags & kSCNetworkReachabilityFlagsReachable) != 0 && (reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) == 0 && (reachabilityFlags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { if ((reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN) == 0) return NetworkConnectionType::WiFi; return NetworkConnectionType::Cell; } } return NetworkConnectionType::None; } ScreenOrientationType iOSPlatform::GetScreenOrientationType() { UIInterfaceOrientation orientation = UIInterfaceOrientationUnknown; Function func = [&orientation]() { orientation = [[[[[UIApplication sharedApplication] delegate] window] windowScene] interfaceOrientation]; }; iOSPlatform::RunOnUIThread(func, true); switch (orientation) { case UIInterfaceOrientationPortrait: return ScreenOrientationType::Portrait; case UIInterfaceOrientationPortraitUpsideDown: return ScreenOrientationType::PortraitUpsideDown; case UIInterfaceOrientationLandscapeLeft: return ScreenOrientationType::LandscapeLeft; case UIInterfaceOrientationLandscapeRight: return ScreenOrientationType::LandscapeRight; case UIInterfaceOrientationUnknown: default: return ScreenOrientationType::Unknown; } } Guid iOSPlatform::GetUniqueDeviceId() { return DeviceId; } String iOSPlatform::GetComputerName() { return TEXT("iPhone"); } bool iOSPlatform::GetHasFocus() { return HasFocus; } bool iOSPlatform::GetIsPaused() { return IsPaused; } Float2 iOSPlatform::GetDesktopSize() { CGRect frame = [[UIScreen mainScreen] bounds]; float scale = [[UIScreen mainScreen] scale]; return Float2((float)frame.size.width * scale, (float)frame.size.height * scale); } String iOSPlatform::GetMainDirectory() { String path = StringUtils::GetDirectoryName(GetExecutableFilePath()); if (path.EndsWith(TEXT("/Contents/iOS"))) { // If running from executable in a package, go up to the Contents path = StringUtils::GetDirectoryName(path); } return path; } Window* iOSPlatform::CreateWindow(const CreateWindowSettings& settings) { return New(settings); } #endif