84

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).

keegan3d
  • 10,357
  • 9
  • 53
  • 77
  • 1
    Do you mean the status bar background or the status bar text? – graycampbell Jul 17 '19 at 00:44
  • 1
    Status bar text, switching to the light style – keegan3d Jul 17 '19 at 00:54
  • Are you trying to change it for the whole app or just one view? – graycampbell Jul 17 '19 at 00:55
  • 2
    Whole app would be fine, but would be nice to know how to change just one view – keegan3d Jul 17 '19 at 01:56
  • For global changes, I can use `UIApplication.shared.statusBarStyle = .lightContent` but I get a warning in Xcode that this was deprecated in iOS 13. But when I try to set this on the window scene: `windowScene.statusBarManager?.statusBarStyle = .light` I get an error that: `Cannot assign to property: 'statusBarStyle' is a get-only property` – keegan3d Jul 17 '19 at 02:33
  • 3
    See this question: https://stackoverflow.com/questions/17678881/how-to-change-status-bar-text-color-in-ios/17768797#17768797 – Vlad Lego Jul 23 '19 at 11:17
  • Perfect, the code to subclass `UIHostingController` was what I needed, thanks! – keegan3d Jul 23 '19 at 15:01
  • Possible duplicate of [How to change Status Bar text color in iOS](https://stackoverflow.com/questions/17678881/how-to-change-status-bar-text-color-in-ios) – Celina Aug 05 '19 at 09:12

22 Answers22

65

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
}
Dan Sandland
  • 7,095
  • 2
  • 29
  • 29
  • 3
    Thanks. This is the actual SwiftUI way. – Fahim Rahman Jul 12 '20 at 07:39
  • 58
    I believe this will change the color scheme entirely, not just the status bar text color. If your app uses dark theme then it's not a viable solution – Emil Aug 14 '20 at 13:03
  • 4
    Good answer, but it can lead to more hassle because this will also change color schemes of Lists and other objects inside that Stack. Changing only the UIStatusBar will avoid this and be less code technically. – DaWiseguy Aug 20 '20 at 23:53
38

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())
Hai Feng Kao
  • 5,219
  • 2
  • 27
  • 38
krjw
  • 4,070
  • 1
  • 24
  • 49
30

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...

Mete
  • 5,495
  • 4
  • 32
  • 40
Muvimotv
  • 853
  • 9
  • 14
  • 6
    Doesn't seem to do anything with a SwiftUI app. – boxed Jul 06 '20 at 05:59
  • 1
    @boxed make sure to set "UIViewControllerBasedStatusBarAppearance" to "NO" as well, otherwise SwiftUI may override. – Matt Gallagher Feb 11 '21 at 04:19
  • This is not a solution when you have to choose a status bar style depending on a n UIViewController. – adnako Jun 24 '23 at 15:26
  • @adnako, this is answering the original question. If you are trying to achieve something different than clearly the answer would be different :) – Muvimotv Jun 26 '23 at 04:47
  • This is only a part of the right answer. Do not confuse devs who need to manage status bar color by an UIVC, but not by the app. – adnako Jun 26 '23 at 06:06
  • @adnako "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)." This is the question that was asked the answer is for that question... You are talking about a different use case... – Muvimotv Jun 27 '23 at 17:27
  • @Muvimotv, Can you point me on something in the original question related to "Change the status bar color of the whole app at once". – adnako Jun 28 '23 at 14:55
28

Just add this to info.plist

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

tested on IOS 14, xcode 12

alexsodev
  • 489
  • 5
  • 12
  • Here's a video showing how to update the info.plist https://youtu.be/w-I5I8POMSI – Richard H Apr 02 '21 at 03:20
  • If you use *xcodegen* to generate _info.plist_ file, then you can put the following lines: `properties: UIStatusBarStyle: UIStatusBarStyleLightContent UIViewControllerBasedStatusBarAppearance: false` – Reza Dehnavi May 23 '22 at 22:49
  • This is not a solution when you have to choose a status bar style depending on a n UIViewController. – adnako Jun 24 '23 at 15:26
  • This is just a duplicate of what I posted presented differently :P – Muvimotv Jun 26 '23 at 04:44
25

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.

