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:
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.
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:
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.
In your case, the title can be shown as a header
to a List
Section
, so that it scrolls with the List
.
A GeometryReader
in the background of the substitute title can be used to detect when the title has been scrolled. This in turn can be used to control the visibility (opacity) of the secondary heading.
The secondary heading can simply be defined as a ToolbarItem
with placement .principal
, this supports full styling.
If you want the navigation title to appear as the back link on nested views then it can be set in the usual way but with display mode .inline
. It seems the toolbar item with placement .principal
takes priority over the inline title, so setting a title in this way does not impact the custom styling.
In order to allow for the safe area insets at the top, it would be good if the scroll detector could refer to the coordinate space of the List
. However, I found that it is not possible to refer to the coordinate space of a container from within a list Section
. As a workaround here, the scroll detector uses the global coordinate space and it is supplied with the size of the top safe area insets. These are measured using another GeometryReader
surrounding the main content.
@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))
}
}
}