最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - SwiftUI NavigationLink Unexpectedly Pops When Modifying State - Stack Overflow

programmeradmin2浏览0评论

Issue I’m encountering an issue in SwiftUI where modifying a value-type model inside a child view sometimes causes the navigation stack to pop unexpectedly. This happens even though I'm only modifying a property inside the child view, not replacing the entire model.

Context I have a list of items in a NavigationStack, where selecting an item navigates to a detail view. The detail view allows editing the name property via a TextField.

Here’s the simplified code:

import SwiftUI

struct Item: Identifiable, Hashable {
    let id: UUID
    var name: String
}

struct ContentView: View {
    @State private var items = [
        Item(id: UUID(), name: "Item 1"),
        Item(id: UUID(), name: "Item 2")
    ]

    var body: some View {
        NavigationStack {
            List($items) { $item in
                NavigationLink(item.name, value: item)
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
        }
    }
}

struct DetailView: View {
    @State var item: Item  // <- Local copy of the item

    var body: some View {
        VStack {
            TextField("Edit Name", text: $item.name)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .navigationTitle(item.name)
    }
}

Problem

When I type in the TextField, the view sometimes pops back to the previous screen without pressing the back button. It seems like the navigation stack is losing track of my Item instance when its name is updated.

  1. Used @Binding in DetailView
  • Modified DetailView to accept a @Binding var item: Item instead of using @State.
  • This caused a compiler error because NavigationDestination passes a copied value, not a binding.
  1. Wrapped Item in a Reference Type (class)
  • Converted Item from a struct to a class and used @ObservableObject.
  • This fixed the navigation pop issue but introduced complications with Codable, Hashable, and thread safety.
  1. Tracked the Selected Item Separately
  • Introduced an explicit @State private var selectedItem: Item? in ContentView.

  • Manually assigned the selected item before navigation.

  • This worked but felt like a hack rather than a SwiftUI-friendly approach.

  • Expected Behavior Updating item.name inside DetailView should only update that field and not cause SwiftUI to pop the navigation stack.

  • Actual Behavior Changing item.name sometimes causes the view to pop unexpectedly, as if SwiftUI lost track of the navigation state.

Issue I’m encountering an issue in SwiftUI where modifying a value-type model inside a child view sometimes causes the navigation stack to pop unexpectedly. This happens even though I'm only modifying a property inside the child view, not replacing the entire model.

Context I have a list of items in a NavigationStack, where selecting an item navigates to a detail view. The detail view allows editing the name property via a TextField.

Here’s the simplified code:

import SwiftUI

struct Item: Identifiable, Hashable {
    let id: UUID
    var name: String
}

struct ContentView: View {
    @State private var items = [
        Item(id: UUID(), name: "Item 1"),
        Item(id: UUID(), name: "Item 2")
    ]

    var body: some View {
        NavigationStack {
            List($items) { $item in
                NavigationLink(item.name, value: item)
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
        }
    }
}

struct DetailView: View {
    @State var item: Item  // <- Local copy of the item

    var body: some View {
        VStack {
            TextField("Edit Name", text: $item.name)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .navigationTitle(item.name)
    }
}

Problem

When I type in the TextField, the view sometimes pops back to the previous screen without pressing the back button. It seems like the navigation stack is losing track of my Item instance when its name is updated.

  1. Used @Binding in DetailView
  • Modified DetailView to accept a @Binding var item: Item instead of using @State.
  • This caused a compiler error because NavigationDestination passes a copied value, not a binding.
  1. Wrapped Item in a Reference Type (class)
  • Converted Item from a struct to a class and used @ObservableObject.
  • This fixed the navigation pop issue but introduced complications with Codable, Hashable, and thread safety.
  1. Tracked the Selected Item Separately
  • Introduced an explicit @State private var selectedItem: Item? in ContentView.

  • Manually assigned the selected item before navigation.

  • This worked but felt like a hack rather than a SwiftUI-friendly approach.

  • Expected Behavior Updating item.name inside DetailView should only update that field and not cause SwiftUI to pop the navigation stack.

  • Actual Behavior Changing item.name sometimes causes the view to pop unexpectedly, as if SwiftUI lost track of the navigation state.

Share Improve this question asked Mar 15 at 12:21 Akhlaq AhmadAkhlaq Ahmad 17 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 0

State should always be private

Since the navigationDestination by Type does not support Binding

you should use the one wirh isPresented

struct RowView: View {

    @State private var showDetails = false
    @Binding var item: Item

    var body: some View {

    
        Button("Show details") {
            showDetails = true
        }
         .navigationDestination(isPresented: $showDetails) {
        DetailView(item: $item)
    }
}
  • Remove Hashable from Item.
  • Change value: to item.id.
  • Change destination type to Item.ID.self.
  • Compute a binding from the id to the item in the destination closure and use @Binding in DetailView. See sample code below that implements all of this:

The reason it has to be done this way is NavigationStack has a path and the NavigationLink is like a button that adds the value to the path. It can't update an existing path value when the value changes, it can't find it because its whole hash has changed, instead it has to remove the value and add the new one. That is what is causing the pop and push you sometimes see when the name property of the item struct is changed. The name should not be part of the hash that is used to identify values in the path. So the solution is to make the value the id instead, so that the same id can stay in the path when the corresponding item's name changes.

The other problem with a whole item in the path is the source of truth is wrong. Because you are trying to go from the state to the nav path to the destination, where as it should directly flow from the state to the destination so that a new binding can be computed from the state whenever any item is changed, and hence the state changes and body is called which recalculates a replacement destination for the same path value.

This pattern also allows you to use an .onChange(items.map(\.ids)) and you can programatically pop the nav path if the id is no longer contained in the items, e.g. it has been deleted somewhere. I'll add that as a second sample.

import SwiftUI

struct Item: Identifiable {
    let id: UUID
    var name: String
}

struct ContentView: View {
    @State private var items = [
        Item(id: UUID(), name: "Item 1"),
        Item(id: UUID(), name: "Item 2")
    ]
    
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(item.name, value: item.id)
            }
            .navigationDestination(for: Item.ID.self) { id in
                if let index = items.firstIndex(where: { $0.id == id }) {
                    DetailView(item: $items[index])
                }
            }
        }
    }
}

struct DetailView: View {
    @Binding var item: Item  // <- Local copy of the item
    
    var body: some View {
        VStack {
            TextField("Edit Name", text: $item.name)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .navigationTitle(item.name)
    }
}

Version with programatic pop:

import SwiftUI

struct Item: Identifiable {
    let id: UUID
    var name: String
}

struct ContentView: View {
    @State var path: [Item.ID] = []
    @State private var items = [
        Item(id: UUID(), name: "Item 1"),
        Item(id: UUID(), name: "Item 2")
    ]
    
    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(item.name, value: item.id)
            }
            .navigationDestination(for: Item.ID.self) { id in
                if let index = items.firstIndex(where: { $0.id == id }) {
                    DetailView(item: $items[index])
                }
            }
        }
        .onChange(of: items.map(\.id)) { _, newIDs in
            // clear path if the detail item was deleted
            if let first = path.first {
                if !newIDs.contains(first) {
                    path = []
                }
            }
        }
    }
}

struct DetailView: View {
    @Binding var item: Item  // <- Local copy of the item
    
    var body: some View {
        VStack {
            TextField("Edit Name", text: $item.name)
                .textFieldStyle(.roundedBorder)
                .padding()
        }
        .navigationTitle(item.name)
    }
}
发布评论

评论列表(0)

  1. 暂无评论