SplashScreen.mm 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. #include "SplashScreen.h"
  2. #include "UnityViewControllerBase.h"
  3. #include "OrientationSupport.h"
  4. #include "Unity/ObjCRuntime.h"
  5. #include "UI/UnityView.h"
  6. #include <cstring>
  7. extern "C" const char* UnityGetLaunchScreenXib();
  8. #include <utility>
  9. static SplashScreen* _splash = nil;
  10. static SplashScreenController* _controller = nil;
  11. static bool _isOrientable = false; // true for iPads and iPhone 6+
  12. static bool _usesLaunchscreen = false;
  13. static ScreenOrientation _nonOrientableDefaultOrientation = portrait;
  14. // we will create and show splash before unity is inited, so we can use only plist settings
  15. static bool _canRotateToPortrait = false;
  16. static bool _canRotateToPortraitUpsideDown = false;
  17. static bool _canRotateToLandscapeLeft = false;
  18. static bool _canRotateToLandscapeRight = false;
  19. #if !PLATFORM_TVOS
  20. typedef id (*WillRotateToInterfaceOrientationSendFunc)(struct objc_super*, SEL, UIInterfaceOrientation, NSTimeInterval);
  21. typedef id (*DidRotateFromInterfaceOrientationSendFunc)(struct objc_super*, SEL, UIInterfaceOrientation);
  22. #endif
  23. typedef id (*ViewWillTransitionToSizeSendFunc)(struct objc_super*, SEL, CGSize, id<UIViewControllerTransitionCoordinator>);
  24. static const char* GetScaleSuffix(float scale, float maxScale)
  25. {
  26. if (scale > maxScale)
  27. scale = maxScale;
  28. if (scale <= 1.0)
  29. return "";
  30. if (scale <= 2.0)
  31. return "@2x";
  32. return "@3x";
  33. }
  34. static const char* GetOrientationSuffix(ScreenOrientation orient)
  35. {
  36. bool orientPortrait = (orient == portrait || orient == portraitUpsideDown);
  37. bool orientLandscape = (orient == landscapeLeft || orient == landscapeRight);
  38. bool rotateToPortrait = _canRotateToPortrait || _canRotateToPortraitUpsideDown;
  39. bool rotateToLandscape = _canRotateToLandscapeLeft || _canRotateToLandscapeRight;
  40. if (orientPortrait && rotateToPortrait)
  41. return "-Portrait";
  42. else if (orientLandscape && rotateToLandscape)
  43. return "-Landscape";
  44. else if (rotateToPortrait)
  45. return "-Portrait";
  46. else
  47. return "-Landscape";
  48. }
  49. // Returns a launch image name for launch images stored on file system or asset catalog
  50. static NSArray<NSString*>* GetLaunchImageNames(UIUserInterfaceIdiom idiom, const CGSize& screenSize,
  51. ScreenOrientation orient)
  52. {
  53. NSMutableArray<NSString*>* ret = [[NSMutableArray<NSString *> alloc] init];
  54. if (idiom == UIUserInterfaceIdiomPad)
  55. {
  56. // iPads
  57. const char* iOSSuffix = _ios70orNewer ? "-700" : "";
  58. const char* orientSuffix = GetOrientationSuffix(orient);
  59. const char* scaleSuffix = GetScaleSuffix([UIScreen mainScreen].scale, 2.0);
  60. [ret addObject: [NSString stringWithFormat: @"LaunchImage%s%s%s~ipad",
  61. iOSSuffix, orientSuffix, scaleSuffix]];
  62. }
  63. else
  64. {
  65. // iPhones
  66. float scale = [UIScreen mainScreen].scale;
  67. // Note that on pre-iOS 11 using modifiers such as LaunchImage~568h works. Since
  68. // iOS launch image support is quite hard to get right and has _many_ gotchas, we
  69. // just use the old code path on these devices.
  70. if (screenSize.height == 568 || screenSize.width == 568) // iPhone 5
  71. {
  72. const char* iOS7Suffix = _ios70orNewer ? "-700" : "";
  73. [ret addObject: [NSString stringWithFormat: @"LaunchImage%s-568h@2x", iOS7Suffix]];
  74. [ret addObject: @"LaunchImage~568h"];
  75. }
  76. else if (screenSize.height == 667 || screenSize.width == 667) // iPhone 6
  77. {
  78. // note that scale may be 3.0 if display zoom is enabled
  79. if (scale < 2.0) // not expected, but handle just in case. Image name is valid
  80. [ret addObject: @"LaunchImage-800-667h"];
  81. [ret addObject: @"LaunchImage-800-667h@2x"];
  82. [ret addObject: @"LaunchImage~667h"];
  83. }
  84. else if (screenSize.height == 736 || screenSize.width == 736) // iPhone 6+
  85. {
  86. const char* orientSuffix = GetOrientationSuffix(orient);
  87. if (scale < 3.0) // not expected, but handle just in case. Image name is valid
  88. [ret addObject: [NSString stringWithFormat: @"LaunchImage-800%s-736h", orientSuffix]];
  89. [ret addObject: [NSString stringWithFormat: @"LaunchImage-800%s-736h@3x", orientSuffix]];
  90. [ret addObject: @"LaunchImage~736h"];
  91. }
  92. else if (screenSize.height == 812 || screenSize.width == 812) // iPhone X
  93. {
  94. const char* orientSuffix = GetOrientationSuffix(orient);
  95. if (scale < 3.0) // not expected, but handle just in case. Image name is valid
  96. [ret addObject: [NSString stringWithFormat: @"LaunchImage-1100%s-2436h", orientSuffix]];
  97. [ret addObject: [NSString stringWithFormat: @"LaunchImage-1100%s-2436h@3x", orientSuffix]];
  98. }
  99. if (scale > 1.0)
  100. [ret addObject: @"LaunchImage@2x"];
  101. }
  102. [ret addObject: @"LaunchImage"];
  103. return ret;
  104. }
  105. @implementation SplashScreen
  106. {
  107. UIImageView* m_ImageView;
  108. UIView* m_XibView;
  109. }
  110. - (id)initWithFrame:(CGRect)frame
  111. {
  112. self = [super initWithFrame: frame];
  113. return self;
  114. }
  115. /* The following launch images are produced by Xcode6:
  116. LaunchImage.png
  117. LaunchImage@2x.png
  118. LaunchImage-568h@2x.png
  119. LaunchImage-700@2x.png
  120. LaunchImage-700-568h@2x.png
  121. LaunchImage-700-Landscape@2x~ipad.png
  122. LaunchImage-700-Landscape~ipad.png
  123. LaunchImage-700-Portrait@2x~ipad.png
  124. LaunchImage-700-Portrait~ipad.png
  125. LaunchImage-800-667h@2x.png
  126. LaunchImage-800-Landscape-736h@3x.png
  127. LaunchImage-800-Portrait-736h@3x.png
  128. LaunchImage-1100-Landscape-2436h@3x.png
  129. LaunchImage-1100-Portrait-2436h@3x.png
  130. LaunchImage-Landscape@2x~ipad.png
  131. LaunchImage-Landscape~ipad.png
  132. LaunchImage-Portrait@2x~ipad.png
  133. LaunchImage-Portrait~ipad.png
  134. */
  135. - (void)updateOrientation:(ScreenOrientation)orient
  136. {
  137. CGFloat scale = UnityScreenScaleFactor([UIScreen mainScreen]);
  138. UnityReportResizeView(self.bounds.size.width * scale, self.bounds.size.height * scale, orient);
  139. // Storyboards should have a view controller to automatically configure orientation
  140. NSString* launchScreenStoryboard = [[[[NSBundle mainBundle] infoDictionary] objectForKey: @"UILaunchStoryboardName"] stringByDeletingPathExtension];
  141. const bool hasStoryboard = [[NSBundle mainBundle] pathForResource: launchScreenStoryboard ofType: @"storyboardc"] != nil;
  142. if (hasStoryboard)
  143. return;
  144. UIUserInterfaceIdiom idiom = [[UIDevice currentDevice] userInterfaceIdiom];
  145. NSString* xibName = nil;
  146. if (idiom == UIUserInterfaceIdiomPhone)
  147. xibName = @"LaunchScreen-iPhone";
  148. else if (idiom == UIUserInterfaceIdiomPad)
  149. xibName = @"LaunchScreen-iPad";
  150. const bool hasLaunchScreen = [[NSBundle mainBundle] pathForResource: xibName ofType: @"nib"] != nil;
  151. if (_usesLaunchscreen && hasLaunchScreen)
  152. {
  153. // Launch screen uses the same aspect-filled image for all iPhone and/or
  154. // all iPads, as configured in Unity. We need a special case if there's
  155. // a launch screen and iOS is configured to use it.
  156. if (self->m_XibView == nil)
  157. {
  158. self->m_XibView = [[[NSBundle mainBundle] loadNibNamed: xibName owner: nil options: nil] objectAtIndex: 0];
  159. [self addSubview: self->m_XibView];
  160. }
  161. return;
  162. }
  163. UIImage* image = nil;
  164. CGSize screenSize = [[UIScreen mainScreen] bounds].size;
  165. // For launch images we implement fallback order with multiple images. First we try images via
  166. // [UIImage imageNamed] method and if this fails, we try to load from filesystem directly.
  167. // Note that file system resource names and image names accepted by UIImage are the same.
  168. // Multiple fallbacks are implemented because different iOS versions behave differently and have
  169. // many gotchas that are hard to get right. So we use the images that are present on app bundles
  170. // made with latest version of Xcode as the first priority and then fall back to any image that we
  171. // have used at some time in the past.
  172. NSArray<NSString*>* imageNames = GetLaunchImageNames(idiom, screenSize, orient);
  173. for (NSString* imageName in imageNames)
  174. {
  175. image = [UIImage imageNamed: imageName];
  176. if (image)
  177. break;
  178. }
  179. if (image == nil)
  180. {
  181. // Old launch image from file
  182. for (NSString* imageName in imageNames)
  183. {
  184. image = [UIImage imageNamed: imageName];
  185. if (image)
  186. break;
  187. NSString* imagePath = [[NSBundle mainBundle] pathForResource: imageName ofType: @"png"];
  188. image = [UIImage imageWithContentsOfFile: imagePath];
  189. if (image)
  190. break;
  191. }
  192. }
  193. // should not ever happen, but just in case
  194. if (image == nil)
  195. return;
  196. if (self->m_ImageView == nil)
  197. {
  198. self->m_ImageView = [[UIImageView alloc] initWithImage: image];
  199. [self addSubview: self->m_ImageView];
  200. }
  201. else
  202. {
  203. self->m_ImageView.image = image;
  204. }
  205. }
  206. - (void)layoutSubviews
  207. {
  208. if (self->m_XibView)
  209. self->m_XibView.frame = self.bounds;
  210. else if (self->m_ImageView)
  211. self->m_ImageView.frame = self.bounds;
  212. }
  213. + (SplashScreen*)Instance
  214. {
  215. return _splash;
  216. }
  217. - (void)FreeSubviews
  218. {
  219. m_ImageView = nil;
  220. m_XibView = nil;
  221. }
  222. @end
  223. @implementation SplashScreenController
  224. #if !PLATFORM_TVOS
  225. static void WillRotateToInterfaceOrientation_DefaultImpl(id self_, SEL _cmd, UIInterfaceOrientation toInterfaceOrientation, NSTimeInterval duration)
  226. {
  227. if (_isOrientable)
  228. [_splash updateOrientation: ConvertToUnityScreenOrientation(toInterfaceOrientation)];
  229. UNITY_OBJC_FORWARD_TO_SUPER(self_, [UIViewController class], @selector(willRotateToInterfaceOrientation:duration:), WillRotateToInterfaceOrientationSendFunc, toInterfaceOrientation, duration);
  230. }
  231. static void DidRotateFromInterfaceOrientation_DefaultImpl(id self_, SEL _cmd, UIInterfaceOrientation fromInterfaceOrientation)
  232. {
  233. if (!_isOrientable)
  234. OrientView((SplashScreenController*)self_, _splash, _nonOrientableDefaultOrientation);
  235. UNITY_OBJC_FORWARD_TO_SUPER(self_, [UIViewController class], @selector(didRotateFromInterfaceOrientation:), DidRotateFromInterfaceOrientationSendFunc, fromInterfaceOrientation);
  236. }
  237. #endif
  238. static void ViewWillTransitionToSize_DefaultImpl(id self_, SEL _cmd, CGSize size, id<UIViewControllerTransitionCoordinator> coordinator)
  239. {
  240. UnityViewControllerBase* self = (UnityViewControllerBase*)self_;
  241. ScreenOrientation curOrient = UIViewControllerOrientation(self);
  242. ScreenOrientation newOrient = OrientationAfterTransform(curOrient, [coordinator targetTransform]);
  243. if (_isOrientable)
  244. [_splash updateOrientation: newOrient];
  245. [coordinator animateAlongsideTransition: nil completion:^(id < UIViewControllerTransitionCoordinatorContext > context) {
  246. if (!_isOrientable)
  247. OrientView(self, _splash, _nonOrientableDefaultOrientation);
  248. }];
  249. UNITY_OBJC_FORWARD_TO_SUPER(self_, [UIViewController class], @selector(viewWillTransitionToSize:withTransitionCoordinator:), ViewWillTransitionToSizeSendFunc, size, coordinator);
  250. }
  251. - (id)init
  252. {
  253. if ((self = [super init]))
  254. {
  255. #if !PLATFORM_TVOS
  256. AddViewControllerRotationHandling(
  257. [SplashScreenController class],
  258. (IMP)&WillRotateToInterfaceOrientation_DefaultImpl, (IMP)&DidRotateFromInterfaceOrientation_DefaultImpl,
  259. (IMP)&ViewWillTransitionToSize_DefaultImpl
  260. );
  261. #endif
  262. }
  263. return self;
  264. }
  265. - (void)create:(UIWindow*)window
  266. {
  267. NSArray* supportedOrientation = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UISupportedInterfaceOrientations"];
  268. bool isIphone = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone;
  269. bool isIpad = !isIphone;
  270. // splash will be shown way before unity is inited so we need to override autorotation handling with values read from info.plist
  271. _canRotateToPortrait = [supportedOrientation containsObject: @"UIInterfaceOrientationPortrait"];
  272. _canRotateToPortraitUpsideDown = [supportedOrientation containsObject: @"UIInterfaceOrientationPortraitUpsideDown"];
  273. _canRotateToLandscapeLeft = [supportedOrientation containsObject: @"UIInterfaceOrientationLandscapeRight"];
  274. _canRotateToLandscapeRight = [supportedOrientation containsObject: @"UIInterfaceOrientationLandscapeLeft"];
  275. // special handling of devices/ios that do not support upside down orientation
  276. if (!UnityDeviceSupportsUpsideDown())
  277. {
  278. _canRotateToPortraitUpsideDown = false;
  279. const bool anySupported = _canRotateToPortrait || _canRotateToLandscapeLeft || _canRotateToLandscapeRight;
  280. if (!anySupported)
  281. {
  282. _canRotateToPortrait = true;
  283. printf_console("This device does not support UpsideDown orientation, so we switched to Portrait.\n");
  284. }
  285. }
  286. CGSize size = [[UIScreen mainScreen] bounds].size;
  287. // iPads and iPhone 6+ and iOS11 have orientable splash screen
  288. _isOrientable = isIpad || (size.height == 736 || size.width == 736) || _ios110orNewer;
  289. // Launch screens are used only on iOS8+ iPhones
  290. const char* xib = UnityGetLaunchScreenXib();
  291. #if !PLATFORM_TVOS
  292. _usesLaunchscreen = false;
  293. if (_ios80orNewer && xib != NULL)
  294. {
  295. const char* expectedName = isIphone ? "LaunchScreen-iPhone" : "LaunchScreen-iPad";
  296. if (std::strcmp(xib, expectedName) == 0)
  297. _usesLaunchscreen = true;
  298. }
  299. #else
  300. _usesLaunchscreen = false;
  301. #endif
  302. if (_usesLaunchscreen && !(_canRotateToPortrait || _canRotateToPortraitUpsideDown))
  303. _nonOrientableDefaultOrientation = landscapeLeft;
  304. else
  305. _nonOrientableDefaultOrientation = portrait;
  306. _splash = [[SplashScreen alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
  307. _splash.contentScaleFactor = [UIScreen mainScreen].scale;
  308. if (_isOrientable)
  309. {
  310. _splash.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  311. _splash.autoresizesSubviews = YES;
  312. }
  313. else if (_canRotateToPortrait || _canRotateToPortraitUpsideDown)
  314. {
  315. _canRotateToLandscapeLeft = false;
  316. _canRotateToLandscapeRight = false;
  317. }
  318. // On non-orientable devices with launch screens, landscapeLeft is always used if both
  319. // landscapeRight and landscapeLeft are enabled
  320. if (!_isOrientable && _usesLaunchscreen && _canRotateToLandscapeRight)
  321. {
  322. if (_canRotateToLandscapeLeft)
  323. _canRotateToLandscapeRight = false;
  324. else
  325. _nonOrientableDefaultOrientation = landscapeRight;
  326. }
  327. window.rootViewController = self;
  328. self.view = _splash;
  329. [window addSubview: _splash];
  330. [window bringSubviewToFront: _splash];
  331. ScreenOrientation orient = UIViewControllerOrientation(self);
  332. [_splash updateOrientation: orient];
  333. if (!_isOrientable)
  334. orient = _nonOrientableDefaultOrientation;
  335. // Fix iPhone 5,6 launch images (only in portrait) from being stretched
  336. if (isIphone && _isOrientable && !_usesLaunchscreen && ((size.height == 568 || size.width == 568) || (size.height == 667 || size.width == 667)))
  337. orient = portrait;
  338. OrientView([SplashScreenController Instance], _splash, orient);
  339. }
  340. - (BOOL)shouldAutorotate
  341. {
  342. return YES;
  343. }
  344. - (NSUInteger)supportedInterfaceOrientations
  345. {
  346. NSUInteger ret = 0;
  347. if (_canRotateToPortrait)
  348. ret |= (1 << UIInterfaceOrientationPortrait);
  349. if (_canRotateToPortraitUpsideDown)
  350. ret |= (1 << UIInterfaceOrientationPortraitUpsideDown);
  351. if (_canRotateToLandscapeLeft)
  352. ret |= (1 << UIInterfaceOrientationLandscapeRight);
  353. if (_canRotateToLandscapeRight)
  354. ret |= (1 << UIInterfaceOrientationLandscapeLeft);
  355. return ret;
  356. }
  357. + (SplashScreenController*)Instance
  358. {
  359. return _controller;
  360. }
  361. @end
  362. void ShowSplashScreen(UIWindow* window)
  363. {
  364. NSString* launchScreenStoryboard = [[[[NSBundle mainBundle] infoDictionary] objectForKey: @"UILaunchStoryboardName"] stringByDeletingPathExtension];
  365. const bool hasStoryboard = launchScreenStoryboard != nil && [[NSBundle mainBundle] pathForResource: launchScreenStoryboard ofType: @"storyboardc"] != nil;
  366. if (hasStoryboard)
  367. {
  368. UIStoryboard *storyboard = [UIStoryboard storyboardWithName: launchScreenStoryboard bundle: [NSBundle mainBundle]];
  369. _controller = [storyboard instantiateInitialViewController];
  370. window.rootViewController = _controller;
  371. }
  372. else
  373. {
  374. _controller = [[SplashScreenController alloc] init];
  375. [_controller create: window];
  376. }
  377. [window makeKeyAndVisible];
  378. }
  379. void HideSplashScreen()
  380. {
  381. if (_splash)
  382. {
  383. [_splash removeFromSuperview];
  384. [_splash FreeSubviews];
  385. }
  386. _splash = nil;
  387. _controller = nil;
  388. }