List Items Shift When Tapping on NavigationLink

I am trying to fix an annoying issue here a list of navigation links will shift if the user taps on one of the links after scrolling down.

Notice how after I scroll down a bit and tap on item 60 - the entire list jumps down as the transition to the new view plays out.

This is the source to reproduce the issue. Note that it only happens after you've scrolled down a bit.

import SwiftUI

struct ContentView: View {
    let items = generateItems()

    var body: some View {
        NavigationStack {
            List(items, id: \.self) { item in
                NavigationLink {
                    Text(item)
                } label: {
                    Text(item)
                }
            }
            .navigationTitle("Items")
        }
    }
}

#Preview {
    ContentView()
}

func generateItems() -> [String] {
    var items: [String] = []

    for i in 1...1000 {
        items.append("\(i)")
    }

    return items
}

From what I can see of your code, when you pop back from a navigation link, your ContentView gets rebuilt. The first thing done in ContentView is to create new list data. So, you are creating a new List and since it is new, it has no "remembered" scroll position, so it seems to scroll to the top. The truth is, it has nothing else to display except from the top. In addition, none of your NavigationLink (item views) has an .id so the list has no way of tracking where you are, even if it was smart enough to track.

The new .scrollPosition($savedPosition) could help, but for some reason, it only seems to work with a ScrollView directly, and not within a container with an embedded ScrollView (i.e., `List).

So, some suggestions:

  • Do not create the data for your List inside your ContentView. Create it onAppear on within the init() and save it in a @State. The List should pick up where it left off (I haven't tried this with your example, but I think that's the standard behavior I've seen). Even if this doesn't work, the following suggestions should help (iOS 17+)...

  • Add .id(item) to your ``NavigationLink`. Let it be trackable!

  • Add a @State variable for savedPosition (of type String?) , change your List to a ScrollView { LazyVStack { ForEach { .... } } } and attach a .savedPosition to the ScrollView (iOS 17+):

ScrollView {
    LazyVStack {
        ForEach($items, id: \.self) {
            NavigationLink {
                Text(item)
            } label: {
                Text(item)
            }
            .id(item)
        }
    }
    .scrollTargetLayout()
}
.scrollPosition($savedPosition)

I was running into this issue as well, and was able to resolve it by including a binding for the List's selection argument. I'm not quite sure why this fixed it, but worked for my needs. I was able to verify this in the code sample you provided above as well. Hope this works for you!

import SwiftUI

struct ContentView: View {
    let items = generateItems()
    @State var selectedItem: String?

    var body: some View {
        NavigationStack {
            List(items, id: \.self, selection: $selectedItem, rowContent: { item in
                NavigationLink {
                    Text(item)
                } label: {
                    Text(item)
                }

            })
            .navigationTitle("Items")
        }
    }
}

#Preview {
    ContentView()
}

func generateItems() -> [String] {
    var items: [String] = []

    for i in 1...1000 {
        items.append("\(i)")
    }

    return items
}

I don't have a good solution to the problem but I've found a workaround. For me setting .navigationBarTitleDisplayMode(.inline) somehow fixed it. Maybe something related to the spacing of the navigation bar is messing it up?

Just putting this out here and hopefully it is helpful in identifying the real issue - I believe an inline title is not desirable in both of our cases, but unfortunately I couldn't find anything else.

List Items Shift When Tapping on NavigationLink
 
 
Q