3
votes

I have a Lesson entity stored in Core Data one of whose variables is to store whether the lesson is complete.

The lessons are listed in a SwiftUI list and when selected go to the View where the game exists. Once the game is completed, the complete variable is updated to true. And what is supposed to happen is that the list View displays the listed game with a check mark beside the game.

However, what is happening is that when I save the 'completed' state of the lesson in the game (in the tapRight method - see below), I get the warning:

[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window).

And then when I return to the list View by pushing the navigation button at the top of the game View, I find the game has disappeared from the list.

However, when I close the app and reopen it, the list is there with the row for the game and the checkmark, so I know the core data lesson instance is being updated correctly. Code for List view below.

import SwiftUI
import CoreData
import UIKit

struct LessonList: View {

@Environment(\.managedObjectContext) var moc

@State private var refreshing = false
private var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)

@FetchRequest(entity: Lesson.entity(), sortDescriptors: [], predicate: NSPredicate(format: "(type == %@) AND (stage == %@) AND ((complete == %@) OR (complete == %@))", "phonicIntro", "1", NSNumber(value: true), NSNumber(value: false) )) var phonicIntroLessons1: FetchedResults<Lesson>

var body: some View {

    NavigationView {      
        List {
            self.stage1Section
        }
        .navigationBarTitle("Lessons")
    }
}


private var phonicIntroLink : some View {
    ForEach(phonicIntroLessons1) { lesson in
        NavigationLink(destination: PhonicIntroGame(lesson: lesson)) {
            LessonRow(lesson: lesson)
        }
    }
}

private var stage1Section : some View {
    Section(header: Text("Stage 1")) {
        phonicIntroLink
    }.onReceive(self.didSave) { _ in
        self.refreshing.toggle()
    }
}

Relevant code in game View for saving completed status:

import SwiftUI
import AVFoundation

struct PhonicIntroGame: View {

@Environment(\.managedObjectContext) var moc

var lesson: Lesson?

func tapRight() {
    if ((self.lesson?.phonicsArray.count ?? 1) - 1) > self.index {
        self.index = self.index + 1
        print("This is index \(self.index)")
        //On completion
        if (index + 1)  == (lesson!.phonicsArray.count) {
            self.lessonComplete()
        }
    } else {
        print(self.index)

func lessonComplete() {
    self.lesson?.setComplete(true)
    self.saveLesson()
}

func saveLesson() {
    do {
        try moc.save()
    } catch {
        print("Error saving context \(error)")
    }
}

In the NSManagedObject Subclass:

extension Lesson {

func setComplete (_ state: Bool) {
    objectWillChange.send()
    self.complete = state
}
}

The code for the lessonRow is as follows:

import SwiftUI
import CoreData

struct LessonRow: View {

@Environment(\.managedObjectContext) var moc
@State var refreshing1 = false
var didSave =  NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
var lesson: Lesson

var body: some View {
        HStack {
            Image(lesson.wrappedLessonImage )
                .resizable()
                .frame(width: 80, height: 80)
                .cornerRadius(20)

            Text(lesson.wrappedTitle)
                .font(.system(size: 20))
                .fontWeight(.thin)
                .padding()

            if lesson.complete == true {
                Image(systemName: "checkmark.circle")
                    .resizable()
                    .frame(width: 30, height: 30)
                    .foregroundColor(.green)
            } else {
                Rectangle()
                    .frame(width: 30, height: 30)
                    .foregroundColor(.clear)
            }
        }.padding()
            .onReceive(self.didSave) { _ in
                self.refreshing1.toggle()
            }


   }
}

The things I have tried:

  1. Forcing the list to reload with notifications and .onReceive
  2. Setting the Lesson NSManagedObject subclass with setComplete function, which calls 'objectWillChange'
  3. Making the @FetchRequest more detailed by including both true and false for the complete variable

I'd be grateful for any suggestions on how to resolve this. I'm new to SwiftUI so thanks in advance for all your help.

2
Your code doesn't compile. Provide minimal code that shows the issue and can be compiled and run. That way it's easier to provide answers.Felix Marianayagam
Thanks for the response. The only other code that I think would be relevant is for the lessonRow which I have now added above. I'm trying to keep to only the relevant content, as to not to overload the question with too much information.Zenman C
Still your code doesn't compile. Create a new project and copy the code from this post and try compiling.Felix Marianayagam
were you able to fix this?rv7284
Yes, see below for how it was solved in the end. Had to put as an answer because cannot put two @ in comments apparently.Zenman C

2 Answers

4
votes

Yes, the way I solved this in the end was by adding @ObservedObject to the stored object in the PhonicIntroGame : View. As follows -

@ObservedObject var lesson: Lesson
0
votes

Core Data batch updates do not update the in-memory objects. You have to manually refresh afterwards.

Batch operations bypass the normal Core Data operations and operate directly on the underlying SQLite database (or whatever is backing your persistent store). They do this for benefits of speed but it means they also don't trigger all the stuff you get using normal fetch requests.

You need to do something like shown in Apple's Core Data Batch Programming Guide: Implementing Batch Updates - Updating Your Application After Execution

Original answer similar case similar case