590 lines
17 KiB
C++
590 lines
17 KiB
C++
// Copyright (c) 2012-2023 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/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 <UIKit/UIKit.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <QuartzCore/CAMetalLayer.h>
|
|
#include <SystemConfiguration/SystemConfiguration.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
|
|
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;
|
|
|
|
struct MessagePipeline
|
|
{
|
|
CriticalSection Locker;
|
|
Array<Function<void()>> List;
|
|
|
|
void Add(const Function<void()>& 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
|
|
{
|
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
|
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
|
}
|
|
|
|
- (void)applicationDidEnterBackground:(UIApplication *)application
|
|
{
|
|
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
|
}
|
|
|
|
- (void)applicationWillEnterForeground:(UIApplication *)application
|
|
{
|
|
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
|
}
|
|
|
|
- (void)applicationDidBecomeActive:(UIApplication *)application
|
|
{
|
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
|
}
|
|
|
|
@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);
|
|
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];
|
|
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 Platform::GetHasFocus() && IsFocused();
|
|
}
|
|
|
|
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<void()>& func, bool wait)
|
|
{
|
|
UIThreadPipeline.Add(func, wait);
|
|
}
|
|
|
|
void iOSPlatform::RunOnMainThread(const Function<void()>& 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<iOSTouchScreen>());
|
|
|
|
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));
|
|
}
|
|
|
|
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<void()> 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");
|
|
}
|
|
|
|
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<iOSWindow>(settings);
|
|
}
|
|
|
|
#endif
|