SwiftUI’s .navigationTitle

I did take a look at SwiftUI several times but always found problems too big to consider it production ready. Not even the example app ran without crashing. So for three or four tries, it went like this: I started a demo project, after one or two hours I ran into a problem that I could not figure out how to solve in SwiftUI on my own. I visited the usual websites and found more often then not that it is something that is missing in SwiftUI.

Three years in, this SwiftUI thing still has not disappeared. Quite the opposite seems to be true, it’s more like Apple went all-in on SwiftUI. With iOS 16, SwiftUI seems to have grown a lot. I decided to give it another try. I’m not too surprised that there still are rough edges. More than I’d think is acceptable for an framework that is out of beta for so long. But that’s another story.

What I found confusing this time is which .navigationTitle gets used if it appears more than once in the view hierarchy. Think about the following code:

struct RecipeView: View {
   var ingredients: [String]
   var instructions: String

   var body: some View {
      VStack {
         IngredientsView(ingredients: ingredients)
         InstructionsView(instructions: instructions)
      }
      .navigationTitle("Recipe")
    }
}

If you placed it in a NavigationView (or NavigationStack), which title would you expect?

The SwiftUI-answer is: It depends.

SwiftUI does the equivalent of a deeps first tree search for the first .navigationTitle. While doing so, it ignores any title that is attached to an EmptyView or an empty ForEach. So if your IngredientsView looks like this:

struct IngredientsView: View {
   var ingredients: [String]

   var body: some View {
      HStack {
         ForEach(ingredients, id: \.self) { ingredient in
            Text(verbatim: ingredient)
               .navigationTitle("Ingredient: \(ingredient)")
         }
         .navigationTitle ("Ingredients")
      }
   }
}

The title will be “Ingredient: Butter” given the first ingredient is “Butter”. SwiftUI encourages composition so I would probably not use Text here but an IngredientView that might set its navigationTitle so it can be used in a NavigationView on its own. In this case you have to dig even deeper to see the title that is used.

If there is no ingredient, the ForEach is empty thus the “Ingredients” title is never used. It will use whatever InstructionsView wants as title. Or “Recipe”, depending on implementation details of InstructionsView.

How to solve this?

If you want to make sure that your navigationTitle is used, your title has to be the first that SwiftUI sees. You cannot just start the body with an EmptyView().navigationTitle("...") because these get ignored. What I found currently works is e.g. an empty HStack:

   var body: some View {
      HStack(){}
         .navigationTitle("Recipe")

      // […]
   }

Probably something like the following would be useful:

struct NavigationTitleView<Content>: View where Content: View {
   var title: Text
   var content: () -> Content

   init(_ title: Text, @ViewBuilder content: @escaping () -> Content) {
      self.title = title
      self.content = content
   }

   init(_ titleKey: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content) {
      self.title = Text(titleKey)
      self.content = content
   }

   @_disfavoredOverload
   init<S>(_ title: S, @ViewBuilder content: @escaping () -> Content) where S: StringProtocol {
      self.title = Text(title)
      self.content = content
   }

   init(verbatim title: String, @ViewBuilder content: @escaping () -> Content) {
      self.title = Text(verbatim: title)
      self.content = content
   }

   var body: some View {
      ZStack {
         HStack(){}.navigationTitle(title)

         content()
      }
   }
}

This also places the empty HStack in an ZStack to prevent it to move down the content if any padding is applied.

Note: All of the above is true for .navigationDocument, too.