SplashScreen.mm 16 KB

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