51
votes

Is there a way to change the status bar to white for a SwiftUI view?

I'm probably missing something simple, but I can't seem to find a way to change the status bar to white in SwiftUI. So far I just see .statusBar(hidden: Bool).

18
Do you mean the status bar background or the status bar text?graycampbell
Status bar text, switching to the light stylekeegan3d
Are you trying to change it for the whole app or just one view?graycampbell
Whole app would be fine, but would be nice to know how to change just one viewkeegan3d

18 Answers

36
votes

As in the comments linked to I edited this question here

But to answer this question and help people find the answer directly:

Swift 5 and SwiftUI

For SwiftUI create a new swift file called HostingController.swift

import SwiftUI

class HostingController<ContentView>: UIHostingController<ContentView> where ContentView : View {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}

Then change the following lines of code in the SceneDelegate.swift

window.rootViewController = UIHostingController(rootView: ContentView())

to

window.rootViewController = HostingController(rootView: ContentView())
32
votes

The status bar text/tint/foreground color can be set to white by setting the View's .dark or .light mode color scheme using .preferredColorScheme(_ colorScheme: ColorScheme?).

The first view in your hierarchy that uses this method will take precedence.

For example:

var body: some View {
  ZStack { ... }
  .preferredColorScheme(.dark) // white tint on status bar
}
var body: some View {
  ZStack { ... }
  .preferredColorScheme(.light) // black tint on status bar
}
20
votes

In info.plist, you can simply set

  • "Status bar style" to "Light Content"
  • "View controller-based status bar appearance" to NO

No need to change anything into your code...

15
votes

The existing answers cover the case where you want to just change the status bar color once (ex. use light content throughout your app), but if you want to do it programmatically then preference keys are a way to accomplish that.

The full example can be found below, but here is a description of what we're going to do:

  • Define a struct conforming to PreferenceKey, this will be used by Views to set their preferred status bar style
  • Create a subclass of UIHostingController that can detect preference changes and bridge them to the relevant UIKit code
  • Add an extension View to get an API that almost looks official

Preference Key Conformance

struct StatusBarStyleKey: PreferenceKey {
  static var defaultValue: UIStatusBarStyle = .default
  
  static func reduce(value: inout UIStatusBarStyle, nextValue: () -> UIStatusBarStyle) {
    value = nextValue()
  }
}

UIHostingController Subclass

class HostingController: UIHostingController<AnyView> {
  var statusBarStyle = UIStatusBarStyle.default

  //UIKit seems to observe changes on this, perhaps with KVO?
  //In any case, I found changing `statusBarStyle` was sufficient
  //and no other method calls were needed to force the status bar to update
  override var preferredStatusBarStyle: UIStatusBarStyle {
    statusBarStyle
  }

  init<T: View>(wrappedView: T) {
// This observer is necessary to break a dependency cycle - without it 
// onPreferenceChange would need to use self but self can't be used until 
// super.init is called, which can't be done until after onPreferenceChange is set up etc.
    let observer = Observer()

    let observedView = AnyView(wrappedView.onPreferenceChange(StatusBarStyleKey.self) { style in
      observer.value?.statusBarStyle = style
    })

    super.init(rootView: observedView)
    observer.value = self
  }

  private class Observer {
    weak var value: HostingController?
    init() {}
  }

  @available(*, unavailable) required init?(coder aDecoder: NSCoder) {
    // We aren't using storyboards, so this is unnecessary
    fatalError("Unavailable")
  }
}

View Extension

extension View {
  func statusBar(style: UIStatusBarStyle) -> some View {
    preference(key: StatusBarStyleKey.self, value: style)
  }
}

Usage

First, in your SceneDelegate you'll need to replace UIHostingController with your subclass:

//Previously: window.rootViewController = UIHostingController(rootView: rootView)
window.rootViewController = HostingController(wrappedView: rootView)

Any views can now use your extension to specify their preference:

VStack {
   Text("Something")
}.statusBar(style: .lightContent)

Notes

The solution of using a HostingController subclass to observe preference key changes was suggested in this answer to another question - I had previously used @EnvironmentObject which had a lot of downsides, preference keys seem much more suited to this problem.

Is this the right solution to this issue? I'm not sure. There are likely edge cases that this doesn't handle, for instance I haven't thoroughly tested to see what view gets priority if multiple views in the hierarchy specify a preference key. In my own usage, I have two mutually exclusive views that specify their preferred status bar style, so I haven't had to deal with this. So you may need to modify this to suit your needs (ex. maybe use a tuple to specify both a style and a priority, then have your HostingController check it's previous priority before overriding).

15
votes

SwiftUI 1 and 2 Only!

Create a hosting controller, DarkHostingController and set the preferredStatusBarStyle on it:

class DarkHostingController<ContentView> : UIHostingController<ContentView> where ContentView : View {
    override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
        .lightContent
    }
}

