openEventsPlatformApp/plugins/cordova-plugin-splashscreen/src/ios/CDVSplashScreen.m
plugins/cordova-plugin-splashscreen/src/ios/CDVSplashScreen.mRaw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18 */
19
20#import "CDVSplashScreen.h"
21#import <Cordova/CDVViewController.h>
22#import <Cordova/CDVScreenOrientationDelegate.h>
23#import "CDVViewController+SplashScreen.h"
24
25#define kSplashScreenDurationDefault 3000.0f
26#define kFadeDurationDefault 500.0f
27
28
29@implementation CDVSplashScreen
30
31- (void)pluginInitialize
32{
33 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pageDidLoad) name:CDVPageDidLoadNotification object:nil];
34
35 [self setVisible:YES];
36}
37
38- (void)show:(CDVInvokedUrlCommand*)command
39{
40 [self setVisible:YES];
41}
42
43- (void)hide:(CDVInvokedUrlCommand*)command
44{
45 [self setVisible:NO andForce:YES];
46}
47
48- (void)pageDidLoad
49{
50 id autoHideSplashScreenValue = [self.commandDelegate.settings objectForKey:[@"AutoHideSplashScreen" lowercaseString]];
51
52 // if value is missing, default to yes
53 if ((autoHideSplashScreenValue == nil) || [autoHideSplashScreenValue boolValue]) {
54 [self setVisible:NO];
55 }
56}
57
58- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
59{
60 [self updateImage];
61}
62
63- (void)createViews
64{
65 /*
66 * The Activity View is the top spinning throbber in the status/battery bar. We init it with the default Grey Style.
67 *
68 * whiteLarge = UIActivityIndicatorViewStyleWhiteLarge
69 * white = UIActivityIndicatorViewStyleWhite
70 * gray = UIActivityIndicatorViewStyleGray
71 *
72 */
73
74 // Determine whether rotation should be enabled for this device
75 // Per iOS HIG, landscape is only supported on iPad and iPhone 6+
76 CDV_iOSDevice device = [self getCurrentDevice];
77 BOOL autorotateValue = (device.iPad || device.iPhone6Plus) ?
78 [(CDVViewController *)self.viewController shouldAutorotateDefaultValue] :
79 NO;
80
81 [(CDVViewController *)self.viewController setEnabledAutorotation:autorotateValue];
82
83 NSString* topActivityIndicator = [self.commandDelegate.settings objectForKey:[@"TopActivityIndicator" lowercaseString]];
84 UIActivityIndicatorViewStyle topActivityIndicatorStyle = UIActivityIndicatorViewStyleGray;
85
86 if ([topActivityIndicator isEqualToString:@"whiteLarge"])
87 {
88 topActivityIndicatorStyle = UIActivityIndicatorViewStyleWhiteLarge;
89 }
90 else if ([topActivityIndicator isEqualToString:@"white"])
91 {
92 topActivityIndicatorStyle = UIActivityIndicatorViewStyleWhite;
93 }
94 else if ([topActivityIndicator isEqualToString:@"gray"])
95 {
96 topActivityIndicatorStyle = UIActivityIndicatorViewStyleGray;
97 }
98
99 UIView* parentView = self.viewController.view;
100 parentView.userInteractionEnabled = NO; // disable user interaction while splashscreen is shown
101 _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:topActivityIndicatorStyle];
102 _activityView.center = CGPointMake(parentView.bounds.size.width / 2, parentView.bounds.size.height / 2);
103 _activityView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin
104 | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin;
105 [_activityView startAnimating];
106
107 // Set the frame & image later.
108 _imageView = [[UIImageView alloc] init];
109 [parentView addSubview:_imageView];
110
111 id showSplashScreenSpinnerValue = [self.commandDelegate.settings objectForKey:[@"ShowSplashScreenSpinner" lowercaseString]];
112 // backwards compatibility - if key is missing, default to true
113 if ((showSplashScreenSpinnerValue == nil) || [showSplashScreenSpinnerValue boolValue])
114 {
115 [parentView addSubview:_activityView];
116 }
117
118 // Frame is required when launching in portrait mode.
119 // Bounds for landscape since it captures the rotation.
120 [parentView addObserver:self forKeyPath:@"frame" options:0 context:nil];
121 [parentView addObserver:self forKeyPath:@"bounds" options:0 context:nil];
122
123 [self updateImage];
124 _destroyed = NO;
125}
126
127- (void)hideViews
128{
129 [_imageView setAlpha:0];
130 [_activityView setAlpha:0];
131}
132
133- (void)destroyViews
134{
135 _destroyed = YES;
136 [(CDVViewController *)self.viewController setEnabledAutorotation:[(CDVViewController *)self.viewController shouldAutorotateDefaultValue]];
137
138 [_imageView removeFromSuperview];
139 [_activityView removeFromSuperview];
140 _imageView = nil;
141 _activityView = nil;
142 _curImageName = nil;
143
144 self.viewController.view.userInteractionEnabled = YES; // re-enable user interaction upon completion
145 @try {
146 [self.viewController.view removeObserver:self forKeyPath:@"frame"];
147 [self.viewController.view removeObserver:self forKeyPath:@"bounds"];
148 }
149 @catch (NSException *exception) {
150 // When reloading the page from a remotely connected Safari, there
151 // are no observers, so the removeObserver method throws an exception,
152 // that we can safely ignore.
153 // Alternatively we can check whether there are observers before calling removeObserver
154 }
155}
156
157- (CDV_iOSDevice) getCurrentDevice
158{
159 CDV_iOSDevice device;
160
161 UIScreen* mainScreen = [UIScreen mainScreen];
162 CGFloat mainScreenHeight = mainScreen.bounds.size.height;
163 CGFloat mainScreenWidth = mainScreen.bounds.size.width;
164
165 int limit = MAX(mainScreenHeight,mainScreenWidth);
166
167 device.iPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
168 device.iPhone = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone);
169 device.retina = ([mainScreen scale] == 2.0);
170 device.iPhone4 = (device.iPhone && limit == 480.0);
171 device.iPhone5 = (device.iPhone && limit == 568.0);
172 // note these below is not a true device detect, for example if you are on an
173 // iPhone 6/6+ but the app is scaled it will prob set iPhone5 as true, but
174 // this is appropriate for detecting the runtime screen environment
175 device.iPhone6 = (device.iPhone && limit == 667.0);
176 device.iPhone6Plus = (device.iPhone && limit == 736.0);
177
178 return device;
179}
180
181- (BOOL) isUsingCDVLaunchScreen {
182 NSString* launchStoryboardName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UILaunchStoryboardName"];
183 if (launchStoryboardName) {
184 return ([launchStoryboardName isEqualToString:@"CDVLaunchScreen"]);
185 } else {
186 return NO;
187 }
188}
189
190- (NSString*)getImageName:(UIInterfaceOrientation)currentOrientation delegate:(id<CDVScreenOrientationDelegate>)orientationDelegate device:(CDV_iOSDevice)device
191{
192 // Use UILaunchImageFile if specified in plist. Otherwise, use Default.
193 NSString* imageName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UILaunchImageFile"];
194
195 // detect if we are using CB-9762 Launch Storyboard; if so, return the associated image instead
196 if ([self isUsingCDVLaunchScreen]) {
197 imageName = @"LaunchStoryboard";
198 return imageName;
199 }
200
201 NSUInteger supportedOrientations = [orientationDelegate supportedInterfaceOrientations];
202
203 // Checks to see if the developer has locked the orientation to use only one of Portrait or Landscape
204 BOOL supportsLandscape = (supportedOrientations & UIInterfaceOrientationMaskLandscape);
205 BOOL supportsPortrait = (supportedOrientations & UIInterfaceOrientationMaskPortrait || supportedOrientations & UIInterfaceOrientationMaskPortraitUpsideDown);
206 // this means there are no mixed orientations in there
207 BOOL isOrientationLocked = !(supportsPortrait && supportsLandscape);
208
209 if (imageName)
210 {
211 imageName = [imageName stringByDeletingPathExtension];
212 }
213 else
214 {
215 imageName = @"Default";
216 }
217
218 // Add Asset Catalog specific prefixes
219 if ([imageName isEqualToString:@"LaunchImage"])
220 {
221 if (device.iPhone4 || device.iPhone5 || device.iPad) {
222 imageName = [imageName stringByAppendingString:@"-700"];
223 } else if(device.iPhone6) {
224 imageName = [imageName stringByAppendingString:@"-800"];
225 } else if(device.iPhone6Plus) {
226 imageName = [imageName stringByAppendingString:@"-800"];
227 if (currentOrientation == UIInterfaceOrientationPortrait || currentOrientation == UIInterfaceOrientationPortraitUpsideDown)
228 {
229 imageName = [imageName stringByAppendingString:@"-Portrait"];
230 }
231 }
232 }
233
234 if (device.iPhone5)
235 { // does not support landscape
236 imageName = [imageName stringByAppendingString:@"-568h"];
237 }
238 else if (device.iPhone6)
239 { // does not support landscape
240 imageName = [imageName stringByAppendingString:@"-667h"];
241 }
242 else if (device.iPhone6Plus)
243 { // supports landscape
244 if (isOrientationLocked)
245 {
246 imageName = [imageName stringByAppendingString:(supportsLandscape ? @"-Landscape" : @"")];
247 }
248 else
249 {
250 switch (currentOrientation)
251 {
252 case UIInterfaceOrientationLandscapeLeft:
253 case UIInterfaceOrientationLandscapeRight:
254 imageName = [imageName stringByAppendingString:@"-Landscape"];
255 break;
256 default:
257 break;
258 }
259 }
260 imageName = [imageName stringByAppendingString:@"-736h"];
261
262 }
263 else if (device.iPad)
264 { // supports landscape
265 if (isOrientationLocked)
266 {
267 imageName = [imageName stringByAppendingString:(supportsLandscape ? @"-Landscape" : @"-Portrait")];
268 }
269 else
270 {
271 switch (currentOrientation)
272 {
273 case UIInterfaceOrientationLandscapeLeft:
274 case UIInterfaceOrientationLandscapeRight:
275 imageName = [imageName stringByAppendingString:@"-Landscape"];
276 break;
277
278 case UIInterfaceOrientationPortrait:
279 case UIInterfaceOrientationPortraitUpsideDown:
280 default:
281 imageName = [imageName stringByAppendingString:@"-Portrait"];
282 break;
283 }
284 }
285 }
286
287 return imageName;
288}
289
290- (UIInterfaceOrientation)getCurrentOrientation
291{
292 UIInterfaceOrientation iOrientation = [UIApplication sharedApplication].statusBarOrientation;
293 UIDeviceOrientation dOrientation = [UIDevice currentDevice].orientation;
294
295 bool landscape;
296
297 if (dOrientation == UIDeviceOrientationUnknown || dOrientation == UIDeviceOrientationFaceUp || dOrientation == UIDeviceOrientationFaceDown) {
298 // If the device is laying down, use the UIInterfaceOrientation based on the status bar.
299 landscape = UIInterfaceOrientationIsLandscape(iOrientation);
300 } else {
301 // If the device is not laying down, use UIDeviceOrientation.
302 landscape = UIDeviceOrientationIsLandscape(dOrientation);
303
304 // There's a bug in iOS!!!! http://openradar.appspot.com/7216046
305 // So values needs to be reversed for landscape!
306 if (dOrientation == UIDeviceOrientationLandscapeLeft)
307 {
308 iOrientation = UIInterfaceOrientationLandscapeRight;
309 }
310 else if (dOrientation == UIDeviceOrientationLandscapeRight)
311 {
312 iOrientation = UIInterfaceOrientationLandscapeLeft;
313 }
314 else if (dOrientation == UIDeviceOrientationPortrait)
315 {
316 iOrientation = UIInterfaceOrientationPortrait;
317 }
318 else if (dOrientation == UIDeviceOrientationPortraitUpsideDown)
319 {
320 iOrientation = UIInterfaceOrientationPortraitUpsideDown;
321 }
322 }
323
324 return iOrientation;
325}
326
327// Sets the view's frame and image.
328- (void)updateImage
329{
330 NSString* imageName = [self getImageName:[self getCurrentOrientation] delegate:(id<CDVScreenOrientationDelegate>)self.viewController device:[self getCurrentDevice]];
331
332 if (![imageName isEqualToString:_curImageName])
333 {
334 UIImage* img = [UIImage imageNamed:imageName];
335 _imageView.image = img;
336 _curImageName = imageName;
337 }
338
339 // Check that splash screen's image exists before updating bounds
340 if (_imageView.image)
341 {
342 [self updateBounds];
343 }
344 else
345 {
346 NSLog(@"WARNING: The splashscreen image named %@ was not found", imageName);
347 }
348}
349
350- (void)updateBounds
351{
352 if ([self isUsingCDVLaunchScreen]) {
353 // CB-9762's launch screen expects the image to fill the screen and be scaled using AspectFill.
354 CGSize viewportSize = [UIApplication sharedApplication].delegate.window.bounds.size;
355 _imageView.frame = CGRectMake(0, 0, viewportSize.width, viewportSize.height);
356 _imageView.contentMode = UIViewContentModeScaleAspectFill;
357 return;
358 }
359
360 UIImage* img = _imageView.image;
361 CGRect imgBounds = (img) ? CGRectMake(0, 0, img.size.width, img.size.height) : CGRectZero;
362
363 CGSize screenSize = [self.viewController.view convertRect:[UIScreen mainScreen].bounds fromView:nil].size;
364 UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
365 CGAffineTransform imgTransform = CGAffineTransformIdentity;
366
367 /* If and only if an iPhone application is landscape-only as per
368 * UISupportedInterfaceOrientations, the view controller's orientation is
369 * landscape. In this case the image must be rotated in order to appear
370 * correctly.
371 */
372 CDV_iOSDevice device = [self getCurrentDevice];
373 if (UIInterfaceOrientationIsLandscape(orientation) && !device.iPhone6Plus && !device.iPad)
374 {
375 imgTransform = CGAffineTransformMakeRotation(M_PI / 2);
376 imgBounds.size = CGSizeMake(imgBounds.size.height, imgBounds.size.width);
377 }
378
379 // There's a special case when the image is the size of the screen.
380 if (CGSizeEqualToSize(screenSize, imgBounds.size))
381 {
382 CGRect statusFrame = [self.viewController.view convertRect:[UIApplication sharedApplication].statusBarFrame fromView:nil];
383 if (!(IsAtLeastiOSVersion(@"7.0")))
384 {
385 imgBounds.origin.y -= statusFrame.size.height;
386 }
387 }
388 else if (imgBounds.size.width > 0)
389 {
390 CGRect viewBounds = self.viewController.view.bounds;
391 CGFloat imgAspect = imgBounds.size.width / imgBounds.size.height;
392 CGFloat viewAspect = viewBounds.size.width / viewBounds.size.height;
393 // This matches the behaviour of the native splash screen.
394 CGFloat ratio;
395 if (viewAspect > imgAspect)
396 {
397 ratio = viewBounds.size.width / imgBounds.size.width;
398 }
399 else
400 {
401 ratio = viewBounds.size.height / imgBounds.size.height;
402 }
403 imgBounds.size.height *= ratio;
404 imgBounds.size.width *= ratio;
405 }
406
407 _imageView.transform = imgTransform;
408 _imageView.frame = imgBounds;
409}
410
411- (void)setVisible:(BOOL)visible
412{
413 [self setVisible:visible andForce:NO];
414}
415
416- (void)setVisible:(BOOL)visible andForce:(BOOL)force
417{
418 if (visible != _visible || force)
419 {
420 _visible = visible;
421
422 id fadeSplashScreenValue = [self.commandDelegate.settings objectForKey:[@"FadeSplashScreen" lowercaseString]];
423 id fadeSplashScreenDuration = [self.commandDelegate.settings objectForKey:[@"FadeSplashScreenDuration" lowercaseString]];
424
425 float fadeDuration = fadeSplashScreenDuration == nil ? kFadeDurationDefault : [fadeSplashScreenDuration floatValue];
426
427 id splashDurationString = [self.commandDelegate.settings objectForKey: [@"SplashScreenDelay" lowercaseString]];
428 float splashDuration = splashDurationString == nil ? kSplashScreenDurationDefault : [splashDurationString floatValue];
429
430 id autoHideSplashScreenValue = [self.commandDelegate.settings objectForKey:[@"AutoHideSplashScreen" lowercaseString]];
431 BOOL autoHideSplashScreen = true;
432
433 if (autoHideSplashScreenValue != nil) {
434 autoHideSplashScreen = [autoHideSplashScreenValue boolValue];
435 }
436
437 if (!autoHideSplashScreen) {
438 // CB-10412 SplashScreenDelay does not make sense if the splashscreen is hidden manually
439 splashDuration = 0;
440 }
441
442
443 if (fadeSplashScreenValue == nil)
444 {
445 fadeSplashScreenValue = @"true";
446 }
447
448 if (![fadeSplashScreenValue boolValue])
449 {
450 fadeDuration = 0;
451 }
452 else if (fadeDuration < 30)
453 {
454 // [CB-9750] This value used to be in decimal seconds, so we will assume that if someone specifies 10
455 // they mean 10 seconds, and not the meaningless 10ms
456 fadeDuration *= 1000;
457 }
458
459 if (_visible)
460 {
461 if (_imageView == nil)
462 {
463 [self createViews];
464 }
465 }
466 else if (fadeDuration == 0 && splashDuration == 0)
467 {
468 [self destroyViews];
469 }
470 else
471 {
472 __weak __typeof(self) weakSelf = self;
473 float effectiveSplashDuration;
474
475 // [CB-10562] AutoHideSplashScreen may be "true" but we should still be able to hide the splashscreen manually.
476 if (!autoHideSplashScreen || force) {
477 effectiveSplashDuration = (fadeDuration) / 1000;
478 } else {
479 effectiveSplashDuration = (splashDuration - fadeDuration) / 1000;
480 }
481
482 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (uint64_t) effectiveSplashDuration * NSEC_PER_SEC), dispatch_get_main_queue(), CFBridgingRelease(CFBridgingRetain(^(void) {
483 if (!_destroyed) {
484 [UIView transitionWithView:self.viewController.view
485 duration:(fadeDuration / 1000)
486 options:UIViewAnimationOptionTransitionNone
487 animations:^(void) {
488 [weakSelf hideViews];
489 }
490 completion:^(BOOL finished) {
491 // Always destroy views, otherwise you could have an
492 // invisible splashscreen that is overlayed over your active views
493 // which causes that no touch events are passed
494 if (!_destroyed) {
495 [weakSelf destroyViews];
496 // TODO: It might also be nice to have a js event happen here -jm
497 }
498 }
499 ];
500 }
501 })));
502 }
503 }
504}
505
506@end