Home > Software engineering >  SwiftUI: NavigationView detail view pops backstack when previous's view List changes
SwiftUI: NavigationView detail view pops backstack when previous's view List changes

Time:07-25

I have a List of ids and scores in my first screen.

In the detail screen I click and call a callback that add to the score resorts the List by the score.

When I do this with an item at the top of the list, nothing happens. (Good)

When I do this with an item at the bottom of the list, the navigation view pops the backstack and lands me back on the first page. (Bad)

import SwiftUI

class IdAndScoreItem {
    var id: Int
    var score: Int
    init(id: Int, score: Int) {
        self.id = id
        self.score = score
    }
}

@main
struct CrazyBackStackProblemApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ListView()

            }
            .navigationViewStyle(.stack)
        }
    }
}

struct ListView: View {
    @State var items = (1...50).map { IdAndScoreItem(id: $0, score: 0) }
    func addScoreAndSort(item: IdAndScoreItem) {
        items = items
            .map {
                if($0.id == item.id) { $0.score  = 1 }
                return $0
            }
            .sorted {
                $0.score > $1.score
            }
    }
    var body: some View {
        List(items, id: \.id) { item in
            NavigationLink {
                ScoreClickerView(
                    onClick: { addScoreAndSort(item: item) }
                )
            } label: {
                Text("id: \(item.id) score:\(item.score)")
            }
        }
    }
}

struct ScoreClickerView: View {
    var onClick: () -> Void
    var body: some View {
        Text("tap me to increase the score")
            .onTapGesture {
                onClick()
            }
    }
}

How can I make it so I reorder the list on the detail page, and that's reflected on the list page, but the navigation stack isn't popped (when I'm doing it on a list item at the bottom of the list). I tried added navigationStyle(.stack) to no avail.

Thanks for any and all help!

CodePudding user response:

Resort changes order of IDs making list recreate content that leads to current NavigationLinks destroying, so navigating back.

A possible solution is to separate link from content - it can be done with introducing something like selection (tapped row) and one navigation link activated with that selection.

Tested with Xcode 14 / iOS 16

demo

@State private var selectedItem: IdAndScoreItem?  // selection !!

var isNavigate: Binding<Bool> {   // link activator !!
    Binding(get: { selectedItem != nil}, set: { _ in selectedItem = nil })
}

var body: some View {
    List(items, id: \.id) { item in
        Text("id: \(item.id) score:\(item.score)")   // tappable row
            .frame(maxWidth: .infinity, alignment: .leading)
            .contentShape(Rectangle())
            .onTapGesture {
                selectedItem = item
            }
    }
    .background(
        NavigationLink(isActive: isNavigate) {  // one link !!
            ScoreClickerView {
                if let item = selectedItem {
                    addScoreAndSort(item: item)
                }
            }
        } label: {
            EmptyView()
        }
    )
}

CodePudding user response:

Do your sorting on onAppear. No need to sort on each click.

struct ListView: View {
    
    @State var items = (1...50).map { IdAndScoreItem(id: $0, score: 0) }
    
    func addScoreAndSort(item: IdAndScoreItem) {
        item.score  = 1
    }
    var body: some View {
        List(items, id: \.id) { item in
            NavigationLink {
                ScoreClickerView(
                    onClick: { addScoreAndSort(item: item) }
                )
            } label: {
                Text("id: \(item.id) score:\(item.score)")
            }
        }.onAppear { // <==== Here
            items = items
                .sorted {
                    $0.score > $1.score
                }
        }
    }
}

Note : No need to use map here. since you are using class so it will update with reference.

  • Related