I want to display a profile button like how iOS Health and App Store app do, and I thought it was done using .toolbarTitleDisplayMode(.inlineLarge)
(available in iOS 17+). I get the desired look but the Image
in the code below does not hide on scroll/when navigationTitle transitions to inline.
struct SomeView: View {
var body: some View {
NavigationStack {
ScrollView {
ForEach(0..<50) { i in
Text("Item \(i)")
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Image(systemName: "person.circle.fill")
.toolbarTitleDisplayMode(.inlineLarge)
}
}
.navigationTitle("Profile")
}
}
}
I am not sure that your interpretation of .toolbarTitleDisplayMode(.inlineLarge)
is correct. The documentation says:
In iOS, this behavior displays the title as large inside the toolbar and moves any leading or centered toolbar items into the overflow menu of the toolbar.
This does not seem to suggest, that an item with placement .topBarTrailing
will be hidden when a large title is scrolled up.
However, one way to get it working is to detect when the content has been scrolled up by a threshold amount and to hide the item when this happens.
A GeometryReader
behind the scrolled content can be used to detect the scroll offset.
I found it works best to examine the scroll position in the global coordinate space, using the coordinate space of the ScrollView
was less reliable. This may be because the position of the content inside the ScrollView
is changing as the navigation re-arranges itself. So this means knowing the size of the top safe area insets. A GeometryReader
around the NavigationStack
can be used to measure the safe area insets.
Once the scroll offset exceeds a threshold amount, the item can be hidden. One way to do this would just be to stop showing it, but it may be difficult to animate the change when doing it this way. As an alternative, you can hide the item by setting opacity to 0. An opacity change can be animated easily.
As the content is scrolled up, the inline navigation title appears before the navigation background turns to a material effect. You might like to hide the item around the same time as the background changes. The timing can be tweaked by changing the size of the scroll threshold.
struct SomeView: View {
@State private var isInlineTitle = false
private func scrollDetector(topInsets: CGFloat) -> some View {
GeometryReader { proxy in
let minY = proxy.frame(in: .global).minY
let isUnderToolbar = minY - topInsets - 40 < 0
Color.clear
.onChange(of: isUnderToolbar) { _, newVal in
isInlineTitle = newVal
}
}
}
var body: some View {
GeometryReader { outer in
let topInsets = outer.safeAreaInsets.top
NavigationStack {
ScrollView {
VStack {
ForEach(0..<50) { i in
Text("Item \(i)")
}
}
.frame(maxWidth: .infinity)
.background {
scrollDetector(topInsets: topInsets)
}
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Image(systemName: "person.circle.fill")
.opacity(isInlineTitle ? 0 : 1)
}
}
.navigationTitle("Profile")
}
.animation(.easeInOut(duration: 0.2), value: isInlineTitle)
}
}
}