3
votes

I'm looking for a way to change the text color of the status bar that allows a different text color to be used for each view.

I've seen this Q&A, but it's not what I'm looking for. I'm not looking for solutions that only allow for one status bar text color for all views. I want to change the status bar text color for each view. For example, one view might have a dark background and so I need light text. I might navigate to another view with a light background, so now I need dark text. The suggested duplicate answer only returns .lightContent, which means that the status bar text color cannot change dynamically when I move to a different view.

This answer here works on my machine, but it's not performant. A comment under it corroborates this. The lag is unacceptable, so this solution is not good.

Other solutions I've seen so far cause this particular error:

Compiling failed: extensions of generic classes cannot contain '@objc' members

I've also tried using an Environment Object inside my Custom Controller:

import SwiftUI

/// Allows for the status bar colors to be changed from black to white on the dark gray title bar
class Controller<ContentView> : UIHostingController<ContentView> where ContentView : View {
    @EnvironmentObject var statusBarTextColor: StatusBarTextColor

    lazy var isDark: Bool = self.statusBarTextColor.isDark

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return isDark ? .lightContent : .darkContent
    }
}

This results in the error:

Thread 1: Fatal error: No ObservableObject of type StatusBarTextColor found. A View.environmentObject(_:) for StatusBarTextColor may be missing as an ancestor of this view.

Inside my SceneDelegate file, I do specify the StatusBarTextColor environmentObject:

            window.rootViewController = Controller(
                rootView: Home()
                    .environmentObject(PostData())
                    .environmentObject(CardPosition())
                    .environmentObject(StatusBarTextColor())
            )

And this is the ObservableObject itself:

import Combine
import SwiftUI

final class StatusBarTextColor: ObservableObject {
    @Published var isDark: Bool = true
}

If I were to guess why this doesn't work, I'd say it's because the Controller gets initialized before StatusBarTextColor is available.

The more I look into this problem, the more I think there isn't a solution. I've gone through just about every article, answer, and video on the subject. They all either use a Controller to only return .lightContent, or use storyboards and multiple controllers, which isn't what I'm using.

2

2 Answers

1
votes

You can use the solution you found here, but instead of using onDisappear, which will have a delay for the color change until the view is completely gone, you can create a view modifier called onWillDisappear that exposes viewWillDisappear. The color change will happen as sooner.

Usage:

struct MyClass: View {
      @Environment(\.localStatusBarStyle) var statusBarStyle
    // ...
        SomeView()
        }.onAppear {
            self.statusBarStyle.currentStyle = .darkContent
        }
        .onWillDisappear {
            self.statusBarStyle.currentStyle = .lightContent
        }
}

Code:

   import SwiftUI
    
    
    class HostingController<Content>: UIHostingController<Content> where Content: View {
        private var internalStyle = UIStatusBarStyle.lightContent
    
        @objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
            get {
                internalStyle
            }
            set {
                internalStyle = newValue
                self.setNeedsStatusBarAppearanceUpdate()
            }
        }
    
        override init(rootView: Content) {
            super.init(rootView:rootView)
    
            LocalStatusBarStyleKey.defaultValue.getter = { self.preferredStatusBarStyle }
            LocalStatusBarStyleKey.defaultValue.setter = { self.preferredStatusBarStyle = $0 }
        }
    
        @objc required dynamic init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    }
    
    class LocalStatusBarStyle { // style proxy to be stored in Environment
        fileprivate var getter: () -> UIStatusBarStyle = { .default }
        fileprivate var setter: (UIStatusBarStyle) -> Void = {_ in}
    
        var currentStyle: UIStatusBarStyle {
            get { self.getter() }
            set { self.setter(newValue) }
        }
    }
    
    // Custom Environment key, as it is set once, it can be accessed from anywhere
    // of SwiftUI view hierarchy

struct LocalStatusBarStyleKey: EnvironmentKey {
    static let defaultValue: LocalStatusBarStyle = LocalStatusBarStyle()
}

extension EnvironmentValues { // Environment key path variable
    var localStatusBarStyle: LocalStatusBarStyle {
        get {
            return self[LocalStatusBarStyleKey.self]
        }
    }
}

struct WillDisappearHandler: UIViewControllerRepresentable {
    func makeCoordinator() -> WillDisappearHandler.Coordinator {
        Coordinator(onWillDisappear: onWillDisappear)
    }

    let onWillDisappear: () -> Void

    func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
        context.coordinator
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
    }

    typealias UIViewControllerType = UIViewController

class Coordinator: UIViewController {
        let onWillDisappear: () -> Void

        init(onWillDisappear: @escaping () -> Void) {
            self.onWillDisappear = onWillDisappear
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            onWillDisappear()
        }
    }
}
struct WillDisappearModifier: ViewModifier {
    let callback: () -> Void

    func body(content: Content) -> some View {
        content
            .background(WillDisappearHandler(onWillDisappear: callback))
    }
}

extension View {
    func onWillDisappear(_ perform: @escaping () -> Void) -> some View {
        self.modifier(WillDisappearModifier(callback: perform))
    }
}

See original post with onWillDisappear code here

0
votes

In your SceneDelegate you inject StatusBarTextColor() to the Home view. However, you declared the EvironmentObject in Controller.