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?