Skip to content

Commit 0ab311f

Browse files
authored
feat(statusbar): Simple built-in status bar support (#1523)
* fix: Prevent recursion in scrollView monkeypatch If you call scrollView on a UIView that does not have its own scrollView method, this hijack method will keep recursively calling itself until you run out of stack space. Try to guard against that by making sure that it's not getting called on the same instance multiple times. TODO: Think about thread safety, but in theory because this is view-related stuff it's only safe to run on the main thread. * feat(statusbar): Simple built-in status bar background This adds a background behind the status bar automatically when a page does not request to extend itself behind the status bar using a meta tag with `viewport-fit=cover`. It automatically shows and hides as the value of the viewport-fit option changes. On iOS 16.4 and newer, it will be coloured according to the meta `theme-color` tag, and a default colour can be set in the storyboard for a CDVViewController. We expose a very basic JS API hanging off the existing `window.statusbar` property that allows controlling the visibility (of the status bar contents entirely, not just of the background view) and overriding the background colour. The colour is determined as follows: 1. Any colour set explicitly with the JS API (if any) 2. Any colour pulled from the meta tag (iOS 16.4+, if any) 3. The default colour specified in the storyboard (if any) 4. The background colour specified in the storyboard (if any) 5. The default system background colour Efforts were made to ensure that this does not interfere with the existing CDVStatusBar plugin implementation (although maybe this can replace it for most common use cases), by making the display of this built-in conditional on not having the CDVStatusBar plugin installed. * feat(statusbar): Hook up StatusBarBackgroundColor
1 parent b41a842 commit 0ab311f

File tree

17 files changed

+434
-7
lines changed

17 files changed

+434
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 <Cordova/CDVViewController.h>
21+
22+
@interface CDVViewController (Private)
23+
24+
- (void)setStatusBarWebViewColor:(UIColor *)color;
25+
26+
- (void)showStatusBar:(BOOL)visible;
27+
28+
@end

CordovaLib/Classes/Private/Plugins/CDVLaunchScreen/CDVLaunchScreen.m

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Licensed to the Apache Software Foundation (ASF) under one
1818
*/
1919

2020
#import "CDVLaunchScreen.h"
21+
#import "CDVViewController+Private.h"
2122

2223
@implementation CDVLaunchScreen
2324

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 <Cordova/CDVPlugin.h>
21+
22+
@interface CDVStatusBarInternal : CDVPlugin
23+
24+
- (void)setVisible:(CDVInvokedUrlCommand*)command;
25+
- (void)setBackgroundColor:(CDVInvokedUrlCommand*)command;
26+
27+
@end
28+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 "CDVStatusBarInternal.h"
21+
#import "CDVViewController+Private.h"
22+
23+
@implementation CDVStatusBarInternal
24+
25+
- (void)setVisible:(CDVInvokedUrlCommand *)command
26+
{
27+
id value = [command argumentAtIndex:0];
28+
if (!([value isKindOfClass:[NSNumber class]])) {
29+
value = [NSNumber numberWithBool:YES];
30+
}
31+
32+
[self.viewController showStatusBar:[value boolValue]];
33+
}
34+
35+
- (void)setBackgroundColor:(CDVInvokedUrlCommand *)command
36+
{
37+
NSInteger valueR = [[command argumentAtIndex:0 withDefault:@0] integerValue];
38+
NSInteger valueG = [[command argumentAtIndex:1 withDefault:@0] integerValue];
39+
NSInteger valueB = [[command argumentAtIndex:2 withDefault:@0] integerValue];
40+
41+
UIColor *bgColor = [UIColor colorWithRed:valueR/255.f green:valueG/255.f blue:valueB/255.f alpha:1.f];
42+
[self.viewController setStatusBarBackgroundColor:bgColor];
43+
}
44+
45+
@end
46+

CordovaLib/Classes/Private/Plugins/CDVWebViewEngine/CDVWebViewEngine.m

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Licensed to the Apache Software Foundation (ASF) under one
2222
#import "CDVURLSchemeHandler.h"
2323
#import <Cordova/CDVWebViewProcessPoolFactory.h>
2424
#import <Cordova/CDVSettingsDictionary.h>
25+
#import "CDVViewController+Private.h"
2526

2627
#import <objc/message.h>
2728

@@ -242,6 +243,8 @@ - (void)pluginInitialize
242243
wkWebView.customUserAgent = [settings cordovaSettingForKey:@"OverrideUserAgent"];
243244
}
244245

246+
[wkWebView addObserver:self forKeyPath:@"themeColor" options:NSKeyValueObservingOptionInitial context:nil];
247+
245248
self.engineWebView = wkWebView;
246249

247250
if ([self.viewController conformsToProtocol:@protocol(WKUIDelegate)]) {
@@ -477,6 +480,17 @@ - (CDVWebViewPermissionGrantType)parsePermissionGrantType:(NSString*)optionStrin
477480
return result;
478481
}
479482

