FetchedResult views do not update after navigating away from view & back

SwiftUI FetchedResult views fail to update when you navigate away from them and return.


I have a simple todo list app I've created as an example. This app consists of 2 entities:


- A TodoList, which can contain many TodoItem(s)

- A TodoItem, which belongs to one TodoList


First, here are my core data models:


https://i.stack.imgur.com/JOsEw.png


https://i.stack.imgur.com/Q9tOs.png



For the entities, I am using `Class Definition` in `CodeGen`.


There are only 4 small views I am using in this example.


`TodoListView`:


    struct TodoListView: View {
        @Environment(\.managedObjectContext) var managedObjectContext
        @FetchRequest(
            entity: TodoList.entity(),
            sortDescriptors: []
        ) var todoLists: FetchedResults
        @State var todoListAdd: Bool = false
      
        var body: some View {
            NavigationView {
                List {
                    ForEach(todoLists, id: \.self) { todoList in
                        NavigationLink(destination: TodoItemView(todoList: todoList), label: {
                            Text(todoList.title ?? "")
                        })
                    }
                }
                .navigationBarTitle("Todo Lists")
                .navigationBarItems(trailing:
                    Button(action: {
                        self.todoListAdd.toggle()
                    }, label: {
                        Text("Add")
                    })
                    .sheet(isPresented: $todoListAdd, content: {
                        TodoListAdd().environment(\.managedObjectContext, self.managedObjectContext)
                    })
                )
            }
        }
    }


This simply fetches all TodoList(s) and spits them out in a list. There is a button in the navigation bar which allows for adding new todo lists.


`TodoListAdd`:


    struct TodoListAdd: View {
        @Environment(\.presentationMode) var presentationMode
        @Environment(\.managedObjectContext) var managedObjectContext
        @State var todoListTitle: String = ""
      
        var body: some View {
            NavigationView {
                Form {
                    TextField("Title", text: $todoListTitle)
                  
                    Button(action: {
                        self.saveTodoList()
                        self.presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Save")
                    })
                  
                    Button(action: {
                        self.presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Cancel")
                    })
                }
                .navigationBarTitle("Add Todo List")
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
      
        func saveTodoList() {
            let todoList = TodoList(context: managedObjectContext)
            todoList.title = todoListTitle
          
            do { try managedObjectContext.save() }
            catch { print(error) }
        }
    }


This simply saves a new todo list and then dismisses the modal.


`TodoItemView`:


    struct TodoItemView: View {
        @Environment(\.managedObjectContext) var managedObjectContext
        var todoList: TodoList
        @FetchRequest var todoItems: FetchedResults
        @State var todoItemAdd: Bool = false
      
        init(todoList: TodoList) {
            self.todoList = todoList
            self._todoItems = FetchRequest(
                entity: TodoItem.entity(),
                sortDescriptors: [],
                predicate: NSPredicate(format: "todoList == %@", todoList)
            )
        }
      
        var body: some View {
            List {
                ForEach(todoItems, id: \.self) { todoItem in
                    Button(action: {
                        self.checkTodoItem(todoItem: todoItem)
                    }, label: {
                        HStack {
                            Image(systemName: todoItem.checked ? "checkmark.circle" : "circle")
                            Text(todoItem.title ?? "")
                        }
                    })
                }
            }
            .navigationBarTitle(todoList.title ?? "")
            .navigationBarItems(trailing:
                Button(action: {
                    self.todoItemAdd.toggle()
                }, label: {
                    Text("Add")
                })
                .sheet(isPresented: $todoItemAdd, content: {
                    TodoItemAdd(todoList: self.todoList).environment(\.managedObjectContext, self.managedObjectContext)
                })
            )
        }
      
        func checkTodoItem(todoItem: TodoItem) {
            todoItem.checked = !todoItem.checked
          
            do { try managedObjectContext.save() }
            catch { print(error) }
        }
    }

This view fetches all of the TodoItem(s) that belong to the TodoList that was tapped. This is where the problem is occurring. I'm not sure if it is because of my use of `init()` here, but there is a bug. When you first enter this view, you can tap a todo item in order to "check" it and the changes show up in the view immediately. However, when you navigate to a different TodoItemView for a different TodoList and back, the views no longer update when tapped. The checkmark image does not show up, and you need to leave that view and then re-enter it in order for said changes to actually appear.


`TodoItemAdd`:


    struct TodoItemAdd: View {
        @Environment(\.presentationMode) var presentationMode
        @Environment(\.managedObjectContext) var managedObjectContext
        var todoList: TodoList
        @State var todoItemTitle: String = ""
      
        var body: some View {
            NavigationView {
                Form {
                    TextField("Title", text: $todoItemTitle)
                  
                    Button(action: {
                        self.saveTodoItem()
                        self.presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Save")
                    })
                  
                    Button(action: {
                        self.presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Cancel")
                    })
                }
                .navigationBarTitle("Add Todo Item")
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
      
        func saveTodoItem() {
            let todoItem = TodoItem(context: managedObjectContext)
            todoItem.title = todoItemTitle
            todoItem.todoList = todoList
          
            do { try managedObjectContext.save() }
            catch { print(error) }
        }
    }


This simply allows the user to add a new todo item.


As I mentioned above, the views stop updating automatically when you leave and re-enter the TodoItemView. Here is a recording of this behaviour:


https://i.imgur.com/q3ceNb1.mp4


What exactly am I doing wrong here? If I'm not supposed to use `init()` because views in navigation links are initialized before they even appear, then what is the proper implementation?

Replies

I'll have to dig some more into this to get proper answers for you, but using `init()` is absolutely the right way to do this. FetchRequest is a DynamicProperty, so it implements a method named `update()` which is called before rendering an associated view `body`. Its initializer just copies the `NSFetchRequest` and `Transaction` (if supplied), arguments, then all the real work—including resource allocation—happens either in `update()` or when you access the `wrappedValue`.


I did some spelunking into how it was implemented and wrote a very slightly modified version which allows me to swap out the NSFetchRequest at will. The code is in my AQUI project on Github.