heiko
  • 422
  • 7
  • 7
  • 6
    This is the only thing that worked for me on a SwiftUI Lifecycle app. Thanks for sharing! – Ruben Martinez Jr. Mar 22 '21 at 14:26
  • 4
    Caveat: This breaks `onOpenURL` in iOS15. – Nicolas Sep 30 '21 at 11:24
  • For me, I didn't need to set "View controller-based status bar appearance" to "YES" in Info.plist. – wristbands Nov 30 '21 at 02:44
  • 2
    I am getting the error `Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x14cf0e3a0>.` in the console when running my app with this solution. Anyone know how to resolve this? – wristbands Dec 30 '21 at 00:04
  • This works great for my iOS 14 devices, but in iOS 15 it doesn't apply the status bar changes to views inside a fullScreenCover. Instead, it goes back to the default status bar style for light mode or dark mode once inside the fullScreenCover, and only returns to using my selected style once the fullScreenCover is dismissed. – wristbands Feb 24 '22 at 22:23
  • What was breaking the onOpenURL in iOS 15 I am currently seeing my onOpenURL not being called in iOS 15 – Bodlund Apr 14 '22 at 10:38
  • By the way, @wristbands, were you able to fix the warning "Unbalanced calls to begin/end appearance transitions for..."? – Tomas Feb 24 '23 at 20:43
  • Thank you for posting this solution. I noticed that if I want the specific color to work for one view only (and leave the rest as they are), I have to set those other views that are connected to the first one with .statusBarStyle(.default) – Tomas Feb 24 '23 at 20:49
  • On iOS16, I am getting the error `Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x14cf0e3a0>`. in the console when running my app. Anyone know how to resolve this? – KaayZenn Jun 17 '23 at 06:03
  • For iOS 16 ///Sets the App to start at rootView func setHostingController(rootView: AnyView) { let hostingController = HostingController(rootView: AnyView(rootView)) if let windowScene = connectedScenes.first as? UIWindowScene { if let firstWindow = windowScene.windows.first { firstWindow.rootViewController = hostingController UIApplication.hostingController = hostingController } } } – mystride Aug 08 '23 at 01:27
24

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).

Arkcann
  • 618
  • 7
  • 15
  • This solution is most valuable when you have a different style of bar status on each next screen. – A.Kant Dec 27 '20 at 16:04
  • You need to tell appkit the value has changed `let observedView = AnyView(rootView.onPreferenceChange(StatusBarStyleKey.self) { style in observer.value?.statusBarStyle = style observer.value?.setNeedsStatusBarAppearanceUpdate() })` – Klajd Deda Dec 29 '20 at 19:23
  • @KlajdDeda in my experiments that wasn't needed, I think AppKit is using some KVO to determine a change has been made. That said, I have since found some limitations with the approach described here - specifically preferences in SwiftUI are applied from shallowest to deepest view, with the preferences of shallower views taking higher priority. So if your root view prefers light content, and a deeper view prefers dark content, the root view will "win". I don't know of a good way to work around this. – Arkcann Dec 30 '20 at 15:21
  • @Arkcann Thanks your answer helped me. I think you can define custom logic in `PreferenceKey.reduce` method to make it how ever you want. You can append values to an array and use only first item (or last) if you want to deeper values to take precedence. – Orkhan Alikhanov Jan 22 '21 at 16:50
  • 5
    This works for apps still using the UIKit app lifecycle. Do you have a solution for apps using the new SwiftUI App main? – Edward Mar 15 '21 at 14:16
  • @Edward unfortunately I do not - I'm hopeful that someone else is able to provide such a solution, or that Apple eventually does. As it stands, this is preventing me from adopting the new SwiftUI lifecycle. – Arkcann Mar 16 '21 at 15:13
  • You can create a `SceneDelegate` and `AppDelegate` and drop the `AppDelegate` into your `App` and wire the `SceneDelegate` into the `AppDelegate`. This has worked for other issues. I need to toggle the status bar text color on-the-fly within a single view, so I will be trying this. – David Reich Oct 05 '21 at 02:32
  • I am using above code and in my case I am wrapping ZStack inside NavigationView and it is not working for me, If I remove naviagationView then it is working as expected but I need keep navigationview Any idea why this is happening? – Ashwini Salunkhe Apr 28 '23 at 11:26