and wrap in SceneDelegate:

window.rootViewController = DarkHostingController(rootView: ContentView())
14
votes

Just add this to info.plist

<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

tested on IOS 14, xcode 12

9
votes

This solution works for apps using the new SwiftUI Lifecycle:

I needed to change the status bar text dynamically and couldn't access window.rootViewController because SceneDelegate doesn't exist for the SwiftUI Lifecycle.

I finally found this easy solution by Xavier Donnellon: https://github.com/xavierdonnellon/swiftui-statusbarstyle

Copy the StatusBarController.swift file into your project and wrap your main view into a RootView:

@main
struct ProjectApp: App {     
    var body: some Scene {
        WindowGroup {
            //wrap main view in RootView
            RootView {
                //Put the view you want your app to present here
                ContentView()
                    //add necessary environment objects here 
            }
        }
    }
}

Then you can change the status bar text color by using the .statusBarStyle(.darkContent) or .statusBarStyle(.lightContent) view modifiers, or by calling e.g. UIApplication.setStatusBarStyle(.lightContent) directly.

Don't forget to set "View controller-based status bar appearance" to "YES" in Info.plist.

6
votes

Create a new class called HostingController:

import SwiftUI

final class HostingController<T: View>: UIHostingController<T> {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        .lightContent
    }
}

In your SceneDelegate.swift, replace all occurrences of UIHostingController with HostingController.

6
votes

Update: It looks like Hannes Sverrisson's answer above is the closest, but our answers are slightly different.

The above answers with the UIHostingController subclass, as written, don't work in XCode 11.3.1.

The following did work for me, for the subclass (which handles the ContentView environment settings as well):

import SwiftUI

class HostingController<Content>: UIHostingController<Content> where Content : View {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
}

Then in SceneDelegate.swift, changing the window.rootViewController setting as such does indeed work:

window.rootViewController = HostingController(rootView: contentView)
5
votes

This is what worked for me. Add these lines to your info.plist file. You'll need to toggle the top setting (View controller-based status bar appearance) to determine what you're looking for.

enter image description here

4
votes

In the case you use environmentObject you can use the solution proposed in this answer.

Create a new file and paste the following code

import SwiftUI

class HostingController: UIHostingController<AnyView> {
   override var preferredStatusBarStyle: UIStatusBarStyle {
      return .lightContent
   }
}

The difference here is that we use AnyView instead of ContentView, which allows us to replace this:

window.rootViewController = UIHostingController(rootView:contentView.environmentObject(settings))

by this:

window.rootViewController = HostingController(rootView: AnyView(contentView.environmentObject(settings)))
4
votes

Answer from @Dan Sandland worked for me, but in my case it was required keep the interface in .light mode

ZStack {
    Rectangle()...
    
    VStack(spacing: 0) {
        ...
    }.colorScheme(.light)
}
.preferredColorScheme(.dark)
1
votes

Above solution works for the status bar style. If you want apply a background color to the status bar then you need to use a VStack that ignores top save area.

    GeometryReader{geometry in
        VStack{
            Rectangle().frame(width: geometry.size.width, height: 20, alignment: .center).foregroundColor(.red)
            Spacer()
            Your content view goes here
        }
        .frame(width: geometry.size.width, height: geometry.size.height)
    }.edgesIgnoringSafeArea(.top)

You can use actual status bar height instead of fixed 20. Please refer to the link below to get the status bar height. Status bar height in Swift

0
votes

I am using something like this

extension UIApplication {

    enum ColorMode {
        case dark, light
    }

    class func setStatusBarTextColor(_ mode: ColorMode) {
        if #available(iOS 13.0, *) {
            var style: UIUserInterfaceStyle
            switch mode {
            case .dark:
                style = .dark
            default:
                style = .light
            }
            if let window = Self.activeSceneDelegate?.window as? UIWindow {
                window.overrideUserInterfaceStyle = style
                window.setNeedsDisplay()
            }
        }
    }

    class var activeSceneDelegate: UIWindowSceneDelegate? {
        (Self.activeScene)?.delegate as? UIWindowSceneDelegate
    }
}
0
votes

Create a new swift file called HostingController.swift or just add this class on your existing swift file

class HostingController: UIHostingController<ContentView> {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .darkContent //or .lightContent

    }
}

Then change the line of code in the SceneDelegate.swift

window.rootViewController = UIHostingController(rootView: contentView)

to

