prefersStatusBarHidden isn't getting called - ios

I have a standard Master-Detail Application, and I'm trying to conditionally show/hide the status bar.
Overriding prefersStatusBarHidden() in MasterViewController does nothing. It never even gets called.
override func prefersStatusBarHidden() -> Bool {
return true
}
Setting UIViewControllerBasedStatusBarAppearance in Info.plist doesn't help, presumably since YES is already the default value. Calling setNeedsStatusBarAppearanceUpdate() doesn't help either.
I am targeting iOS 9.

There is a little bit cleaner solution. There is a function childViewControllerForStatusBarHidden which is specifically designed to return a child view controller to which prefersStatusBarHidden should be forwarded.
So, it will be better to override it. It will look like this:
override func childViewControllerForStatusBarHidden() -> UIViewController? {
if var topViewController = self.viewControllers.first {
if let navigationController = topViewController as? UINavigationController {
topViewController = navigationController.topViewController!
}
return topViewController
}
return super.childViewControllerForStatusBarHidden()
}
And probably you can even omit following. NavigationViewController has childViewControllerForStatusBarHidden() on it's own which will send it to child viewcontroller.
if let navigationController = topViewController as? UINavigationController {
topViewController = navigationController.topViewController!
}

The answer is to override prefersStatusBarHidden() starting from the window's root view controller. In a Master-Detail Application, this requires subclassing UISplitViewController to forward the message down the view controller hierarchy.
Something like this:
override func prefersStatusBarHidden() -> Bool {
if var topViewController = self.viewControllers.first {
if let navigationController = topViewController as? UINavigationController {
topViewController = navigationController.topViewController!
}
return topViewController.prefersStatusBarHidden()
}
return super.prefersStatusBarHidden()
}

If you're fine doing it for all your split view controllers, this works for me:
extension UISplitViewController {
override open var childForStatusBarHidden: UIViewController? {
return (viewControllers.last as? UINavigationController)?.visibleViewController
}
}

Related

getting current active viewController in UITabBarControllerDelegate

I want to implement a scrollToTop method on all of my viewControllers in my UITabBarController. The following is a method in the UITabBarControllerDelegate and triggers, when I select a tab.
The problem is, that I only want to scroll to the top of the viewController, when the viewController is active. So that the user can switch tabs without losing the scroll position, but when he touches the tab in the tabBar of the currently active tab, it should scroll to the top.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewControllerThatIsCurrentlyActiveInTabBar == viewController {
scrollToTop()
}
}
Basically, I need that condition of the if statement above.
I tried: viewController.isViewLoaded, tabBarController.selectedViewController == viewController, viewController.isBeingPresented. None of those conditions worked. It would either not trigger scrollToTop() or it would trigger always so that you lose the scroll position when you change tabs because it would immediately scroll to the top.
You need to make a code in should select instead of didselect. As it is unable to find the previous controller after selection. below is the example code for it.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.selectedViewController == viewController {
print("Same viewcontroller")
}
return true
}
Can you use below extension for getting top viewcontroller of tabbarcontroller.
extension UIViewController {
var top: UIViewController? {
if let controller = self as? UINavigationController {
return controller.topViewController?.top
}
if let controller = self as? UISplitViewController {
return controller.viewControllers.last?.top
}
if let controller = self as? UITabBarController {
return controller.selectedViewController?.top
}
if let controller = presentedViewController {
return controller.top
}
return self
}
}
You can use above extension below
if let rootViewController = UIApplication.top() {
//do with Active view controller
}

UINavigationController inside UITabBarController inside UISplitViewController (still) shows detail controller modally instead of pushing

