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