Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: List shifts items down when tapping on Navigation Link

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):

enter image description here

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")
    }
}
like image 448
Daniel Smith Avatar asked Dec 22 '25 01:12

Daniel Smith


1 Answers

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:

  • Incorporate the large title into the section header.
  • If the list has not been scrolled, show the inline title with opacity 0.
  • Use a GeometryReader in the background of the header to detect scrolling.
  • Once the header has been scrolled up as far as the safe area inset, show the inline title with opacity 1.

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")
    }
}

Animation

like image 84
Benzy Neez Avatar answered Dec 25 '25 18:12

Benzy Neez



Donate For Us

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