Whenever I tap on a NavigationLink in a List after the list is scrolled a bit, it appears that the list shifts down temporarily. Please see the following video (simulator with "Slow Animations" enabled):

Here's my code:
struct SearchView: View {
private static let navTitle = "Title"
@EnvironmentObject var locModel: LocationViewModel
@ObservedObject private var searchModel = SearchViewModel()
var body: some View {
NavigationStack {
if searchModel.isLoading {
ProgressView()
.navigationTitle(SearchView.navTitle)
} else if searchModel.results.isEmpty {
Text("No results.") // TODO
.navigationTitle(SearchView.navTitle)
} else if let total = searchModel.total, let lastPage = searchModel.lastLoadedPage {
List {
Section {
ForEach(searchModel.results.indices, id: \.self) { i in
SearchResultRow(searchModel.results[i]).id(i)
}
if searchModel.results.count < total {
HStack {
Spacer()
ProgressView()
Spacer()
}.task {
await load(page: lastPage + 1)
}
}
} header: {
if let locName = locModel.location?.name {
Text("**\(total)** results near **\(locName)**")
}
}
}
.navigationTitle(SearchView.navTitle)
}
}
.onAppear {
Task.detached { await load() }
}
.onChange(of: locModel) {
Task.detached { await load() }
}
}
private func load(page: Int = 1) async {
if let loc = locModel.location {
await searchModel.load(location: loc, page: page)
}
}
}
struct MinyanSearchResultRow: View {
let searchResult: SearchResult
init(_ searchResult: SearchResult) {
self.searchResult = searchResult
}
var body: some View {
NavigationLink {
DetailView(searchResult: searchResult)
} label: {
VStack(alignment: .leading) {
Text("Hello there")
}
}
}
}
struct DetailView: View {
let searchResult: SearchResult
var body: some View {
Text("Test")
}
}
This problem seems to be related to the way the navigation title is displayed:
If the navigation title is .large (the default), the rows jump when a link is selected.
If the navigation title is not set, it reserves space for the navigation toolbar when a link is selected and this also causes a jump.
However, if the navigation title is .inline, there is no jump.
So as a workaround, you can use an inline title, along with these changes:
GeometryReader in the background of the header to detect scrolling.This is actually the same technique as used for styling the navigation title in the answer to Change NavigationStack title font, tint, and background in SwiftUI (it was my answer).
Here is a stripped-down version of your example to show it working:
struct SearchView: View {
private static let navTitle = "Title"
private let results: [SearchResult]
@State private var showingScrolledTitle = false
init() {
var results = [SearchResult]()
for _ in 1...100 {
results.append(SearchResult())
}
self.results = results
}
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 { proxy in
NavigationStack {
List {
Section {
ForEach(results) { result in
SearchResultRow(result)
}
} header: {
VStack(alignment: .leading, spacing: 25) {
Text(SearchView.navTitle)
.font(.largeTitle)
.fontWeight(.bold)
.textCase(nil)
Text("**\(results.count)** results near **here**")
.font(.footnote)
.padding(.leading, 24)
.foregroundStyle(.secondary)
}
.foregroundStyle(.primary)
.background {
scrollDetector(topInsets: proxy.safeAreaInsets.top)
}
.listRowInsets(.init(top: 4, leading: -4, bottom: 6, trailing: 0))
}
}
.toolbar {
ToolbarItem(placement: .principal) {
Text(SearchView.navTitle)
.font(.headline)
.opacity(showingScrolledTitle ? 1 : 0)
.animation(.easeInOut, value: showingScrolledTitle)
}
}
.navigationTitle(SearchView.navTitle)
.navigationBarTitleDisplayMode(.inline)
}
}
}
}
struct SearchResult: Identifiable {
let id = UUID()
}
struct SearchResultRow: View {
let searchResult: SearchResult
init(_ searchResult: SearchResult) {
self.searchResult = searchResult
}
var body: some View {
NavigationLink {
DetailView(searchResult: searchResult)
} label: {
VStack(alignment: .leading) {
Text("Hello there")
}
}
}
}
struct DetailView: View {
let searchResult: SearchResult
var body: some View {
Text("Test")
}
}

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With