I have what seems to be a very common setup in my universal application, with a root UISplitViewController, using a UITabBarController as a masterViewController, and then I want to:
either push the detail view controller onto the stack if I'm on a vertical iPhone
show the detail controller in the detailViewController of the UISplitViewController on lanscape iPhone 6+ and other larger screens like iPads and such
To that effect, I have exactly the same setup as the ones described in all those discussions that mention a similar issue:
UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone
iOS8 TabbarController inside a UISplitviewController Master
Adaptive show detail segue transformed to modal instead of push on iPhone when master view controller is a UITabBarController
But none of the solutions mentioned in those questions works. Some of them create an infinite recursive loop and an EXC_BAD_ACCESS. And the latest one I tried simply keeps presenting the detail view controller modally instead of pushing it onto the stack on iPhones. What I did is create a custom UISplitViewController subclass as such:
class RootSplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
}
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if let navController = tabController.selectedViewController as? UINavigationController {
return navController.popViewController(animated: false)
} else {
return nil
}
} else {
return nil
}
}
}
And here is the code in the master view controller to show the detail view controller:
self.performSegue(withIdentifier: "showReference", sender: ["tags": tags, "reference": reference])
Where tags and reference where loaded from Firebase. And of course the "showReference" segue is of the "Show Detail (e.g. Replace)" kind.
The first delegate method is called correctly, as evidenced by the breakpoint that gets hit there when I click an item in the list inside the UITabBarController. And yet the detail view controller still presents modally on iPhone. No problem on iPad though: the detail view controller appears on the right, as expected.
Most of the answers mentioned above are pretty old and some of the solutions are implemented in Objective-C so maybe I did something wrong in the conversion, or something changed in the UISplitViewController implementation since then.
Does anyone have any suggestion?
I figured it out. In fact, it was related to the target view controller I was trying to show. Of the 2 methods I was overriding in UISplitViewControllerDelegate, only the first one was called:
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
But the view controller I was showing in the first branch of the test was already embedded into a UINavigationController, so I was essentially showing a UINavigationController into another one, and in that case the modal made more sense. So in that case I needed to show the top view controller of the UINavigationController, which I assume was the purpose of the second method I'm overriding in the delegate, but it was never called. So I did it right there with the following implementation:
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
if let navController = vc as? UINavigationController, let actualVc = navController.topViewController {
tabController.selectedViewController?.show(actualVc, sender: sender)
navController.popViewController(animated: false)
} else {
tabController.selectedViewController?.show(vc, sender: sender)
}
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
}
And that seems to work perfectly, both on iPhones and iPads
Having you tried ShowDetailViewController Method to change the detail view controller in split view controller.
splitViewController.showDetailViewController(vc, sender: self)
If in case your view controller does not contain navigation controller you can also embed it in a navigation controller.
let nav = UINavigationController.init(rootViewController: vc)
splitViewController.showDetailViewController(nav, sender: self)

Edit status bar style embed viewController Swift

I want to edit the status bar style of my application. However, I'm unable to edit it from my main UIViewController. So I think multiple status bars settings are stacked because of embed UIViewControllers.
Here is how I initiate my navigationController in the didFinishLaunchingWithOptions method:
let navigationVC = CustomNavigationController(rootViewController: MenuInstance)
navigationVC.setNavigationBarHidden(true, animated: false)
Then, I move some views of other UIViewControllers in the menu UIViewController (MenuInstance) for any reason like this:
let scannerVC = ScannerViewController()
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(scannerVC)
scannerVC.didMove(toParentViewController: self)
}
I tried to create class to set prefersStatusBarHidden = true
class ModalViewViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
I also created a class for the UINavigationController
class CustomNavigationController: UINavigationController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
The goal of trying to remove the status bar is to find where the status bar comes from. I want only one status bar in the MenuInstance UIViewController that I can edit. The fact that some views of UIViewControllers are embed in one UIViewController makes me confused.
If the MenuInstance view controller is the root view controller of the navigation controller, then the MenuInstance's implementation of prefersStatusBarHidden is all that matters. No other view controller's preference is consulted. The "embed" stuff is irrelevant (unless you want to make it relevant).
class MenuInstance : UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
}
Note, however, that this will not work on an iPhone X. You cannot hide the status bar on an iPhone X.

Accessing the frontmost controller of an iOS App

