5
votes

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

}
4
What type of User and Item and how/where do you update them? - Asperi
@Asperi They are both structs, but Item is nested within User. It may be because the views are not updated from initialized properties, but may need a binding. Which make it look more complex. I'm wondering if there's something else I can do. - Michael Ozeryansky
You definitely need bindings, because you pass just copies of your model values into subviews, so any modifications inside remain inside. - Asperi

4 Answers

1
votes

This is the test I made and is working well, everything updates as expected.

struct User: Identifiable {
    var id: String
    var name: String
    var items: [Item]
}

struct Item: Identifiable {
    var id: String
    var name: String
    var details: [String]
}

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

    init() {
    let items1 = [Item(id: UUID().uuidString, name: "item1", details: ["d1","d2"]), Item(id: UUID().uuidString, name: "item2", details: ["d3","d4"])]
    let items2 = [Item(id: UUID().uuidString, name: "item3", details: ["e1","e2"]), Item(id: UUID().uuidString, name: "item4", details: ["e3","e4"])]
    users.append(User(id: UUID().uuidString, name: "user1", items: items1))
    users.append(User(id: UUID().uuidString, name: "user2", items: items2))
    }
}


struct ContentView: View {

@ObservedObject var app = App()

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


struct UserView: View {
@State var user: User

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

struct ItemView: View {
@State var item: Item

var body: some View {
    List {
        ForEach(item.details, id: \.self) { detail in
            Text(detail)
        }
    }
}
}
1
votes

Ok, I think I'm starting to understand what you want. How about something like this:

struct ContentView: View {

@EnvironmentObject var app: App

var body: some View {
    NavigationView {
        List(0..<app.users.count) { i in
            NavigationLink(destination: UserView(user: self.$app.users[i])) {
                Text(self.app.users[i].name)
            }
        }
    }
}


struct UserView: View {
@Binding var user: User
var body: some View {
    List(0..<user.items.count) { i in
            NavigationLink(destination:
            ItemView(item: self.$user.items[i])) {
                Text(self.user.items[i].name)
            }
        }

}
}

struct ItemView: View {
@Binding var item: Item
@State var hasChanged = false
var body: some View {
    VStack {
        Button(action: {
            self.item.details.append(contentsOf: ["wx","wy"])
            self.hasChanged.toggle()
        }) {
            Text("Add an item")
        }
        List(0..<item.details.count, id: \.self) { i in
            Text(self.item.details[i])
        }
    }
}
}
0
votes

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
        }
    }
}
0
votes

I had an identical problem, and now I develop my solution like this below, based on answers in this topic

import SwiftUI

struct TItem: Identifiable {
    var id: String
    var name: String
    var accounts: [TAccount]
}

struct TAccount: Identifiable {
    var id: String
    var name: String
}

class TApp: ObservableObject {
    @Published var items: [TItem] = [
        TItem(id: "a", name: "A", accounts: [
            TAccount(id: "1", name: "Alice"),
            TAccount(id: "2", name: "Amazon"),
            TAccount(id: "3", name: "Arfa")
        ]),
        TItem(id: "b", name: "B", accounts: [
            TAccount(id: "1", name: "Boris"),
            TAccount(id: "2", name: "Bob"),
            TAccount(id: "3", name: "Blaster")
        ])
    ]
}

struct TRootView: View {
    var body: some View {
        NavigationView {
            TContentView().environmentObject(TApp())
        }
    }
}

struct TContentView: View {
    
    @EnvironmentObject var app: TApp
    
    var body: some View {
        List {
            ForEach(0..<app.items.count) { index in
                let item = $app.items[index]
                let wItem = item.wrappedValue
                NavigationLink(destination: TItemView(item: item)) {
                    VStack(alignment: .leading) {
                        Text("\(wItem.id) - \(wItem.name)")
                        HStack {
                            ForEach(wItem.accounts) { account in
                                Text("\(account.name)")
                                    .font(.callout)
                                    .foregroundColor(.gray)
                            }
                        }
                    }
                }
            }
            Button(action: { app.items[0].name = "XXX" }) {
                Text("Change Item Name")
            }
        }
    }
    
}

struct TItemView: View {
    
    @Binding var item: TItem

    var body: some View {
        List {
            Text("\(item.id) - \(item.name)")
            ForEach(0..<item.accounts.count) { index in
                NavigationLink(destination: TAccountView(account: $item.accounts[index])) {
                    Text("\(item.accounts[index].id) - \(item.accounts[index].name)")
                }
            }
            Button(action: { self.$item.name.wrappedValue = "XXX" }) {
                Text("Change Item Name")
            }
        }

    }
    
}

struct TAccountView: View {
    
    @Binding var account: TAccount
    
    
    var body: some View {
        List {
            Text("\(account.id) - \(account.name)")
            Button(action: { $account.name.wrappedValue = "CCC" }) {
                Text("Change Account Name")
            }
        }
    }
}


struct TRootView_Previews: PreviewProvider {
    static var previews: some View {
        TRootView()
    }
}