5

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.

0

2 Answers 2

0

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

Sign up to request clarification or add additional context in comments.

4 Comments

That's true, but from my attempts, trying to inject StatusBarTextColor into Controller like I've done for Home doesn't work. Do you know of a way to do so?
Yes, .environmentObject is a view modifier. Controller is not a View.
Does this mean there is no way to use @EnvironmentObject inside a Controller at all?
You'll have to find another way of injecting the dependency. Try overriding the initializer with the EnvironmentObject as a parameter. But, seeing as @EnvironmentObject won't do anything when it's used outside a view, you could just leave it be entirely.
0

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.