iosswiftswiftuiswiftui-navigationstack

Change NavigationStack title font, tint, and background in SwiftUI


I am updating the UI of an iOS app, now targeting iOS 16 and above.

In the previous version of the design I was changing the .navigationTitle font, style, and colour using something like this code:

final class AppSettings: ObservableObject {
  @Published var tint: Color = .red {
    didSet { updateNavigationBarAppearance() }
  }

  init() {
    updateNavigationBarAppearance()
  }

  private func updateNavigationBarAppearance() {
    let appearance = UINavigationBarAppearance()
    appearance.configureWithTransparentBackground()
    appearance.largeTitleTextAttributes = [
      .font: UIFont.preferredFont(forTextStyle: .largeTitle).roundedBold,
      .foregroundColor: UIColor(tint).withAlphaComponent(0.9)
    ]
   appearance.titleTextAttributes = [
      .font: UIFont.preferredFont(forTextStyle: .headline).roundedBold,
      .foregroundColor: UIColor(tint).withAlphaComponent(0.9)
    ]
    let navBarAppearance = UINavigationBar.appearance()
    navBarAppearance.standardAppearance = appearance
    navBarAppearance.compactAppearance = appearance
    navBarAppearance.scrollEdgeAppearance = appearance
  }
}

extension UIFont {
  var roundedBold: UIFont {
    guard let descriptor = fontDescriptor
                             .withDesign(.rounded)?
                             .withSymbolicTraits(.traitBold) else { return self }
  return UIFont(descriptor: descriptor, size: pointSize)
  }
}

That gave me something that looks like this:

Rounded navigation title font and colour

But had the bug of this occurring on scrolled views as we were using the appearance.configureWithTransparentBackground(), and transparency doesn't work in the UINavigationBar.appearance().backgroundColor configuration.

Scrolled content overlapped

With the OS version bump, we can now take advantage of using .toolbarBackground which has fixed the transparency issue when scrolling into the nav area.

However, when that modifier is implemented it affects the font of the navigation title, returning it to the default serif face and black/white colour.

Does anyone know of a way to customise the title while also using the .toolbarBackground modifier?

Though the .navigationTitle accepts a Text element, it seems you cannot customise it, for example:

  .navigationTitle(
    Text("Today")
      .font(.system(.largeTitle, design: .rounded, weight: .black))
  )

As you get this warning in Xcode:


Solution

  • I couldn't find a way to change the font or color of the main navigation title. However, a workaround is to show your own title. You can then style it any way you like.

    @State private var showingScrolledTitle = false
    
    private func scrollDetector(topInsets: CGFloat) -> some View {
        GeometryReader { proxy in
            let minY = proxy.frame(in: .global).minY
            let isUnderToolbar = minY - topInsets < 0
            Color.clear
                // pre iOS 17: .onChange(of: isUnderToolbar) { newVal in
                .onChange(of: isUnderToolbar) { _, newVal in
                    showingScrolledTitle = newVal
                }
        }
    }
    
    var body: some View {
        GeometryReader { outer in
            NavigationStack {
                List {
                    Section {
                        ForEach(1...5, id: \.self) { val in
                            NavigationLink("List item \(val)") {
                                Text("List item \(val)")
                            }
                        }
                    } header: {
                        Text("Today")
                            .font(.custom("Chalkboard SE", size: 36))
                            .textCase(nil)
                            .bold()
                            .listRowInsets(.init(top: 4, leading: 0, bottom: 8, trailing: 0))
                            .background {
                                scrollDetector(topInsets: outer.safeAreaInsets.top)
                            }
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        Image(systemName: "gearshape.fill")
                    }
                    ToolbarItem(placement: .principal) {
                        Text("Today")
                            .font(.custom("Chalkboard SE", size: 18))
                            .bold()
                            .opacity(showingScrolledTitle ? 1 : 0)
                            .animation(.easeInOut, value: showingScrolledTitle)
                    }
                    ToolbarItem(placement: .topBarTrailing) {
                        Image(systemName: "calendar")
                            .padding(.trailing, 20)
                    }
                    ToolbarItem(placement: .topBarTrailing) {
                        Image(systemName: "plus.circle.fill")
                    }
                }
                .navigationTitle("Today")
                .navigationBarTitleDisplayMode(.inline)
                .scrollContentBackground(.hidden)
                .foregroundStyle(.indigo)
                .background(.indigo.opacity(0.1))
                .toolbarBackground(.indigo.opacity(0.1))
            }
        }
    }
    

    Animation