#include "UnityAppController+ViewHandling.h" #include "UnityAppController+Rendering.h" #include "UI/OrientationSupport.h" #include "UI/UnityView.h" #include "UI/UnityViewControllerBase.h" #include "Unity/DisplayManager.h" // TEMP: ? #include "UI/ActivityIndicator.h" #include "UI/SplashScreen.h" #include "UI/Keyboard.h" #include extern bool _skipPresent; extern bool _unityAppReady; @implementation UnityAppController (ViewHandling) #if UNITY_SUPPORT_ROTATION // special case for when we DO know the app orientation, but dont get it through normal mechanism (UIViewController orientation handling) // how can this happen: // 1. On startup: ios is not sending "change orientation" notifications on startup (but rather we "start" in correct one already) // 2. When using presentation controller it can override orientation constraints, so on dismissing we need to tweak app orientation; // pretty much like startup situation UIViewController would have correct orientation, and app will be out-of-sync - (void)updateAppOrientation:(UIInterfaceOrientation)orientation { _curOrientation = orientation; [_unityView willRotateToOrientation: orientation fromOrientation: (UIInterfaceOrientation)UIInterfaceOrientationUnknown]; [_unityView didRotate]; } #endif - (UnityView*)createUnityView { return [[UnityView alloc] initFromMainScreen]; } - (UIViewController*)createUnityViewControllerDefault { UnityDefaultViewController* ret = [[UnityDefaultViewController alloc] init]; #if PLATFORM_TVOS // This enables game controller use in on-screen keyboard ret.controllerUserInteractionEnabled = YES; #endif return ret; } #if UNITY_SUPPORT_ROTATION - (UIViewController*)createUnityViewControllerForOrientation:(UIInterfaceOrientation)orient { switch (orient) { case UIInterfaceOrientationPortrait: return [[UnityPortraitOnlyViewController alloc] init]; case UIInterfaceOrientationPortraitUpsideDown: return [[UnityPortraitUpsideDownOnlyViewController alloc] init]; case UIInterfaceOrientationLandscapeLeft: return [[UnityLandscapeLeftOnlyViewController alloc] init]; case UIInterfaceOrientationLandscapeRight: return [[UnityLandscapeRightOnlyViewController alloc] init]; default: NSAssert(false, @"bad UIInterfaceOrientation provided"); } return nil; } #endif - (UIViewController*)createRootViewController { UIViewController* ret = nil; if (!UNITY_SUPPORT_ROTATION || UnityShouldAutorotate()) { if (_viewControllerForOrientation[0] == nil) _viewControllerForOrientation[0] = [self createUnityViewControllerDefault]; ret = _viewControllerForOrientation[0]; } #if UNITY_SUPPORT_ROTATION if (ret == nil) { UIInterfaceOrientation orientation = ConvertToIosScreenOrientation((ScreenOrientation)UnityRequestedScreenOrientation()); ret = [self createRootViewControllerForOrientation: orientation]; } #endif return ret; } - (UIViewController*)topMostController { UIViewController *topController = self.window.rootViewController; while (topController.presentedViewController) topController = topController.presentedViewController; return topController; } - (void)willStartWithViewController:(UIViewController*)controller { _unityView.contentScaleFactor = UnityScreenScaleFactor([UIScreen mainScreen]); _unityView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _rootController.view = _rootView = _unityView; } - (void)willTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController { } - (void)didTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController { #if UNITY_SUPPORT_ROTATION // when transitioning between view controllers ios will not send reorient events (because they are bound to controllers, not view) // so we imitate them here so unity view can update its size/orientation [_unityView willRotateToOrientation: UIViewControllerInterfaceOrientation(toController) fromOrientation: ConvertToIosScreenOrientation(_unityView.contentOrientation)]; [_unityView didRotate]; // NB: this is both important and insane at the same time (that we have several places to keep current orentation and we need to sync them) _curOrientation = UIViewControllerInterfaceOrientation(toController); #endif } - (UIView*)createSnapshotView { // Snapshot API appeared on iOS 7, however before iOS 8 tweaking hierarchy like that on going to // background results in all kind of weird things when going back to foreground so we do snapshotting // only on iOS 8 and newer. // Note that on iPads with iOS 9 or later (up to iOS 10.2 at least) there's a bug in the iOS // compositor: any use of -[UIView snapshotViewAfterScreenUpdates] causes black screen being shown // temporarily when 4 finger gesture to swipe to another app in the task switcher is being performed slowly #if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE return [_rootView snapshotViewAfterScreenUpdates: YES]; #else return nil; #endif } - (void)createUI { NSAssert(_unityView != nil, @"_unityView should be inited at this point"); NSAssert(_window != nil, @"_window should be inited at this point"); _rootController = [self createRootViewController]; [self willStartWithViewController: _rootController]; NSAssert(_rootView != nil, @"_rootView should be inited at this point"); NSAssert(_rootController != nil, @"_rootController should be inited at this point"); [_window makeKeyAndVisible]; [UIView setAnimationsEnabled: NO]; // TODO: extract it? ShowSplashScreen(_window); #if UNITY_SUPPORT_ROTATION // to be able to query orientation from view controller we should actually show it. // at this point we can only show splash screen, so update app orientation after we started showing it // NB: _window.rootViewController = splash view controller (not _rootController) [self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_window.rootViewController))]; #endif NSNumber* style = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"Unity_LoadingActivityIndicatorStyle"]; ShowActivityIndicator([SplashScreen Instance], style ? [style intValue] : -1); NSNumber* vcControlled = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIViewControllerBasedStatusBarAppearance"]; if (vcControlled && ![vcControlled boolValue]) printf_console("\nSetting UIViewControllerBasedStatusBarAppearance to NO is no longer supported.\n" "Apple actively discourages that, and all application-wide methods of changing status bar appearance are deprecated\n\n" ); } - (void)showGameUI { HideActivityIndicator(); HideSplashScreen(); // make sure that we start up with correctly created/inited rendering surface // NB: recreateRenderingSurface won't go into rendering because _unityAppReady is false #if UNITY_SUPPORT_ROTATION [self checkOrientationRequest]; #endif [_unityView recreateRenderingSurface]; // UI hierarchy [_window addSubview: _rootView]; _window.rootViewController = _rootController; [_window bringSubviewToFront: _rootView]; #if UNITY_SUPPORT_ROTATION // to be able to query orientation from view controller we should actually show it. // at this point we finally started to show game view controller. Just in case update orientation again [self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_rootController))]; #endif // why we set level ready only now: // surface recreate will try to repaint if this var is set (poking unity to do it) // but this frame now is actually the first one we want to process/draw // so all the recreateSurface before now (triggered by reorientation) should simply change extents _unityAppReady = true; // why we skip present: // this will be the first frame to draw, so Start methods will be called // and we want to properly handle resolution request in Start (which might trigger surface recreate) // NB: we want to draw right after showing window, to avoid black frame creeping in _skipPresent = true; if (!UnityIsPaused()) UnityRepaint(); _skipPresent = false; [self repaint]; [UIView setAnimationsEnabled: YES]; } - (void)transitionToViewController:(UIViewController*)vc { [self willTransitionToViewController: vc fromViewController: _rootController]; // first: remove from view hierarchy. // if we simply hide the window before assigning the new view controller, it will cause black frame flickering // on the other hand, hiding the window is important by itself to better signal the intent to iOS // e.g. unless we hide the window view, safeArea might stop working (due to bug in iOS if we're to speculate) // due to that we do this hide/unhide sequence: we want to to make it hidden, but still unhide it before changing window view controller. _window.hidden = YES; _window.hidden = NO; _rootController.view = nil; _window.rootViewController = nil; // second: assign new root controller (and view hierarchy with that), restore bounds _rootController = _window.rootViewController = vc; _rootController.view = _rootView; _window.bounds = [UIScreen mainScreen].bounds; // required for iOS 8, otherwise view bounds will be incorrect _rootView.bounds = _window.bounds; _rootView.center = _window.center; // third: restore window as key and layout subviews to finalize size changes [_window makeKeyAndVisible]; [_window layoutSubviews]; [self didTransitionToViewController: vc fromViewController: _rootController]; } #if UNITY_SUPPORT_ROTATION - (void)interfaceWillChangeOrientationTo:(UIInterfaceOrientation)toInterfaceOrientation { UIInterfaceOrientation fromInterfaceOrientation = _curOrientation; _curOrientation = toInterfaceOrientation; [_unityView willRotateToOrientation: toInterfaceOrientation fromOrientation: fromInterfaceOrientation]; } - (void)interfaceDidChangeOrientationFrom:(UIInterfaceOrientation)fromInterfaceOrientation { [_unityView didRotate]; } #endif #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) - (void)executeForEveryViewController:(void(^)(UIViewController*))callback { for (unsigned i = 0; i < ARRAY_SIZE(_viewControllerForOrientation); ++i) { UIViewController* vc = _viewControllerForOrientation[i]; if (vc) callback(vc); } } - (void)notifyHideHomeButtonChange { // Note that we need to update all view controllers because UIKit won't necessarily // update the properties of view controllers when orientation is changed. #if PLATFORM_IOS && UNITY_HAS_IOSSDK_11_0 if (@available(iOS 11.0, *)) { [self executeForEveryViewController: ^(UIViewController* vc) { // setNeedsUpdateOfHomeIndicatorAutoHidden is not implemented on iOS 11.0. // The bug has been fixed in iOS 11.0.1. See http://www.openradar.me/35127134 if ([vc respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) [vc setNeedsUpdateOfHomeIndicatorAutoHidden]; }]; } #endif } - (void)notifyDeferSystemGesturesChange { #if PLATFORM_IOS && UNITY_HAS_IOSSDK_11_0 if (@available(iOS 11.0, *)) { [self executeForEveryViewController: ^(UIViewController* vc) { [vc setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; }]; } #endif } @end #if UNITY_SUPPORT_ROTATION @implementation UnityAppController (OrientationSupport) - (UIViewController*)createRootViewControllerForOrientation:(UIInterfaceOrientation)orientation { NSAssert(orientation != 0, @"Bad UIInterfaceOrientation provided"); if (_viewControllerForOrientation[orientation] == nil) _viewControllerForOrientation[orientation] = [self createUnityViewControllerForOrientation: orientation]; return _viewControllerForOrientation[orientation]; } - (void)checkOrientationRequest { if (!UnityHasOrientationRequest() && !UnityShouldChangeAllowedOrientations()) return; // normally we want to call attemptRotationToDeviceOrientation to tell iOS that we changed orientation constraints // but if the current orientation is disabled we need special processing, as iOS will simply ignore us // the only good/robust way is to simply recreate "autorotating" view controller and transition to it if needed // please note that we want to trigger "orientation request" code path if we recreate autorotating view controller bool changeOrient = UnityHasOrientationRequest(); if (UnityShouldChangeAllowedOrientations()) { // so we can say that the only corner case is when we are autorotating and will autorotate // AND changed allowed orientations while keeping current allowed // in that case we simply trigger attemptRotationToDeviceOrientation and we are done // please note that it this can happen when current *device* orientation is disabled (and we want to enable it) NSUInteger rootOrient = 1 << UIViewControllerInterfaceOrientation(self.rootViewController); if (UnityShouldAutorotate() && _rootController == _viewControllerForOrientation[0] && (rootOrient & EnabledAutorotationInterfaceOrientations())) { [UIViewController attemptRotationToDeviceOrientation]; return; } // otherwise we recreate default autorotating view controller // please note that below we will check if root controller still equals _viewControllerForOrientation[0] // in that case (we update _viewControllerForOrientation[0]) the check will fail and trigger transition (as expected) // you may look at this check as "are we autorotating with same constraints" _viewControllerForOrientation[0] = [self createUnityViewControllerDefault]; changeOrient = true; } if (changeOrient) { if (UnityShouldAutorotate()) { if (_viewControllerForOrientation[0] == nil) _viewControllerForOrientation[0] = [self createUnityViewControllerDefault]; if (_rootController != _viewControllerForOrientation[0]) [self transitionToViewController: _viewControllerForOrientation[0]]; [UIViewController attemptRotationToDeviceOrientation]; } else { ScreenOrientation requestedOrient = (ScreenOrientation)UnityRequestedScreenOrientation(); [self orientInterface: ConvertToIosScreenOrientation(requestedOrient)]; } } UnityOrientationRequestWasCommitted(); } - (void)orientInterface:(UIInterfaceOrientation)orient { if (_unityAppReady) UnityFinishRendering(); [KeyboardDelegate StartReorientation]; [CATransaction begin]; { UIInterfaceOrientation oldOrient = _curOrientation; UIInterfaceOrientation newOrient = orient; [self interfaceWillChangeOrientationTo: newOrient]; [self transitionToViewController: [self createRootViewControllerForOrientation: newOrient]]; [self interfaceDidChangeOrientationFrom: oldOrient]; [UIApplication sharedApplication].statusBarOrientation = orient; } [CATransaction commit]; [KeyboardDelegate FinishReorientation]; } - (void)orientUnity:(UIInterfaceOrientation)orient { [self orientInterface: orient]; } @end #endif extern "C" void UnityNotifyHideHomeButtonChange() { [GetAppController() notifyHideHomeButtonChange]; } extern "C" void UnityNotifyDeferSystemGesturesChange() { [GetAppController() notifyDeferSystemGesturesChange]; }