1 Reply
      Latest reply on Dec 2, 2019 9:15 AM by Jim Dovey
      kdion4891 Level 1 Level 1 (0 points)

        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?

        • Re: FetchedResult views do not update after navigating away from view & back
          Jim Dovey Level 3 Level 3 (210 points)

          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.