-2

I am trying to set the status bar style within the latest SwiftUI Lifecycle. Here's my code that performs the logic to do this, adapted from here.

private class HostingController<Content: View>: UIHostingController<Content> {
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return UIApplication.statusBarStyleHierarchy.last ?? UIApplication.defaultStatusBarStyle
  }
}

/// By wrapping views in a StatusBarControllerView, they will become the app's main / primary view.
/// This will enable setting the statusBarStyle.
struct StatusBarControllerView<Content: View>: View {
  var content: Content

  init(@ViewBuilder content: () -> (Content)) {
    self.content = content()
  }

  var body:some View {
    EmptyView()
      .onAppear {
        UIApplication.shared.setHostingController(rootView: AnyView(content))
      }
  }
}

extension View {
  /// Sets the status bar style color for this view.
  func statusBar(style: UIStatusBarStyle) -> some View {
    UIApplication.statusBarStyleHierarchy.append(style)

    return self
      // Once this view appears, set the style to the new style.
      .onAppear {
        UIApplication.hostingController?.setNeedsStatusBarAppearanceUpdate()
      }
      // Once it disappears, set it to the previous style.
      .onDisappear {
        UIApplication.statusBarStyleHierarchy.removeLast()
        UIApplication.hostingController?.setNeedsStatusBarAppearanceUpdate()
    }
  }
}

private extension UIApplication {
  static var hostingController: HostingController<AnyView>?

  static var statusBarStyleHierarchy: [UIStatusBarStyle] = []
  static let defaultStatusBarStyle: UIStatusBarStyle = .lightContent

  /// Sets the App to start at rootView
  func setHostingController(rootView: AnyView) {
    let hostingController = HostingController(rootView: AnyView(rootView))
    windows.first?.rootViewController = hostingController
    UIApplication.hostingController = hostingController
  }
}

I then set up the StatusBarControllerView by placing it in my root App, as shown below:

@main
struct App: SwiftUI.App {
  var body: some Scene {
    WindowGroup {
      StatusBarControllerView {
        Text("Content goes here")
      }
    }
  }
}

While it's working great visually, I am now getting the following error in the console:

Unbalanced calls to begin/end appearance transitions for <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x13ee0d9d0>.

Does anyone know how to modify the provided code to avoid this error message? The problem appears to occur when setting the root view controller in private extension UIApplication: windows.first?.rootViewController = hostingController

Clarification 1/3/2021

The reason I am trying to change the status bar color this way instead of by editing the plist is because I would like to be able to change the status bar color dynamically in any view of my app, rather than app-wide, as shown below:

struct MyView: View {
  var body: some View {
    MySubview()
      .statusBar(style: .darkContent)
  }
}
wristbands
  • 1,021
  • 11
  • 22
  • Welcome to SO - Please take the [tour](https://stackoverflow.com/tour) and read [How to Ask](https://stackoverflow.com/help/how-to-ask) to improve, edit and format your questions. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. – lorem ipsum Dec 30 '21 at 12:54
  • I have now edited my question in response to the 3 dislikes. Specifically, I added my code to the question itself (instead of linking it), as well as improved the title by turning it into a question. If there are still aspects you dislike about my question, please comment here so that I can make the improvements you desire. – wristbands Jan 03 '22 at 17:52

1 Answers1

1

Before diving into the weeds, I’ve seen some answers that lead me to believe this isn’t necessary, and can be solved by changing your plist: here and here.

Assuming you do want a hosting controller, I believe the issue is that you are setting the view two different ways. One is the empty view that’s being called from the app body (through StatusBarControllerView), and the other is the hosting UIView that is being assigned control through the window rootViewController. This can be demonstrated by changing out EmptyView() with some visible view; it doesn’t appear, because the HostingController is replacing it. Swift is able to replace it, but throws the error that you’re seeing to let you know that this is happening. If you do want the HostingController to be the main view, you could set it directly by changing StatusBarControllerView to a UIViewRepresentable:

struct StatusBarControllerView<Content: View>: UIViewRepresentable {
    var content: () -> (Content)
    
    func makeUIView(context: Context) -> some UIView {
        UIApplication.shared.setHostingController(rootView: AnyView(content()))
        return UIApplication.hostingController?.view ?? UIView()
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

And removing the line windows.first?.rootViewController = hostingController so that it’s only set once.

You could also set the HostingController up in the scene delegate, based on this answer. However, that would require adding a scene delegate and app delegate to your project (here’s a decent tutorial on that).

Chris McElroy
  • 161
  • 11
  • I much appreciate your researched and thorough answer! However, while it did resolve the error, it now does not update the status bar style, and also my navigation view color no longer extends into the status bar region. The reason the status bar no longer changes might be because I'm only taking the *view* from the hostingController, rather than the entire hosting controller. To resolve this, unless you have another suggestion, it seems I either need to add a scene delegate like you mentioned, or I just need to live with the console error until Apple releases a built-in SwiftUI solution. – wristbands Jan 04 '22 at 01:15
  • 1
    Yeah that seems accurate, sorry that I didn’t test that. Using just the view probably doesn’t allow it to serve as a full hosting controller, like you were saying. If changing your plist doesn’t work, then using the app delegate and scene delegate solution is probably the best option. – Chris McElroy Jan 05 '22 at 04:50