483+
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
484+
{
485+
if ([keyPath isEqualToString:@"themeColor"]) {
486+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
487+
if (@available(iOS 15.0, *)) {
488+
[self.viewController setStatusBarWebViewColor:((WKWebView *)self.engineWebView).themeColor];
489+
}
490+
#endif
491+
}
492+
}
493+
480494
#pragma mark - WKScriptMessageHandler implementation
481495

482496
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message

CordovaLib/Classes/Public/CDVPlugin.m

+8-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ @implementation UIView (org_apache_cordova_UIView_Extension)
2828

2929
- (UIScrollView*)scrollView
3030
{
31-
if ([self respondsToSelector:@selector(scrollView)]) {
32-
return [self performSelector:@selector(scrollView)];
31+
static UIView *caller = nil;
32+
33+
if (caller != self && [self respondsToSelector:@selector(scrollView)]) {
34+
caller = self;
35+
UIScrollView *sv = [self performSelector:@selector(scrollView)];
36+
caller = nil;
37+
return sv;
3338
}
39+
caller = nil;
3440
return nil;
3541
}
3642

CordovaLib/Classes/Public/CDVViewController.m

+107-5
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,27 @@ Licensed to the Apache Software Foundation (ASF) under one
3030
#import <Cordova/CDVTimer.h>
3131
#import "CDVCommandDelegateImpl.h"
3232

33-
static UIColor* defaultBackgroundColor(void) {
33+
static UIColor *defaultBackgroundColor(void) {
3434
return UIColor.systemBackgroundColor;
3535
}
3636

37-
@interface CDVViewController () <CDVWebViewEngineConfigurationDelegate> {
37+
@interface CDVViewController () <CDVWebViewEngineConfigurationDelegate, UIScrollViewDelegate> {
3838
id <CDVWebViewEngineProtocol> _webViewEngine;
3939
id <CDVCommandDelegate> _commandDelegate;
4040
NSMutableDictionary<NSString *, CDVPlugin *> *_pluginObjects;
4141
NSMutableDictionary<NSString *, NSString *> *_pluginsMap;
4242
CDVCommandQueue* _commandQueue;
43-
UIColor* _backgroundColor;
44-
UIColor* _splashBackgroundColor;
43+
UIColor *_backgroundColor;
44+
UIColor *_splashBackgroundColor;
45+
UIColor *_statusBarBackgroundColor;
46+
UIColor *_statusBarWebViewColor;
47+
UIColor *_statusBarDefaultColor;
4548
CDVSettingsDictionary* _settings;
4649
}
4750

4851
@property (nonatomic, readwrite, strong) NSMutableArray* startupPluginNames;
49-
@property (nonatomic, readwrite, strong) UIView* launchView;
52+
@property (nonatomic, readwrite, strong) UIView *launchView;
53+
@property (nonatomic, readwrite, strong) UIView *statusBar;
5054
@property (readwrite, assign) BOOL initialized;
5155

5256
@end
@@ -60,6 +64,7 @@ @implementation CDVViewController
6064
@synthesize webViewEngine = _webViewEngine;
6165
@synthesize backgroundColor = _backgroundColor;
6266
@synthesize splashBackgroundColor = _splashBackgroundColor;
67+
@synthesize statusBarBackgroundColor = _statusBarBackgroundColor;
6368
@synthesize settings = _settings;
6469
@dynamic webView;
6570
@dynamic enumerablePlugins;
@@ -168,11 +173,49 @@ - (void)setWwwFolderName:(NSString *)name
168173
- (void)setBackgroundColor:(UIColor *)color
169174
{
170175
_backgroundColor = color ?: defaultBackgroundColor();
176+
177+
[self.webView setBackgroundColor:self.backgroundColor];
171178
}
172179

173180
- (void)setSplashBackgroundColor:(UIColor *)color
174181
{
175182
_splashBackgroundColor = color ?: self.backgroundColor;
183+
184+
[self.launchView setBackgroundColor:self.splashBackgroundColor];
185+
}
186+
187+
- (UIColor *)statusBarBackgroundColor
188+
{
189+
// If a status bar background color has been explicitly set using the JS API, we always use that.
190+
// Otherwise, if the webview reports a themeColor meta tag (iOS 15.4+) we use that.
191+
// Otherwise, we use the status bar background color provided in IB (from config.xml).
192+
// Otherwise, we use the background color.
193+
return _statusBarBackgroundColor ?: _statusBarWebViewColor ?: _statusBarDefaultColor ?: self.backgroundColor;
194+
}
195+
196+
- (void)setStatusBarBackgroundColor:(UIColor *)color
197+
{
198+
// We want the initial value from IB to set the statusBarDefaultColor and
199+
// then all future changes to set the statusBarBackgroundColor.
200+
//
201+
// The reason for this is that statusBarBackgroundColor is treated like a
202+
// forced override when it is set, and we don't want that for the initial
203+
// value from config.xml set via IB.
204+
205+
if (!_statusBarBackgroundColor && !_statusBarWebViewColor && !_statusBarDefaultColor) {
206+
_statusBarDefaultColor = color;
207+
} else {
208+
_statusBarBackgroundColor = color;
209+
}
210+
211+
[self.statusBar setBackgroundColor:self.statusBarBackgroundColor];
212+
}
213+
214+
- (void)setStatusBarWebViewColor:(UIColor *)color
215+
{
216+
_statusBarWebViewColor = color;
217+
218+
[self.statusBar setBackgroundColor:self.statusBarBackgroundColor];
176219
}
177220

178221
// Only for testing
@@ -320,6 +363,11 @@ - (void)viewDidLoad
320363
[self createGapView];
321364
}
322365

366+
// Instantiate the status bar
367+
if (!self.statusBar) {
368+
[self createStatusBarView];
369+
}
370+
323371
// /////////////////
324372

325373
if ([self.startupPluginNames count] > 0) {
@@ -358,6 +406,7 @@ - (void)viewDidLoad
358406

359407
[self.webView setBackgroundColor:self.backgroundColor];
360408
[self.launchView setBackgroundColor:self.splashBackgroundColor];
409+
[self.statusBar setBackgroundColor:self.statusBarBackgroundColor];
361410

362411
if (self.showInitialSplashScreen) {
363412
[self.launchView setAlpha:1];
@@ -525,6 +574,11 @@ - (void)onWebViewPageDidLoad:(NSNotification*)notification
525574
{
526575
self.webView.hidden = NO;
527576

577+
if ([self.webView respondsToSelector:@selector(scrollView)]) {
578+
UIScrollView *scrollView = [self.webView performSelector:@selector(scrollView)];
579+
[self scrollViewDidChangeAdjustedContentInset:scrollView];
580+
}
581+
528582
if ([self.settings cordovaBoolSettingForKey:@"AutoHideSplashScreen" defaultValue:YES]) {
529583
CGFloat splashScreenDelaySetting = [self.settings cordovaFloatSettingForKey:@"SplashScreenDelay" defaultValue:0];
530584

@@ -541,6 +595,23 @@ - (void)onWebViewPageDidLoad:(NSNotification*)notification
541595
}
542596
}
543597

598+
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView
599+
{
600+
if (self.webView.hidden) {
601+
self.statusBar.hidden = true;
602+
return;
603+
}
604+
605+
self.statusBar.hidden = (scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentNever);
606+
}
607+
608+
- (BOOL)prefersStatusBarHidden
609+
{
610+
// The CDVStatusBar plugin overrides this in a category extension, and
611+
// should bypass this implementation entirely
612+
return self.statusBar.alpha < 0.0001f;
613+
}
614+
544615
#pragma mark - View Setup
545616

546617
- (void)loadSettings
@@ -667,6 +738,31 @@ - (void)createGapView
667738

668739
[self.view addSubview:view];
669740
[self.view sendSubviewToBack:view];
741+
742+
if ([self.webView respondsToSelector:@selector(scrollView)]) {
743+
UIScrollView *scrollView = [self.webView performSelector:@selector(scrollView)];
744+
scrollView.delegate = self;
745+
}
746+
}
747+
748+
- (void)createStatusBarView
749+
{
750+
// If cordova-plugin-statusbar is loaded, we'll let it handle the status
751+
// bar to avoid introducing conflict
752+
if (NSClassFromString(@"CDVStatusBar") != nil)
753+
return;
754+
755+
self.statusBar = [[UIView alloc] init];
756+
self.statusBar.translatesAutoresizingMaskIntoConstraints = NO;
757+
758+
[self.view addSubview:self.statusBar];
759+
760+
[self.statusBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor].active = YES;
761+
[self.statusBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor].active = YES;
762+
[self.statusBar.topAnchor constraintEqualToAnchor:self.view.topAnchor].active = YES;
763+
[self.statusBar.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor].active = YES;
764+
765+
self.statusBar.hidden = YES;
670766
}
671767

672768
#pragma mark CordovaCommands
@@ -783,6 +879,12 @@ - (void)showSplashScreen:(BOOL)visible
783879
}];
784880
}
785881

882+
- (void)showStatusBar:(BOOL)visible
883+
{
884+
[self.statusBar setAlpha:(visible ? 1 : 0)];
885+
[self setNeedsStatusBarAppearanceUpdate];
886+
}
887+
786888
- (void)parseSettingsWithParser:(id <NSXMLParserDelegate>)delegate
787889
{
788890
[CDVConfigParser parseConfigFile:self.configFilePath withDelegate:delegate];

0 commit comments

Comments
 (0)