Search code examples
swiftuiswiftui-navigationlink

SwiftUI doesn't update second NavigationLink destination


I have a List with rows which push a View. That view has another List which pushing another View. The original List, and the first pushed List will update when the data changes. However, the last view does not update when pushed. And when I swipe back the view no longer updates, even though it used to.

HomeView > UserView > ItemView

User and Item are structs which are Identifiable. I've tried making them Hashable and using id: \.self, but that didn't seem to work either.

class App: ObservableObject {
    @Published var users = [User]()
}

struct HomeView {

    @EnvironmentObject var app: App

    var body {
        List {
            Section {
                ForEach(app.users) { user in
                    NavigationLink(destination: UserView(user: user)) {
                        Text(user.name)
                    }
                }
            }
        }
    }

}

// Updates fine when `app.users` updates
// Stops updating after going back from ItemView
struct UserView {

    let user: User

    var body {
        List {
            Section {
                ForEach(user.items) { item in
                    NavigationLink(destination: ItemView(user: user, item: item)) {
                        Text(item.name)
                    }
                }
            }
        }
    }

}

/// Does not update when app.users updates
struct ItemView {

    let user: User
    let item: Item

    var body {
        List {
            Section {
                ForEach(item.details) { detail in
                    Text(detail)
                }
            }
        }
    }

}

Solution

  • After long searches I've come up with something I have yet to see elsewhere on the internet. I am initializing my views with a @State from the parent view and updating it using onReceive. Further, onReceive/onAppear checks if the item is still valid and pops the view if needed. It might be more correct to make an init to set the State and make that private.

    The main reason for this was deleting was causing crashes. Here's a complete example I made to test changing and deleting the source.

    struct Item: Identifiable {
        var id: String
        var name: String
        var accounts: [Account]
    }
    
    struct Account: Identifiable {
        var id: String
        var name: String
    }
    
    class App: ObservableObject {
        @Published var items: [Item] = [
            Item(id: "a", name: "A", accounts: [
                Account(id: "1", name: "one"),
                Account(id: "2", name: "two"),
                Account(id: "3", name: "three")
            ])
        ]
    }
    
    struct RootView: View {
        var body: some View {
            NavigationView {
                ContentView().environmentObject(App())
            }
        }
    }
    
    struct ContentView: View {
    
        @EnvironmentObject var app: App
    
        var body: some View {
            List {
                ForEach(app.items) { item in
                    NavigationLink(destination: ItemView(item: item)) {
                        Text("\(item.id) - \(item.name)")
                    }
                }
                Button(action: { self.app.items[0].name = "XXX" }) {
                    Text("Change Item Name")
                }
                Button(action: { self.app.items = [] }) {
                    Text("Clear")
                }
            }
        }
    
    }
    
    struct ItemView: View {
    
        @Environment(\.presentationMode) var presentationMode
        @EnvironmentObject var app: App
    
        @State var item: Item
    
        var body: some View {
            List {
                Text("\(item.id) - \(item.name)")
                ForEach(item.accounts) { account in
                    NavigationLink(destination: AccountView(item: self.item, account: account)) {
                        Text("\(account.id) - \(account.name)")
                    }
                }
                Button(action: { self.app.items[0].name = "XXX" }) {
                    Text("Change Item Name")
                }
                Button(action: { self.app.items[0].accounts[0].name = "AAA" }) {
                    Text("Change Account Name")
                }
                Button(action: { self.app.items = [] }) {
                    Text("Clear")
                }
            }
            .onReceive(app.$items) { items in
                guard let item = items.first(where: { $0.id == self.item.id }) else {
                    self.presentationMode.wrappedValue.dismiss()
                    return
                }
                self.item = item
            }
            .onAppear {
                if !self.app.items.contains(where: { $0.id == self.item.id }) {
                    self.presentationMode.wrappedValue.dismiss()
                }
            }
        }
    
    }
    
    struct AccountView: View {
    
        @Environment(\.presentationMode) var presentationMode
        @EnvironmentObject var app: App
    
        @State var item: Item
        @State var account: Account
    
        var body: some View {
            List {
                Text("\(item.id) - \(item.name)")
                Text("\(account.id) - \(account.name)")
                Button(action: { self.app.items[0].name = "XXX" }) {
                    Text("Change Item Name")
                }
                Button(action: { self.app.items[0].accounts[0].name = "AAA" }) {
                    Text("Change Account Name")
                }
                Button(action: { self.app.items = [] }) {
                    Text("Clear")
                }
            }
            .onReceive(app.$items) { items in
                guard
                    let item = items.first(where: { $0.id == self.item.id }),
                    let account = item.accounts.first(where: { $0.id == self.account.id })
                else {
                    self.presentationMode.wrappedValue.dismiss()
                    return
                }
                self.item = item
                self.account = account
            }
        }
    }