16

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())
Sverrisson
  • 17,970
  • 5
  • 66
  • 62
  • 2
    This works for changing the text color from black to white but how can I change the background color of the status bar? – MobileMon Aug 27 '19 at 18:23
  • 4
    @MobileMon you use .edgesIgnoringSafeArea(.top) to allow your background to go all the way up to the top and you can set that color. You put that directly on the View. – Sverrisson Aug 27 '19 at 18:59
  • What is "Model()"? You don't have it defined. – Richard Witherspoon Nov 19 '19 at 03:12
  • @RichardWitherspoon Model is my data model object and you don't need it for this solution. I will delete it, to prevent confusion. – Sverrisson Nov 19 '19 at 17:32
  • I like the use of generics to disassociate the Controller from a specific type (unlike it is done in the most voted answer), however, what would be the point of using '@objc' and 'dynamic open' in this specific use case? – Repose Jan 16 '20 at 17:04
14

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)
Ned
  • 1,378
  • 16
  • 28
9

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

Justin A
  • 3,891
  • 2
  • 18
  • 22
  • 1
    Wow, this actually worked and is less code and works with the new SwiftUI life cycle. This is the way! – Burgler-dev Feb 20 '21 at 09:39
  • This is not a solution when you have to choose a status bar style depending on a n UIViewController. – adnako Jun 24 '23 at 15:46
6

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.

Ken Mueller
  • 3,659
  • 3
  • 21
  • 33
6

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)
Justin N
  • 489
  • 3
  • 8
4

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)))
GRosay
  • 444
  • 10
  • 26
2

Both static (only works for projects using the old UIWindowSceneDelegate life cycle) and dynamic replacement of the key window's UIHostingController has undesirably side effects (e.g. onOpenURL breaking).

Here's a different approach that involves swizzling preferredStatusBarStyle to point to a computed variable.

extension UIViewController {
    fileprivate enum Holder {
        static var statusBarStyleStack: [UIStatusBarStyle] = .init()
    }

    fileprivate func interpose() -> Bool {
        let sel1: Selector = #selector(
            getter: preferredStatusBarStyle
        )
        let sel2: Selector = #selector(
            getter: preferredStatusBarStyleModified
        )

        let original = class_getInstanceMethod(Self.self, sel1)
        let new = class_getInstanceMethod(Self.self, sel2)

        if let original = original, let new = new {
            method_exchangeImplementations(original, new)

            return true
        }

        return false
    }

    @objc dynamic var preferredStatusBarStyleModified: UIStatusBarStyle {
        Holder.statusBarStyleStack.last ?? .default
    }
}

With some additional scaffolding this can be used to implement a .statusBarStyle view modifier.

enum Interposed {
    case pending
    case successful
    case failed
}

struct InterposedKey: EnvironmentKey {
    static let defaultValue: Interposed = .pending
}

extension EnvironmentValues {
    fileprivate(set) var interposed: Interposed {
        get { self[InterposedKey.self] }
        set { self[InterposedKey.self] = newValue }
    }
}

/// `UIApplication.keyWindow` is deprecated
extension UIApplication {
    var keyWindow: UIWindow? {
        connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap(\.windows)
            .first {
                $0.isKeyWindow
            }
    }
}

extension UIViewController {
    fileprivate enum Holder {
        static var statusBarStyleStack: [UIStatusBarStyle] = .init()
    }

    fileprivate func interpose() -> Bool {
        let sel1: Selector = #selector(
            getter: preferredStatusBarStyle
        )
        let sel2: Selector = #selector(
            getter: preferredStatusBarStyleModified
        )

        let original = class_getInstanceMethod(Self.self, sel1)
        let new = class_getInstanceMethod(Self.self, sel2)

        if let original = original, let new = new {
            method_exchangeImplementations(original, new)

            return true
        }

        return false
    }

    @objc dynamic var preferredStatusBarStyleModified: UIStatusBarStyle {
        Holder.statusBarStyleStack.last ?? .default
    }
}

struct StatusBarStyle: ViewModifier {
    @Environment(\.interposed) private var interposed

