Search code examples
swiftuicore-data

SwiftUI not updating View after changing attribute value of a relationship entity


I have two core data entities, a Deck and and Card. The Deck has a relationship with Card (one Deck to many Card). In SwiftUI after changing the attribute of Card in a different screen, the list of Card does not update its view to reflect the change.

struct DeckDetailView: View {
    @ObservedObject var deck: Deck
    @State private var showSheet : Bool = false
    @Environment(\.managedObjectContext) private var viewContext
    
    var body: some View {
        Form{
            ForEach(deck.cards?.allObjects as? [Card] ?? []){ card in
                NavigationLink {
                    CardDetailView(card: card) { returnedCard in
                        updateCard(card: returnedCard)
                    }
                } label: {
                    Text("Question: \(card.question)")
                }
            }
        }

        .navigationTitle("\(deck.name)")
        .toolbar{
            Button {
                showSheet.toggle()
            } label: {
                Label("Add Deck", systemImage: "plus")
            }
            .sheet(isPresented: $showSheet) {
                NavigationStack{
                    AddCardView { cardData in
                        addCard(cardData: cardData)
                    }
                }
            }
        }
    }
    private func updateCard(card: Card){
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }

    func addCard(cardData: CardSO){
        let card = Card(context: viewContext)
        card.id = UUID().uuidString
        card.question = cardData.question
        card.answer = cardData.answer
        deck.addToCards(card)
        
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
}

Solution

  • Excellent question! I have faced this same issue many times. One of the ways, I ended up solving the issue is by using @FetchRequest property wrapper. I would pass Deck to the detail screen but use the Deck object to perform a new FetchRequest to get all the cards. When you use @FetchRequest, it will automatically track the changes.

    I wrote a similar article on this issue that you can check out below: https://azamsharp.com/2023/01/30/active-record-pattern-swiftui-core-data.html. See the section "Creating, Updating and Reading Reminders".

    struct MyListDetailView: View {
        
        let myList: MyList
        
        @Environment(\.managedObjectContext) private var viewContext
        
        @FetchRequest(sortDescriptors: [])
        private var reminderResults: FetchedResults<Reminder>
        
        init(myList: MyList) {
            self.myList = myList
            _reminderResults = FetchRequest(fetchRequest: Reminder.byList(myList: myList))
        }
    
    extension Reminder: Model {
        static func byList(myList: MyList) -> NSFetchRequest<Reminder> {
            let request = Reminder.fetchRequest()
            request.sortDescriptors = []
            request.predicate = NSPredicate(format: "list = %@", myList)
            return request 
        }
    }