window.rootViewController = HostingController(rootView: contentView)
0
votes

Arkcann's answer was great but unfortunately was not working for me because the StatusBarStyleKey.defaultValue was taking the precedence (I wonder how he managed it work). I made it Optional and override previously set value only if it was explicitly set. (I was testing on a real device on iOS 14.3)

struct StatusBarStyleKey: PreferenceKey {
  static func reduce(value: inout UIStatusBarStyle?, nextValue: () -> UIStatusBarStyle?) {
    guard let v = nextValue() else {
      return
    }
    
    value = v
  }
}

extension View {
  func statusBar(style: UIStatusBarStyle?) -> some View {
    return preference(key: StatusBarStyleKey.self, value: style)
  }
}

I also took a bit different approach in creating the HostingController, I stored the status bar style globally.

private var appStatusBarStyle: UIStatusBarStyle?

private class HostingController<ContentView: View>: UIHostingController<ContentView> {
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return appStatusBarStyle ?? .default
  }
}


func createHostingController<T: View>(rootView :T) -> UIViewController {
  let view = rootView.onPreferenceChange(StatusBarStyleKey.self) {
    appStatusBarStyle = $0
  }
  
  return HostingController(rootView: view)
}

Usage:

window.rootViewController = createHostingController(rootView: MyApp())
0
votes

Out of all the proposed solutions, the less intrusive, most straightforward, and, actually, the only working for us was the one proposed by Michał Ziobro: https://stackoverflow.com/a/60188583/944839

In our app, we need to present a screen as a sheet with a dark Status Bar. Neither of the simple solutions (like setting preferredColorScheme) did work for us. However, manually forcing the app color scheme in onAppear of the screen presented as a sheet and restoring it back in onDisappear did the trick.

Here is the complete extension code:

import SwiftUI
import UIKit

extension ColorScheme {
    var interfaceStyle: UIUserInterfaceStyle {
        switch self {
        case .dark: return .dark
        case .light: return .light
        @unknown default: return .light
        }
    }
}

extension SceneDelegate {
    static var current: Self? {
        let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        return windowScene?.delegate as? Self
    }
}

extension UIApplication {
    static func setColorScheme(_ colorScheme: ColorScheme) {
        if let window = SceneDelegate.current?.window {
            window.overrideUserInterfaceStyle = colorScheme.interfaceStyle
            window.setNeedsDisplay()
        }
    }
}

P.S. In order for the screen itself to still use light color scheme, we apply colorScheme(.light) modifier to the content of a body.

0
votes
  1. Create enum for notifications (or user any way you like):
    enum NotificationCenterEnum: String {

         case changeStatusToDark
         case changeStatusToLight
            
         var notification: Notification.Name {
               return Notification.Name(self.rawValue)
             }
          }
  1. Create custom HostingController
class HostingController<Content: View>: UIHostingController<Content>  {

    override init(rootView: Content) {
        super.init(rootView: rootView)
        
        NotificationCenter.default.addObserver(forName: NotificationCenterEnum.changeStatusToDark.notification, object: nil, queue: .main) { _ in self.statusBarEnterDarkBackground() }
  
        NotificationCenter.default.addObserver(forName: NotificationCenterEnum.changeStatusToLight.notification, object: nil, queue: .main) { _ in self.statusBarEnterLightBackground() }
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    var isDarkContent = true
    
    func statusBarEnterLightBackground() {
        isDarkContent = false
        UIView.animate(withDuration: 0.3) {[weak self] in
            self?.setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    func statusBarEnterDarkBackground() {
        isDarkContent = true
        UIView.animate(withDuration: 0.3) {[weak self] in
            self?.setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    override var preferredStatusBarStyle: UIStatusBarStyle {
        if isDarkContent {
            return .lightContent
        } else {
            return .darkContent
        }
    }
}
  1. In SceneDelegate

    window.rootViewController = HostingController(rootView: ContentView())

  2. In view you have options:

A. Use .onAppear/.onDisappear if you need this for only one view.

    .onAppear { NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToLight.notification, object: nil)
                }
    .onDisappear { NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
                }

B. If you need for multiple views to have one after another: use .onAppear like in A, but trigger changing back on backAction:

    private func backAction() {
            NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
            presentation.wrappedValue.dismiss()
        }

C. You can create modifier like so:

    struct StatusBarModifier: ViewModifier {
        
        func body(content: Content) -> some View {
            content
                .onAppear { NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToLight.notification, object: nil)
                }
                .onDisappear { NotificationCenter.default.post(name: NotificationCenterEnum.changeStatusToDark.notification, object: nil)
                }
        }
    }

and use it:

    .modifier(StatusBarModifier())