    let statusBarStyle: UIStatusBarStyle
    let animationDuration: TimeInterval

    private func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle) {
        UIViewController.Holder.statusBarStyleStack.append(statusBarStyle)

        UIView.animate(withDuration: animationDuration) {
            UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
        }
    }

    func body(content: Content) -> some View {
        content
            .onAppear {
                setStatusBarStyle(statusBarStyle)
            }
            .onChange(of: statusBarStyle) {
                setStatusBarStyle($0)
                UIViewController.Holder.statusBarStyleStack.removeFirst(1)
            }
            .onDisappear {
                UIViewController.Holder.statusBarStyleStack.removeFirst(1)

                UIView.animate(withDuration: animationDuration) {
                    UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
                }
            }
            // Interposing might still be pending on initial render
            .onChange(of: interposed) { _ in
                UIView.animate(withDuration: animationDuration) {
                    UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate()
                }
            }
    }
}

extension View {
    func statusBarStyle(
        _ statusBarStyle: UIStatusBarStyle,
        animationDuration: TimeInterval = 0.3
    ) -> some View {
        modifier(StatusBarStyle(statusBarStyle: statusBarStyle, animationDuration: animationDuration))
    }
}


@main
struct YourApp: App {
    @Environment(\.scenePhase) private var scenePhase

    /// Ensures that interposing only occurs once
    private var interposeLock = NSLock()

    @State private var interposed: Interposed = .pending

    var body: some Scene {
        WindowGroup {
            VStack {
                Text("Hello, world!")
                    .padding()
            }
            .statusBarStyle(.lightContent)
            .environment(\.interposed, interposed)
        }
        .onChange(of: scenePhase) { phase in
            /// `keyWindow` isn't set before first `scenePhase` transition
            if case .active = phase {
                interposeLock.lock()
                if case .pending = interposed,
                   case true = UIApplication.shared.keyWindow?.rootViewController?.interpose() {
                    interposed = .successful
                } else {
                    interposed = .failed
                }
                interposeLock.unlock()
            }
        }
    }
}

Some additional context.

  • Pretty interesting answer. It's strange that you're removing the first item from the stack when the view disappears - it should be the last one, as it's a stack. An other problem is that with transition `onDisappear` is getting called only after transition changes. It causes two problems. The first one is that animation starts after transition, which is not perfect. The second one is more problematic - if both screens have `statusBarStyle` modifier, new value will be added into the stack before removing the disappearing view style, and so it'll be removed after transition end. – Phil Dukhov Apr 14 '22 at 18:14
2

Here is an answer that I made for projects with the new SwiftUI lifecycle.

This solution allows for dynamic statusbar color changing, doesn't break onOpenURL, and also works with sheets.

Inspired from this article by Barstool Engineering

If you want a gist, it's located here

First, create an ObservableObject (that subclasses UIViewController) for a new ViewController. This will eventually override the app's existing RootViewController. I'll call this HostingViewController (Like the article).

class HostingViewController: UIViewController, ObservableObject {
    // The main controller to customize
    var rootViewController: UIViewController?

    // The statusbar style, updates on change
    var style: UIStatusBarStyle = .lightContent {
        didSet {
            // Can remove the animation block
            UIView.animate(withDuration: 0.3) {
                 self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
            }
        }
    }

    // If the statusbar is hidden. Subclassing breaks SwiftUI's statusbar modifier, so handle hiding here
    var isHidden: Bool = false {
        didSet {
            // Can remove the animation block
            UIView.animate(withDuration: 0.3) {
                self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
            }
        }
    }

    // Ignore dark mode color inversion
    var ignoreDarkMode: Bool = false

    init(rootViewController: UIViewController?, style: UIStatusBarStyle, ignoreDarkMode: Bool = false) {
        self.rootViewController = rootViewController
        self.style = style
        self.ignoreDarkMode = ignoreDarkMode
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        guard let child = rootViewController else { return }
        addChild(child)
        view.addSubview(child.view)
        child.didMove(toParent: self)
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        if ignoreDarkMode || traitCollection.userInterfaceStyle == .light {
            return style
        } else {
            if style == .darkContent {
                return .lightContent
            } else {
                return .darkContent
            }
        }
    }

