#if PLATFORM_IOS #import "UnityViewControllerBase.h" #import "UnityAppController.h" #include "OrientationSupport.h" #include "Keyboard.h" #include "UnityView.h" #include "PluginBase/UnityViewControllerListener.h" #include "UnityAppController.h" #include "UnityAppController+ViewHandling.h" #include "Unity/ObjCRuntime.h" // when returning from presenting UIViewController we might need to update app orientation to "correct" one, as we wont get rotation notification @interface UnityAppController () - (void)updateAppOrientation:(UIInterfaceOrientation)orientation; @end #ifndef __IPHONE_16_0 @interface UIViewController () - (void)setNeedsUpdateOfSupportedInterfaceOrientations; @end #endif @implementation UnityViewControllerBase (iOS) - (BOOL)shouldAutorotate { return YES; } - (BOOL)prefersStatusBarHidden { static bool _PrefersStatusBarHidden = true; static bool _PrefersStatusBarHiddenInited = false; if (!_PrefersStatusBarHiddenInited) { NSNumber* hidden = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIStatusBarHidden"]; _PrefersStatusBarHidden = hidden ? [hidden boolValue] : YES; _PrefersStatusBarHiddenInited = true; } return _PrefersStatusBarHidden; } - (UIStatusBarStyle)preferredStatusBarStyle { static UIStatusBarStyle _PreferredStatusBarStyle = UIStatusBarStyleDefault; static bool _PreferredStatusBarStyleInited = false; if (!_PreferredStatusBarStyleInited) { NSString* style = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIStatusBarStyle"]; if (style && [style isEqualToString: @"UIStatusBarStyleLightContent"]) _PreferredStatusBarStyle = UIStatusBarStyleLightContent; _PreferredStatusBarStyleInited = true; } return _PreferredStatusBarStyle; } - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { UIRectEdge res = UIRectEdgeNone; if (UnityGetDeferSystemGesturesTopEdge()) res |= UIRectEdgeTop; if (UnityGetDeferSystemGesturesBottomEdge()) res |= UIRectEdgeBottom; if (UnityGetDeferSystemGesturesLeftEdge()) res |= UIRectEdgeLeft; if (UnityGetDeferSystemGesturesRightEdge()) res |= UIRectEdgeRight; return res; } - (BOOL)prefersHomeIndicatorAutoHidden { return UnityGetHideHomeButton(); } @end @implementation UnityDefaultViewController // these will be updated in one place where we "sync" UI side orientation handling to unity side NSUInteger _supportedOrientations; - (id)init { if ((self = [super init])) { NSAssert(UnityShouldAutorotate(), @"UnityDefaultViewController should be used only if unity is set to autorotate"); _supportedOrientations = EnabledAutorotationInterfaceOrientations(); } return self; } - (void)updateSupportedOrientations { _supportedOrientations = EnabledAutorotationInterfaceOrientations(); if (@available(iOS 16.0, *)) [self setNeedsUpdateOfSupportedInterfaceOrientations]; } - (NSUInteger)supportedInterfaceOrientations { return _supportedOrientations; } - (void)viewWillAppear:(BOOL)animated { ScreenOrientation currentOrientation = UIViewControllerOrientation(self); [GetAppController() updateAppOrientation: ConvertToIosScreenOrientation(currentOrientation)]; [super viewWillAppear: animated]; } - (void)viewDidAppear:(BOOL)animated { ScreenOrientation currentOrientation = UIViewControllerOrientation(self); [GetAppController() updateAppOrientation: ConvertToIosScreenOrientation(currentOrientation)]; [super viewDidAppear: animated]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { // CODE ARCHEOLOGY: we were using UIViewControllerOrientation, but on showing view with "Requires full screen" // CODE ARCHEOLOGY: we will get the size/orientation *already* set, and the rotation logic would break const ScreenOrientation curOrient = GetAppController().unityView.contentOrientation; const ScreenOrientation newOrient = OrientationAfterTransform(curOrient, [coordinator targetTransform]); // in case of presentation controller it will take control over orientations // so to avoid crazy corner cases, make default view controller to ignore "wrong" orientations // as they will come only in case of presentation view controller and will be reverted anyway // NB: we still want to pass message to super, we just want to skip unity-specific magic NSUInteger targetMask = 1 << ConvertToIosScreenOrientation(newOrient); if (([self supportedInterfaceOrientations] & targetMask) != 0) { [UIView setAnimationsEnabled: UnityUseAnimatedAutorotation() ? YES : NO]; [KeyboardDelegate StartReorientation]; [GetAppController() interfaceWillChangeOrientationTo: ConvertToIosScreenOrientation(newOrient)]; [coordinator animateAlongsideTransition: nil completion:^(id context) { [self.view setNeedsLayout]; [GetAppController() interfaceDidChangeOrientationFrom: ConvertToIosScreenOrientation(curOrient)]; [KeyboardDelegate FinishReorientation]; [UIView setAnimationsEnabled: YES]; }]; } [super viewWillTransitionToSize: size withTransitionCoordinator: coordinator]; } @end @interface UnityFixedOrientationViewController () { UIInterfaceOrientation _fixedOrientation; } @end @implementation UnityFixedOrientationViewController - (instancetype)initWithOrientation:(UIInterfaceOrientation)interfaceOrientation { self = [super init]; if (self) { _fixedOrientation = interfaceOrientation; } return self; } - (NSUInteger)supportedInterfaceOrientations { return 1 << _fixedOrientation; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return _fixedOrientation; } - (void)viewWillAppear:(BOOL)animated { [GetAppController() updateAppOrientation: _fixedOrientation]; [super viewWillAppear: animated]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { // Adding a call to notify about changed orientation. In iOS16 there was a runtime change where view controller // is not fully set up as soon as we make the view controller change in AppController at -transitionToViewController:. // And instead iOS calls this method, in other cases this method is not called. So we want to call // didTransitionToViewController here as this is the place where we get updated orientation. // // NB: Look for additional explanation at UnityAppController+ViewHandling.mm method -transitionToViewController: before // call to same method. [GetAppController() didTransitionToViewController: self fromViewController: self]; [super viewWillTransitionToSize: size withTransitionCoordinator: coordinator]; } @end @implementation UnityPortraitOnlyViewController - (instancetype)init { self = [super initWithOrientation: UIInterfaceOrientationPortrait]; return self; } @end @implementation UnityPortraitUpsideDownOnlyViewController - (instancetype)init { self = [super initWithOrientation: UIInterfaceOrientationPortraitUpsideDown]; return self; } @end @implementation UnityLandscapeLeftOnlyViewController - (instancetype)init { self = [super initWithOrientation: UIInterfaceOrientationLandscapeLeft]; return self; } @end @implementation UnityLandscapeRightOnlyViewController - (instancetype)init { self = [super initWithOrientation: UIInterfaceOrientationLandscapeRight]; return self; } @end NSUInteger EnabledAutorotationInterfaceOrientations() { NSUInteger ret = 0; if (UnityIsOrientationEnabled(portrait)) ret |= (1 << UIInterfaceOrientationPortrait); if (UnityDeviceSupportsUpsideDown() && UnityIsOrientationEnabled(portraitUpsideDown)) ret |= (1 << UIInterfaceOrientationPortraitUpsideDown); if (UnityIsOrientationEnabled(landscapeLeft)) ret |= (1 << UIInterfaceOrientationLandscapeRight); if (UnityIsOrientationEnabled(landscapeRight)) ret |= (1 << UIInterfaceOrientationLandscapeLeft); // Handling unexpected case where autorotation is on and all the orientations are off by defaulting to current orientation. // Previously we returned 0 and iOS were handling it by keeping orientation as is. From iOS16 behaviour changed and the bug was raised. // Either way iOS requires us to provide non 0 value to supportedInterfaceOrientations. if (ret == 0) { NSLog(@"[Error] All orientations are off for autorotation. Preventing crash by using current orientation."); ret = (1 << [GetAppController() interfaceOrientation]); } return ret; } #endif // PLATFORM_IOS