I'm trying to access the frontmost controller of the Application during the user navigation using this code:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println(UIApplication.sharedApplication().keyWindow?.rootViewController)
}
But it seems that the rootViewController always refers to the first controller defined by the storyboard independently by when I'm accessing that property.
Is there something I'm doing wrong or I've misunderstood about the rootViewController property?
rootViewController is indeed the topmost, ultimate view controller owned by UIWindow.
To get the currently displaying view controller, you need to walk down the controller hierarchy. Here is an Objective-C category that you can add to your application, and using a bridging header you'll easily be able to call this UIWindow category from your swift code.
OK, based on the code that Michael pointed out, I wrote some Swift (1.2) code to do the same thing. You can add this as an extension to UIViewController (as I did), UIApplication, or for that matter simply make it a global function.
extension UIViewController {
static func getVisibleViewController () -> UIViewController {
let rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
return getVisibleViewControllerFrom(rootViewController!)
}
static func getVisibleViewControllerFrom(viewController: UIViewController) -> UIViewController {
let vcToReturn: UIViewController
if let navController = viewController as? UINavigationController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(navController.visibleViewController)
}
else if let tabBarController = viewController as? UITabBarController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(tabBarController.selectedViewController!)
}
else {
if let presentedViewController = viewController.presentedViewController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(presentedViewController)
}
else {
vcToReturn = viewController
}
}
return vcToReturn
}
}
You'd call this in the following way:
let visibleViewController = UIViewController.getVisibleViewController()
Hope this helps.
Andrew
PS I haven't tried this in Swift 2.0 yet, so I can't guarantee it will work without issues there. I know it won't work (as written) in Swift 1.1 or 1.0.

preferredStatusBarStyle isn't called

