Skip to content

Commit 15ae151

Browse files
committed
feat(ios): statusBarStyle improvements for other edge cases
1 parent edfa9b0 commit 15ae151

File tree

5 files changed

+108
-13
lines changed

5 files changed

+108
-13
lines changed

packages/core/ui/core/view/index.ios.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -965,20 +965,33 @@ export class View extends ViewCommon {
965965
}
966966
[statusBarStyleProperty.setNative](value: 'light' | 'dark') {
967967
this.style.statusBarStyle = value;
968+
this.updateStatusBarStyle(value);
969+
}
970+
971+
updateStatusBarStyle(value: 'dark' | 'light') {
972+
// Keep UINavigationBar style aligned (affects legacy + some container defaults).
968973
const parent = this.parent;
969-
if (parent) {
970-
const ctrl = parent.ios?.controller;
971-
if (ctrl && ctrl instanceof UINavigationController) {
972-
const navigationBar = ctrl.navigationBar;
973-
if (!navigationBar) return;
974+
const ctrl = parent?.ios?.controller;
975+
if (ctrl && ctrl instanceof UINavigationController) {
976+
const navigationBar = ctrl.navigationBar;
977+
if (navigationBar) {
978+
navigationBar.barStyle = value === 'light' ? UIBarStyle.Black : UIBarStyle.Default;
979+
}
980+
}
974981

975-
if (typeof value === 'string') {
976-
navigationBar.barStyle = value === 'dark' ? UIBarStyle.Black : UIBarStyle.Default;
977-
} else {
978-
navigationBar.barStyle = value;
979-
}
982+
// iOS requires a controller invalidation to re-evaluate `preferredStatusBarStyle`.
983+
const ownerController = this.viewController || IOSHelper.getParentWithViewController(this as any)?.viewController;
984+
985+
// Force overrideUserInterfaceStyle if available (iOS 13+) to ensure status bar contrast.
986+
if (SDK_VERSION >= 13 && ownerController) {
987+
const style = value === 'light' ? UIUserInterfaceStyle.Dark : UIUserInterfaceStyle.Light;
988+
ownerController.overrideUserInterfaceStyle = style;
989+
if (ctrl && ctrl instanceof UINavigationController) {
990+
ctrl.overrideUserInterfaceStyle = style;
980991
}
981992
}
993+
994+
IOSHelper.invalidateStatusBarAppearance(ownerController, `View.updateStatusBarStyle:${value}`);
982995
}
983996

984997
[iosGlassEffectProperty.setNative](value: GlassEffectType) {
@@ -1229,7 +1242,7 @@ export class CustomLayoutView extends ContainerView {
12291242
nativeViewProtected: UIView;
12301243

12311244
createNativeView() {
1232-
const window = getWindow<UIWindow>();
1245+
const window = getWindow<UIWindow>?.();
12331246
return UIView.alloc().initWithFrame(window ? window.screen.bounds : UIScreen.mainScreen.bounds);
12341247
}
12351248

packages/core/ui/core/view/view-helper/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export namespace IOSHelper {
6262
* @param view The view form which to start the search.
6363
*/
6464
export function getParentWithViewController(view: View): View;
65+
export function invalidateStatusBarAppearance(controller?: any /* UIViewController */, reason?: string): void;
6566
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void;
6667
export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
6768
export function layoutView(controller: any /* UIViewController */, owner: View): void;

packages/core/ui/core/view/view-helper/index.ios.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { View } from '..';
66
import { ViewHelper } from './view-helper-common';
77
import { SDK_VERSION } from '../../../../utils/constants';
88
import { layout, Trace } from './view-helper-shared';
9+
import { ios as iosUtils, getWindow } from '../../../../utils';
910

1011
export * from './view-helper-common';
1112
export const AndroidHelper = 0;
@@ -97,6 +98,9 @@ class UILayoutViewController extends UIViewController {
9798
return;
9899
}
99100

101+
// Ensure iOS re-queries `preferredStatusBarStyle` for this controller.
102+
IOSHelper.invalidateStatusBarAppearance(this, 'UILayoutViewController.viewWillAppear');
103+
100104
IOSHelper.updateAutoAdjustScrollInsets(this, owner);
101105

102106
if (!owner.isLoaded && !owner.parent) {
@@ -112,6 +116,14 @@ class UILayoutViewController extends UIViewController {
112116
}
113117
}
114118

119+
// Forward status bar appearance to our content controller when available.
120+
// Without this, iOS may use this controller's own preferredStatusBarStyle,
121+
// which can be unrelated to the currently shown Page.
122+
// @ts-ignore
123+
public get childViewControllerForStatusBarStyle(): UIViewController {
124+
return this.presentedViewController || this.childViewControllers?.lastObject;
125+
}
126+
115127
// Mind implementation for other controllers
116128
public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void {
117129
super.traitCollectionDidChange(previousTraitCollection);
@@ -213,6 +225,49 @@ export class IOSHelper {
213225
return view;
214226
}
215227

228+
static invalidateStatusBarAppearance(controller?: UIViewController, reason = ''): void {
229+
try {
230+
if (!controller) {
231+
const window = getWindow<UIWindow>?.();
232+
const rootController = window?.rootViewController;
233+
controller = rootController ? iosUtils.getVisibleViewController(rootController) : null;
234+
}
235+
236+
if (!controller) {
237+
if (Trace.isEnabled()) {
238+
Trace.write(`[StatusBar] invalidate skipped (no controller) reason=${reason}`, Trace.categories.NativeLifecycle);
239+
}
240+
return;
241+
}
242+
243+
const container = controller;
244+
let child: UIViewController = null;
245+
try {
246+
child = container.childViewControllerForStatusBarStyle;
247+
} catch {
248+
child = null;
249+
}
250+
if (!child) {
251+
if (container instanceof UINavigationController) {
252+
child = container.topViewController;
253+
} else if (container instanceof UITabBarController) {
254+
child = container.selectedViewController;
255+
}
256+
}
257+
258+
// Always invalidate container and likely child.
259+
container.setNeedsStatusBarAppearanceUpdate?.();
260+
child?.setNeedsStatusBarAppearanceUpdate?.();
261+
262+
// Also invalidate nav container if present.
263+
const nav = container instanceof UINavigationController ? container : container.navigationController;
264+
nav?.setNeedsStatusBarAppearanceUpdate?.();
265+
nav?.topViewController?.setNeedsStatusBarAppearanceUpdate?.();
266+
} catch (e) {
267+
Trace.write(`[StatusBar] invalidate error: ${e}`, Trace.categories.Error, Trace.messageType.warn);
268+
}
269+
}
270+
216271
static updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void {
217272
if (!__VISIONOS__ && SDK_VERSION <= 10) {
218273
owner._automaticallyAdjustsScrollViewInsets = false;

packages/core/ui/frame/index.ios.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ class UINavigationControllerDelegateImpl extends NSObject implements UINavigatio
511511
if (owner) {
512512
owner._onViewControllerShown(viewController);
513513
}
514+
IOSHelper.invalidateStatusBarAppearance(navigationController, 'navigationControllerDidShowViewControllerAnimated');
514515
}
515516
}
516517

@@ -580,19 +581,22 @@ class UINavigationControllerImpl extends UINavigationController {
580581
const navigationTransition = <NavigationTransition>viewController[TRANSITION];
581582
const owner = this._owner?.deref?.();
582583

584+
const logTag = 'UINavigationControllerImpl.pushViewControllerAnimated';
583585
if (Trace.isEnabled()) {
584-
Trace.write(`UINavigationControllerImpl.pushViewControllerAnimated(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
586+
Trace.write(`${logTag}(${viewController}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
585587
}
586588

587589
const nativeTransition = _getNativeTransition(navigationTransition, true, owner?.direction);
588590
if (!animated || !navigationTransition || !nativeTransition) {
589591
super.pushViewControllerAnimated(viewController, animated);
592+
IOSHelper.invalidateStatusBarAppearance(this, logTag);
590593
return;
591594
}
592595

593596
this.animateWithDuration(navigationTransition, nativeTransition, 'push', () => {
594597
super.pushViewControllerAnimated(viewController, false);
595598
});
599+
IOSHelper.invalidateStatusBarAppearance(this, logTag);
596600
}
597601

598602
@profile
@@ -604,8 +608,11 @@ class UINavigationControllerImpl extends UINavigationController {
604608
Trace.write(`UINavigationControllerImpl.setViewControllersAnimated(${viewControllers}, ${animated}); transition: ${JSON.stringify(navigationTransition)}`, Trace.categories.NativeLifecycle);
605609
}
606610

611+
const logTag = 'UINavigationControllerImpl.setViewControllersAnimated';
612+
607613
if (!animated || !navigationTransition) {
608614
super.setViewControllersAnimated(viewControllers, animated);
615+
IOSHelper.invalidateStatusBarAppearance(this, logTag);
609616
return;
610617
}
611618

@@ -614,12 +621,14 @@ class UINavigationControllerImpl extends UINavigationController {
614621

615622
if (!nativeTransition) {
616623
super.setViewControllersAnimated(viewControllers, animated);
624+
IOSHelper.invalidateStatusBarAppearance(this, logTag);
617625
return;
618626
}
619627

620628
this.animateWithDuration(navigationTransition, nativeTransition, 'set', () => {
621629
super.setViewControllersAnimated(viewControllers, false);
622630
});
631+
IOSHelper.invalidateStatusBarAppearance(this, logTag);
623632
}
624633

625634
public popViewControllerAnimated(animated: boolean): UIViewController {
@@ -704,6 +713,12 @@ class UINavigationControllerImpl extends UINavigationController {
704713
public get childViewControllerForStatusBarStyle() {
705714
return this.topViewController;
706715
}
716+
717+
// @ts-ignore
718+
public get preferredStatusBarStyle(): UIStatusBarStyle {
719+
const top = this.topViewController;
720+
return top?.preferredStatusBarStyle ?? UIStatusBarStyle.Default;
721+
}
707722
}
708723

709724
function _getTransitionId(nativeTransition: UIViewAnimationTransition, transitionType: string): string {

packages/core/ui/page/index.ios.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ class UIViewControllerImpl extends UIViewController {
117117
// Pages in backstack are unloaded so raise loaded here.
118118
if (!owner.isLoaded) {
119119
owner.callLoaded();
120+
// On first appearance, apply status bar style after the page is attached to the frame/nav stack.
121+
owner.updateStatusBar();
120122
} else {
121123
// Note: Handle the case of canceled backstack navigation. (https://github.com/NativeScript/NativeScript/issues/7430)
122124
// In this case viewWillAppear will be executed for the previous page and it will change the ActionBar
@@ -457,7 +459,16 @@ export class Page extends PageBase {
457459
const navigationController: UINavigationController = frame.ios.controller;
458460
const navigationBar = navigationController.navigationBar;
459461

460-
navigationBar.barStyle = value === 'dark' ? UIBarStyle.Black : UIBarStyle.Default;
462+
navigationBar.barStyle = value === 'light' ? UIBarStyle.Black : UIBarStyle.Default;
463+
464+
// Force overrideUserInterfaceStyle on the navigation controller as well
465+
if (SDK_VERSION >= 13) {
466+
const style = value === 'light' ? UIUserInterfaceStyle.Dark : UIUserInterfaceStyle.Light;
467+
navigationController.overrideUserInterfaceStyle = style;
468+
navigationBar.overrideUserInterfaceStyle = style;
469+
}
470+
471+
IOSHelper.invalidateStatusBarAppearance(navigationController, `Page._updateStatusBarStyle:${value}`);
461472
}
462473
}
463474

0 commit comments

Comments
 (0)