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
}
}
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
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
}
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
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")
}
}
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).
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.
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.
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.
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.
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.
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()
}
}
}
}
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.
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.