I followed this thread to override -preferredStatusBarStyle, but it isn't called.
Are there any options that I can change to enable it? (I'm using XIBs in my project.)
For anyone using a UINavigationController:
The UINavigationController does not forward on preferredStatusBarStyle calls to its child view controllers. Instead it manages its own state - as it should, it is drawing at the top of the screen where the status bar lives and so should be responsible for it. Therefor implementing preferredStatusBarStyle in your VCs within a nav controller will do nothing - they will never be called.
The trick is what the UINavigationController uses to decide what to return for UIStatusBarStyleDefault or UIStatusBarStyleLightContent. It bases this on its UINavigationBar.barStyle. The default (UIBarStyleDefault) results in the dark foreground UIStatusBarStyleDefault status bar. And UIBarStyleBlack will give a UIStatusBarStyleLightContent status bar.
TL;DR:
If you want UIStatusBarStyleLightContent on a UINavigationController use:
self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
Possible root cause
I had the same problem, and figured out it was happening because I wasn't setting the root view controller in my application window.
The UIViewController in which I had implemented the preferredStatusBarStyle was used in a UITabBarController, which controlled the appearance of the views on the screen.
When I set the root view controller to point to this UITabBarController, the status bar changes started to work correctly, as expected (and the preferredStatusBarStyle method was getting called).
(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
... // other view controller loading/setup code
self.window.rootViewController = rootTabBarController;
[self.window makeKeyAndVisible];
return YES;
}
Alternative method (Deprecated in iOS 9)
Alternatively, you can call one of the following methods, as appropriate, in each of your view controllers, depending on its background color, instead of having to use setNeedsStatusBarAppearanceUpdate:
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
or
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
Note that you'll also need to set UIViewControllerBasedStatusBarAppearance to NO in the plist file if you use this method.
So I actually added a category to UINavigationController but used the methods:
-(UIViewController *)childViewControllerForStatusBarStyle;
-(UIViewController *)childViewControllerForStatusBarHidden;
and had those return the current visible UIViewController. That lets the current visible view controller set its own preferred style/visibility.
Here's a complete code snippet for it:
In Swift:
extension UINavigationController {
public override func childViewControllerForStatusBarHidden() -> UIViewController? {
return self.topViewController
}
public override func childViewControllerForStatusBarStyle() -> UIViewController? {
return self.topViewController
}
}
In Objective-C:
#interface UINavigationController (StatusBarStyle)
#end
#implementation UINavigationController (StatusBarStyle)
-(UIViewController *)childViewControllerForStatusBarStyle {
return self.topViewController;
}
-(UIViewController *)childViewControllerForStatusBarHidden {
return self.topViewController;
}
#end
And for good measure, here's how it's implemented then in a UIViewController:
In Swift
override public func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
override func prefersStatusBarHidden() -> Bool {
return false
}
In Objective-C
-(UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent; // your own style
}
- (BOOL)prefersStatusBarHidden {
return NO; // your own visibility code
}
Finally, make sure your app plist does NOT have the "View controller-based status bar appearance" set to NO. Either delete that line or set it to YES (which I believe is the default now for iOS 7?)
For anyone still struggling with this, this simple extension in swift should fix the problem for you.
extension UINavigationController {
override open var childForStatusBarStyle: UIViewController? {
return self.topViewController
}
}
My app used all three: UINavigationController, UISplitViewController, UITabBarController, thus these all seem to take control over the status bar and will cause preferedStatusBarStyle to not be called for their children. To override this behavior you can create an extension like the rest of the answers have mentioned. Here is an extension for all three, in Swift 4. Wish Apple was more clear about this sort of stuff.
extension UINavigationController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return self.topViewController
}
open override var childViewControllerForStatusBarHidden: UIViewController? {
return self.topViewController
}
}
extension UITabBarController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return self.childViewControllers.first
}
open override var childViewControllerForStatusBarHidden: UIViewController? {
return self.childViewControllers.first
}
}
extension UISplitViewController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return self.childViewControllers.first
}
open override var childViewControllerForStatusBarHidden: UIViewController? {
return self.childViewControllers.first
}
}
Edit: Update for Swift 4.2 API changes
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return self.topViewController
}
open override var childForStatusBarHidden: UIViewController? {
return self.topViewController
}
}
extension UITabBarController {
open override var childForStatusBarStyle: UIViewController? {
return self.children.first
}
open override var childForStatusBarHidden: UIViewController? {
return self.children.first
}
}
extension UISplitViewController {
open override var childForStatusBarStyle: UIViewController? {
return self.children.first
}
open override var childForStatusBarHidden: UIViewController? {
return self.children.first
}
}
On a UINavigationController, preferredStatusBarStyle is not called because its topViewController is preferred to self. So, to get preferredStatusBarStyle called on an UINavigationController, you need to change its childForStatusBarStyle (Swift) / childViewControllerForStatusBarStyle (ObjC).
Recommendation
Override your UINavigationController in your class:
class MyRootNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
override var childForStatusBarStyle: UIViewController? {
return nil
}
}
Non recommended alternative
To do it for all UINavigationController, you could override in an extension (warning: it affects UIDocumentPickerViewController, UIImagePickerController, etc.), but you should probably not do it according to Swift documentation:
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
open override var childForStatusBarStyle: UIViewController? {
return nil
}
}
Tyson's answer is correct for changing the status bar color to white in UINavigationController.
If anyone want's to accomplish the same result by writing the code in AppDelegate then use below code and write it inside AppDelegate's didFinishLaunchingWithOptions method.
And don't forget to set the UIViewControllerBasedStatusBarAppearance to YES in the .plist file, else the change will not reflect.
Code
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// status bar appearance code
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
return YES;
}
In addition to serenn's answer, if you are presenting a view controller with a modalPresentationStyle (for example .overCurrentContext), you should also call this on the newly presented view controller:
presentedViewController.modalPresentationCapturesStatusBarAppearance = true
Don't forget to also override the preferredStatusBarStyle in the presented view controller.
Swift 4.2 and above
As mentioned in selected answer, root cause is to check your window root view controller object.
Possible cases of your flow structure
Custom UIViewController object is window root view controller
Your window root view controller is a UIViewController object and it further adds or removes navigation controller or tabController based on your application flow.
This kind of flow is usually used if your app has pre login flow on navigation stack without tabs and post login flow with tabs and possibly every tab further holds navigation controller.
TabBarController object is window root view controller
This is the flow where window root view controller is tabBarController possibly every tab further holds navigation controller.
NavigationController object is window root view controller
This is the flow where window root view controller is navigationController.
I am not sure if there is any possibility to add tab bar controller or new navigation controller in an existing navigation controller. But if there is such case, we need to pass the status bar style control to the next container. So, I added the same check in UINavigationController extension to find childForStatusBarStyle
Use following extensions, it handles all above scenarios -
extension UITabBarController {
open override var childForStatusBarStyle: UIViewController? {
return selectedViewController?.childForStatusBarStyle ?? selectedViewController
}
}
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return topViewController?.childForStatusBarStyle ?? topViewController
}
}
extension AppRootViewController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
return children.first { $0.childForStatusBarStyle != nil }?.childForStatusBarStyle?.preferredStatusBarStyle ?? .default
}
}
You don't need UIViewControllerBasedStatusBarAppearance key in info.plist as it true by default
Points to consider for more complex flows
In case you present new flow modally, it detaches from the existing status bar style flow. So, suppose you are presenting a NewFlowUIViewController and then add new navigation or tabBar controller to NewFlowUIViewController, then add extension of NewFlowUIViewController as well to manage further view controller's status bar style.
In case you set modalPresentationStyle other than fullScreen while presenting modally, you must set modalPresentationCapturesStatusBarAppearance to true so that presented view controller must receive status bar appearance control.
iOS 13 Solution(s)
UINavigationController is a subclass of UIViewController (who knew 🙃)!
Therefore, when presenting view controllers embedded in navigation controllers, you're not really presenting the embedded view controllers; you're presenting the navigation controllers! UINavigationController, as a subclass of UIViewController, inherits preferredStatusBarStyle and childForStatusBarStyle, which you can set as desired.
Any of the following methods should work:
Opt out of Dark Mode entirely
In your info.plist, add the following property:
Key - UIUserInterfaceStyle (aka. "User Interface Style")
Value - Light
Override preferredStatusBarStyle within UINavigationController
preferredStatusBarStyle (doc) - The preferred status bar style for the view controller
Subclass or extend UINavigationController
class MyNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
}
OR
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
}
Override childForStatusBarStyle within UINavigationController
childForStatusBarStyle (doc) - Called when the system needs the view controller to use for determining status bar style
According to Apple's documentation,
"If your container view controller derives its status bar style from one of its child view controllers, [override this property] and return that child view controller. If you return nil or do not override this method, the status bar style for self is used. If the return value from this method changes, call the setNeedsStatusBarAppearanceUpdate() method."
In other words, if you don't implement solution 3 here, the system will fall back to solution 2 above.
Subclass or extend UINavigationController
class MyNavigationController: UINavigationController {
override var childForStatusBarStyle: UIViewController? {
topViewController
}
}
OR
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
topViewController
}
}
You can return any view controller you'd like above. I recommend one of the following:
topViewController (of UINavigationController) (doc) - The view controller at the top of the navigation stack
visibleViewController (of UINavigationController) (doc) - The view controller associated with the currently visible view in the navigation interface (hint: this can include "a view controller that was presented modally on top of the navigation controller itself")
Note: If you decide to subclass UINavigationController, remember to apply that class to your nav controllers through the identity inspector in IB.
P.S. My code uses Swift 5.1 syntax 😎
An addition to Hippo's answer: if you're using a UINavigationController, then it's probably better to add a category:
// UINavigationController+StatusBarStyle.h:
#interface UINavigationController (StatusBarStyle)
#end
// UINavigationController+StatusBarStyle.m:
#implementation UINavigationController (StatusBarStyle)
- (UIStatusBarStyle)preferredStatusBarStyle
{
//also you may add any fancy condition-based code here
return UIStatusBarStyleLightContent;
}
#end
That solution is probably better than switching to soon-to-be deprecated behaviour.
#serenn's answer above is still a great one for the case of UINavigationControllers. However, for swift 3 the childViewController functions have been changed to vars. So the UINavigationController extension code should be:
override open var childViewControllerForStatusBarStyle: UIViewController? {
return topViewController
}
override open var childViewControllerForStatusBarHidden: UIViewController? {
return topViewController
}
And then in the view controller that should dictate the status bar style:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
If your viewController is under UINavigationController.
Subclass UINavigationController and add
override var preferredStatusBarStyle: UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}
ViewController's preferredStatusBarStyle will be called.
UIStatusBarStyle in iOS 7
The status bar in iOS 7 is transparent, the view behind it shows through.
The style of the status bar refers to the appearances of its content. In iOS 7, the status bar content is either dark (UIStatusBarStyleDefault) or light (UIStatusBarStyleLightContent). Both UIStatusBarStyleBlackTranslucent and UIStatusBarStyleBlackOpaque are deprecated in iOS 7.0. Use UIStatusBarStyleLightContent instead.
How to change UIStatusBarStyle
If below the status bar is a navigation bar, the status bar style will be adjusted to match the navigation bar style (UINavigationBar.barStyle):
Specifically, if the navigation bar style is UIBarStyleDefault, the status bar style will be UIStatusBarStyleDefault; if the navigation bar style is UIBarStyleBlack, the status bar style will be UIStatusBarStyleLightContent.
If there is no navigation bar below the status bar, the status bar style can be controlled and changed by an individual view controller while the app runs.
-[UIViewController preferredStatusBarStyle] is a new method added in iOS 7. It can be overridden to return the preferred status bar style:
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
If the status bar style should be controlled by a child view controller instead of self, override -[UIViewController childViewControllerForStatusBarStyle] to return that child view controller.
If you prefer to opt out of this behavior and set the status bar style by using the -[UIApplication statusBarStyle] method, add the UIViewControllerBasedStatusBarAppearance key to an app’s Info.plist file and give it the value NO.
In my case, I've accidentally presented the View/Navigation Controller as UIModalPresentationStyle.overFullScreen, which causes preferredStatusBarStyle not being called. After switching it back to UIModalPresentationStyle.fullScreen, everything works.
If anyone is using a Navigation Controller and wants all of their navigation controllers to have the black style, you can write an extension to UINavigationController like this in Swift 3 and it will apply to all navigation controllers (instead of assigning it to one controller at a time).
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
self.navigationBar.barStyle = UIBarStyle.black
}
}
As for iOS 13.4 the preferredStatusBarStyle method in UINavigationController category will not be called, swizzling seems to be the only option without the need of using a subclass.
Example:
Category header:
#interface UINavigationController (StatusBarStyle)
+ (void)setUseLightStatusBarStyle;
#end
Implementation:
#import "UINavigationController+StatusBarStyle.h"
#import <objc/runtime.h>
#implementation UINavigationController (StatusBarStyle)
void (^swizzle)(Class, SEL, SEL) = ^(Class c, SEL orig, SEL new){
Method origMethod = class_getInstanceMethod(c, orig);
Method newMethod = class_getInstanceMethod(c, new);
if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
else
method_exchangeImplementations(origMethod, newMethod);
};
+ (void)setUseLightStatusBarStyle {
swizzle(self.class, #selector(preferredStatusBarStyle), #selector(_light_preferredStatusBarStyle));
}
- (UIStatusBarStyle)_light_preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
#end
Usage in AppDelegate.h:
#import "UINavigationController+StatusBarStyle.h"
[UINavigationController setUseLightStatusBarStyle];
In Swift for any kind of UIViewController:
In your AppDelegate set:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window!.rootViewController = myRootController
return true
}
myRootController can be any kind of UIViewController, e.g. UITabBarController or UINavigationController.
Then, override this root controller like this:
class RootController: UIViewController {
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
}
This will change the appearance of the status bar in your whole app, because the root controller is solely responsible for the status bar appearance.
Remember to set the property View controller-based status bar appearance to YES in your Info.plist to make this work (which is the default).
Swift 3 iOS 10 Solution:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Most of the answers don't include good implementation of childViewControllerForStatusBarStyle method for UINavigationController. According to my experience you should handle such cases as when transparent view controller is presented over navigation controller. In these cases you should pass control to your modal controller (visibleViewController), but not when it's disappearing.
override var childViewControllerForStatusBarStyle: UIViewController? {
var childViewController = visibleViewController
if let controller = childViewController, controller.isBeingDismissed {
childViewController = topViewController
}
return childViewController?.childViewControllerForStatusBarStyle ?? childViewController
}
Here's my method for solving this.
Define a protocol called AGViewControllerAppearance.
AGViewControllerAppearance.h
#import <Foundation/Foundation.h>
#protocol AGViewControllerAppearance <NSObject>
#optional
- (BOOL)showsStatusBar;
- (BOOL)animatesStatusBarVisibility;
- (UIStatusBarStyle)preferredStatusBarStyle;
- (UIStatusBarAnimation)prefferedStatusBarAnimation;
#end
Define a category on UIViewController called Upgrade.
UIViewController+Upgrade.h
#import <UIKit/UIKit.h>
#interface UIViewController (Upgrade)
//
// Replacements
//
- (void)upgradedViewWillAppear:(BOOL)animated;
#end
UIViewController+Upgrade.m
#import "UIViewController+Upgrade.h"
#import <objc/runtime.h>
#import "AGViewControllerAppearance.h" // This is the appearance protocol
#implementation UIViewController (Upgrade)
+ (void)load
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wselector"
Method viewWillAppear = class_getInstanceMethod(self, #selector(viewWillAppear:));
#pragma clang diagnostic pop
Method upgradedViewWillAppear = class_getInstanceMethod(self, #selector(upgradedViewWillAppear:));
method_exchangeImplementations(viewWillAppear, upgradedViewWillAppear);
}
#pragma mark - Implementation
- (void)upgradedViewWillAppear:(BOOL)animated
{
//
// Call the original message (it may be a little confusing that we're
// calling the 'same' method, but we're actually calling the original one :) )
//
[self upgradedViewWillAppear:animated];
//
// Implementation
//
if ([self conformsToProtocol:#protocol(AGViewControllerAppearance)])
{
UIViewController <AGViewControllerAppearance> *viewControllerConformingToAppearance =
(UIViewController <AGViewControllerAppearance> *)self;
//
// Status bar
//
if ([viewControllerConformingToAppearance respondsToSelector:#selector(preferredStatusBarStyle)])
{
BOOL shouldAnimate = YES;
if ([viewControllerConformingToAppearance respondsToSelector:#selector(animatesStatusBarVisibility)])
{
shouldAnimate = [viewControllerConformingToAppearance animatesStatusBarVisibility];
}
[[UIApplication sharedApplication] setStatusBarStyle:[viewControllerConformingToAppearance preferredStatusBarStyle]
animated:shouldAnimate];
}
if ([viewControllerConformingToAppearance respondsToSelector:#selector(showsStatusBar)])
{
UIStatusBarAnimation animation = UIStatusBarAnimationSlide;
if ([viewControllerConformingToAppearance respondsToSelector:#selector(prefferedStatusBarAnimation)])
{
animation = [viewControllerConformingToAppearance prefferedStatusBarAnimation];
}
[[UIApplication sharedApplication] setStatusBarHidden:(! [viewControllerConformingToAppearance showsStatusBar])
withAnimation:animation];
}
}
}
#end
Now, it's time to say that you're view controller is implementing the AGViewControllerAppearance protocol.
Example:
#interface XYSampleViewController () <AGViewControllerAppearance>
... the rest of the interface
#end
Of course, you can implement the rest of the methods (showsStatusBar, animatesStatusBarVisibility, prefferedStatusBarAnimation) from the protocol and UIViewController+Upgrade will do the proper
customization based on the values provided by them.
If someone run into this problem with UISearchController.
Just create a new subclass of UISearchController, and then add code below into that class:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
Note that when using the self.navigationController.navigationBar.barStyle = UIBarStyleBlack; solution
be sure to go to your plist and set "View controller-based status bar appearance" to YES. If its NO it will not work.
Since Xcode 11.4, overriding the preferredStatusBarStyle property in a UINavigationController extension no longer works since it will not be called.
Setting the barStyle of navigationBar to .black works indeed but this will add unwanted side effects if you add subviews to the navigationBar which may have different appearances for light and dark mode. Because by setting the barStyle to black, the userInterfaceStyle of a view thats embedded in the navigationBar will then always have userInterfaceStyle.dark regardless of the userInterfaceStyle of the app.
The proper solution I come up with is by adding a subclass of UINavigationController and override preferredStatusBarStyle there. If you then use this custom UINavigationController for all your views you will be on the save side.
The NavigationController or TabBarController are the ones that need to provide the style. Here is how I solved: https://stackoverflow.com/a/39072526/242769

Resources