    override var prefersStatusBarHidden: Bool {
        return isHidden
    }

    // Can change this to whatever animation you want
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return .fade
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        setNeedsStatusBarAppearanceUpdate()
    }
}

Now, you can use any method to grab the UIWindow's rootViewController, but I like using SwiftUI-Introspect since it's easy to get started with.

Here's the ContentView implementing this HostingController. Since the rootViewController is being overriden, the statusBar SwiftUI modifiers will no longer work (hence the isHidden variable in the HostingViewController).

The best way to show the statusbar color in the View is to simply make the ContentView into a ZStack with a color that ignores safe area as the farthest layer back.

import SwiftUI
import Introspect

struct ContentView: View {
    @StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)

    @State var bgColor: Color = .yellow
    @State var showSheet: Bool = false
    
    var body: some View {
        ZStack {
            bgColor
                .ignoresSafeArea()

            VStack(spacing: 30) {
                Button("Light color") {
                    bgColor = .yellow
                }

                Button("Dark color") {
                    bgColor = .black
                }
            }
        }
        // You can use any way to grab the rootViewController, but I want to use Introspect
        .introspectViewController { viewController in
            // Grab the root view controller from the UIWindow and set that to the hosting controller
            let window = viewController.view.window
            guard let rootViewController = window?.rootViewController else { return }
            hostingViewController.rootViewController = rootViewController

            // Ignore system dark mode color inversion
            hostingViewController.ignoreDarkMode = true

            // Hide the statusbar. Overriding the hosting controller disables the statusbar view modifier
            hostingViewController.isHidden = false

            // Set the window's root view controller to the hosting controller subclass
            window?.rootViewController = hostingViewController
        }
        .onChange(of: bgColor) { newColor in
            // darkContent is used for light backgrounds and vice versa
            if newColor.isLight {
                hostingViewController.style = .darkContent
            } else {
                hostingViewController.style = .lightContent
            }
        }
    }
}

I hope this helps someone out there struggling with this issues like I did.

code24
  • 458
  • 1
  • 3
  • 14
1

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())
Orkhan Alikhanov
  • 9,122
  • 3
  • 39
  • 60
0

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.

Serzhas
  • 854
  • 12
  • 23
0
  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())
VadimKat
  • 15
  • 5
0

Lots of answers here already. But here's one more for those seeking a view-specific solution that doesn't involve UIKit/UIHostingController.

This will behave exactly as preferredColorScheme(…) but only for the calling view and its children, not parent views. Rationale at the bottom.

/// A modifier which sets an explicit color scheme for this view that is removed
/// when the view disappears.
struct IsolatedColorSchemeModifier: ViewModifier {
  /// The desired color scheme for the view.
  let colorScheme: ColorScheme
  /// The currently active color scheme.
  @State private var activeColorScheme: ColorScheme?

  func body(content: Content) -> some View {
    content
      .preferredColorScheme(activeColorScheme)
      .onAppear {
        activeColorScheme = colorScheme
      }
      .onDisappear {
        activeColorScheme = .none
      }
  }
}

extension View {
  /// Sets an explicit color scheme for this view and all child views. The color 
  /// scheme will be removed when the view disappears.
  func isolatedColorScheme(_ colorScheme: ColorScheme) -> some View {
    modifier(IsolatedColorSchemeModifier(colorScheme: colorScheme))
  }
}

Since Apple has yet to provide a view modifier which sets the status bar style directly, my guess is that they want developers to prefer designing dark/light adaptive content rather than giving them an easy escape hatch. Fair enough. I'll admit when I've stopped halfway with my design and should revisit it in the future.

Until then, best to keep the engineering effort as low as possible for the workaround, which was the goal of this solution.

Nathan Hosselton
  • 1,089
  • 1
  • 12
  • 16
0

Use the .toolbarColorScheme(.dark, for: .navigationBar) view modifier in your navigation view.

Dinu
  • 49
  • 3
-1

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
    }
}
DZoki019
  • 382
  • 2
  • 13
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
-1

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

Moin Uddin
  • 349
  • 5
  • 16
-3

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)
Fahim Rahman
  • 247
  • 3
  • 13