I’ve been working on an app that requires the main screen to have the navigation bar display the title in line with the other bar button items.

The next view should allow the title to be displayed out-of-line and use a larger font when the main view is tapped.

So, the idea is more or less to set prefersLargeTitles to false on the main view and true for the other child views.

Also, for navigation, I’m using UINavigationController instead of the normal NavigationView provided by SwiftUI. This has helped in a much better navigation flow. All the SwiftUI views are embedded in a UIHostingController and passed around.

I tried many methods. The end goal is to have a smooth transition and not something where the components move up when the screen appears and move down when it disappears, giving an impression of an ugly broken app look.

Access the Navigation Controller

The first hack is to create a @Published variable of the type UINavigationController in the view model and update it accordingly:

class MainViewModel: ObservableObject {
@Published var controller: UINavigationController?

In the SwiftUI view, updating the value of prefersLargeTitles:

struct MainView: View {
  @ObservedObject var viewModel: MainViewModel
  var body: some View {
    Text("VIEW CONTENT")
      .onAppear {
        viewModel.controller?.navigationBar.prefersLargeTitles = false
      .onDisappear {
        viewModel.controller?.navigationBar.prefersLargeTitles = true

Even though this solution works, it gives a weird ugly transition that cannot be described in words. Horrible.

Custom Hosting Controller

Next, I remember using such hacks in UIKit to hide and show the navigation bar conditionally, so it’s time to go back to the old ways.

As the MainView is the root view of a hosting controller, I start with a custom hosting controller to override the viewWillAppear(_:) and viewWillDisappear(_:).

I still have to investigate, but it looks like there’s no alternative to it in SwiftUI. onAppear and onDisappear feels like they are the viewDidAppear(_:) and viewDidDisappear(_:). If I’m wrong or you know more about it, let me know.

From the documentation of UIHostingController for viewWillAppear(_:):

SwiftUI calls this method before adding the hosting controller’s root view to the view hierarchy. You can override this method to perform custom tasks associated with the appearance of the view. If you override this method, you must call super at some point in your implementation.

This seems to be the perfect place to update the navigation controller. Also, I don’t have to write a hack to access the navigation controller from the view model!

The custom controller looks like:

class UICustomHostingController<Content>: UIHostingController<Content> where Content: View {
  override init(rootView: Content) {
    super.init(rootView: rootView)
  @available(*, unavailable)
  required public init?(coder aDecoder: NSCoder) {
    	 fatalError("init(coder:) has not been implemented")
  override func viewWillAppear(_ animated: Bool) {
     navigationController?.navigationBar.prefersLargeTitles = false
  override func viewWillDisappear(_ animated: Bool) {
    navigationController?.navigationBar.prefersLargeTitles = true

And then, instead of using the normal UIHostingController, it uses the custom one:

let view = MainView(viewModel: viewModel)

let controller = UICustomHostingController(rootView: view)

navigationController = UINavigationController(rootViewController: controller)

And this solution gives a smooth transition!


Working with SwiftUI is fun and not so fun. For custom scenarios like these, you’ve to resort to UIKit and look for workarounds.

If you have a better approach, please tag @rudrankriyam on Twitter! I love constructive feedback and appreciate constructive criticism.

Thanks for reading, and I hope you’re enjoying it!