How to change the background color of the status bar in SwiftUI when creating a custom NavigationController View

I wanted to experiment a little using UINavigationController with SwiftUI App Lifecycle

I created a Custom Navigation Controller named JDNavigationController. It does not do much other than having the @Observable macro

public struct NavigationControllerStack<Content: View>: UIViewControllerRepresentable {
    private let initialView: () -> Content

    @State private var controller = JDNavigationController()

    public init(content: @escaping () -> Content) {
        self.initialView = content
    }

    public func makeUIViewController(context: Context) -> JDNavigationController {
        let viewController = self.initialView()
            .environment(self.controller)
            .viewController

        self.controller.viewControllers = [
            viewController
        ]

        return controller
    }
    
    public func updateUIViewController(_ uiViewController: JDNavigationController, context: Context) {

    }

    public typealias UIViewControllerType = JDNavigationController
}

when I check, the functionality of the navigation controller works as expected, however, I can't for the life of me to change the background color of the status bar.

#Preview {
    NavigationControllerStack {
        ZStack {
            Color.purple
                .ignoresSafeArea(.all)

            VStack {
                Text("somethign")
            }
        }
    }
}

I have tried

  • Using the appearance functions

  • Creating a Custom View Controller, and in that view controller I tried using a Hosting Controller by adding the hosting controller as a child and

setting hostingController.view.insetsLayoutMarginsFromSafeArea = false

  • using the toolbar modifiers from SwiftUI View

none seems to work.

I opened the View Debugger it looks like the white portion comes from the UIHosting Controller but I am not sure what I can do to fix it

Answered by DTS Engineer in 797200022

@lukdor TN3105: Customizing the UIKit status bar style covers the UIKit Component side of this.

In SwiftUI, you could change the background color using toolbarColorScheme(_:for:) and toolbarBackground(_:for:)

Give us the full code to reproduce this.

I wrote an extension to the View to convert it to BiewController

extension View {
    public var viewController: UIViewController {
        UIHostingController(rootView: self)
    }
}

the following is my JDNavigastionController

@Observable
public final class JDNavigationController: UINavigationController {
    public func pushView<V: View>(
        _ view: @autoclosure () -> V,
        animated: Bool = true
    ) {
        self.pushViewController(
            view()
                .environment(self)
                .viewController,
            animated: animated
        )
    }

    public func presentView<V: View>(
        _ view: @autoclosure () -> V,
        animated: Bool = true,
        completion: (() -> Void)? = nil
    ) {
        self.present(
            view()
                .environment(self)
                .viewController,
            animated: animated,
            completion: completion
        )
    }

    public func alert(
        title: String? = nil,
        messsage: String? = nil,
        actions: [UIAlertAction] = [],
        animated: Bool = true,
        completion: (() -> Void)? = nil,
        preferredStyle: UIAlertController.Style = .alert
    ) {
        let alertController = UIAlertController(title: title, message: messsage, preferredStyle: preferredStyle)

        actions.forEach {
            alertController.addAction($0)
        }

        self.present(alertController, animated: animated, completion: completion)
    }
}

@lukdor TN3105: Customizing the UIKit status bar style covers the UIKit Component side of this.

In SwiftUI, you could change the background color using toolbarColorScheme(_:for:) and toolbarBackground(_:for:)

Hello, I have made attempts, but unfortunately there were not fruitful.

The setup is as follows, note that I went overboard with the toolbar modifiers just to be sure.

import SwiftData
import SwiftUI

@main
struct NewNavigationApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationControllerView {
                ContentView()
            }
        }
    }
}

@Observable
final class NavigationController: UINavigationController {
    func pushView<Content: View>(_ view: @escaping () -> Content) {
        let content = view()
            .environment(self)

        let controller = UIHostingController(rootView: content)

        pushViewController(controller, animated: true)
    }
}

struct NavigationControllerView<RootView: View>: UIViewControllerRepresentable {
    @State private var navigationController = NavigationController()

    private let rootView: () -> RootView

    init(rootView: @escaping () -> RootView) {
        self.rootView = rootView
    }

    func makeUIViewController(context: Context) -> NavigationController {
        let view = rootView()
            .environment(navigationController)

        navigationController.viewControllers = [
            UIHostingController(rootView: view)
        ]

        return navigationController
    }
    
    func updateUIViewController(_ uiViewController: NavigationController, context: Context) { }

    typealias UIViewControllerType = NavigationController
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue
                .ignoresSafeArea(.all)
                .toolbarBackground(.purple, for: .navigationBar)
                .toolbarColorScheme(.dark, for: .navigationBar)
        }
        .toolbarBackground(.purple, for: .navigationBar)
        .toolbarColorScheme(.dark, for: .navigationBar)
    }
}

#Preview {
    NavigationControllerView {
        ContentView()
            .toolbarBackground(.purple, for: .navigationBar)
            .toolbarColorScheme(.dark, for: .navigationBar)
    }
    .toolbarBackground(.purple, for: .navigationBar)
    .toolbarColorScheme(.dark, for: .navigationBar)
}

However, the result was the same.

Next, I updated my info.plist though that did not change anything.

Afterwards, I attempted to create my own custom ViewController.

final class SwiftUIViewController<Content: View>: UIHostingController<Content> {
    override var preferredStatusBarStyle: UIStatusBarStyle {
        .darkContent
    }

    override init(rootView: Content) {
        super.init(rootView: rootView)
        view.backgroundColor = .purple
    }
    
    @MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

and I have made the following modification inside struct NavigationControllerView<RootView: View>: UIViewControllerRepresentable

func makeUIViewController(context: Context) -> NavigationController {
    let view = rootView()
        .environment(navigationController)
    navigationController.viewControllers = [
        SwiftUIViewController(rootView: view)
    ]
    return navigationController
}

so far, nothing has worked. I am aware this is an unorthodox way of using SwiftUI, I was playing around to see if I can use a SwiftUI App Lifecycle with Imperative navigation like navigation controllers, and adding the navigator to the view environment.

When I am using a UIKit based app lifecycle with app and scene delegates with the following code

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(windowScene: windowScene)
        let navigationController = NavigationController()
        let initialView = ContentView()
            .environment(navigationController)
            .viewController

        navigationController.viewControllers = [initialView]

        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
    }

It appears to be working well.

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.purple
                .ignoresSafeArea(.all)
        }
        .toolbar {
            Button("Toolbar Button") { }
                .buttonStyle(.plain)
        }
    }
}

I am wondering what am I missing when I am trying to achieve this using a SwiftUI based app lifecycle.

Accepted Answer

I was playing around more this morning The solution was very simple I don't know how I did not think of it.

#Preview {
    NavigationControllerView {
        ContentView()
    }
    .ignoresSafeArea() // <--- here
}
How to change the background color of the status bar in SwiftUI when creating a custom NavigationController View
 
 
Q