0

I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.

class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
    var container:NSPersistentContainer? = 
           (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer

   var frc : NSFetchedResultsController<Student>? 

   override func viewDidLoad() {
      container?.performBackgroundTask { context in 
          // adds 100 dummy records in background
          for i in 1...100 {
             let student = Student(context: context)
             student.name = "student \(i)"
          }
          try? context.save()   // this works because count is printed below
          if let count = try? context.count(for: Student.fetchRequest()) {
               print("Number of students in core data: \(count)")  // prints 100
          }
      }  // end of background inserting.

      // now defining frc:
      if let context = container?.viewContext {
          let request:NSFetchRequest<Student> = Student.fetchRequest()
          request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
          frc = NSFetchedResultsController<Student> (
               fetchRequest: request,
               managedObjectContext: context,
               sectionNameKeyPath: nil,
               cacheName: nil )

          try? frc?.performFetch()   // this works and I get no errors
          tableView.reloadData()
          frc.delegate = self
      }  // end of frc definition
   }
}

If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).

But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.

The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:

// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
    self?.tableView.reloadData()   // causes UI to behave weirdly 
}

then I tried to call viewContext.perform to reload the table view in the proper thread

func viewDidLoad() {

   // code for inserting 100 dummy rows in background thread

   // code for defining frc and setting self as delegate

   if let context = container?.viewContext {
      context.perform { [weak self] in
         self?.tableView.reloadData()    // that also causes UI to behave weirdly

      }
   }
}

How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?

Ahmad
  • 12,336
  • 6
  • 48
  • 88
  • Try setting the delegate before performing the fetch. – Jon Rose Apr 09 '18 at 06:55
  • @JonRose I tried that. The delegate methods don't fire if I set the delegate after the perform call. so, the refresh does not take place and the app is more buggier – Ahmad Apr 09 '18 at 13:30
  • @Ahmad What do you mean by "causes UI to behave weirdly"? – staticVoidMan Apr 11 '18 at 09:01
  • @staticVoidMan the animation becomes sluggish and delete button on table row does not trigger the delegate method commiteditingstyle – Ahmad Apr 11 '18 at 09:03
  • @Ahmad Can you create a debug repo and add it to your question? I think it will be faster if we can help you on a debug repo. – trungduc Apr 11 '18 at 10:53
  • @trungduc I am not familiar with creating debug repos. Is there a page I can read about? – Ahmad Apr 11 '18 at 11:04
  • @Ahmad Simply, you just need create a new project which contains problem on your question. After that, upload it to Github or somewhere you like. Finally, add url to the question, we can download it and help you. – trungduc Apr 11 '18 at 11:06
  • @trungduc sure. I will do that tonight. Thanks very much – Ahmad Apr 11 '18 at 11:07
  • You realize you are writing 100 to disk or so records while your table is being initialized and trying to read those 100 or so records you could still be writing (writing to disk takes time!). I would profile the app and check the read/write speeds. Ideally you won't have users adding 100+ records to a database while the view is being loaded. – NSGangster Apr 11 '18 at 15:51
  • @NSGangster The code I posted is just to demonstrate my issue briefly. Ideally, users will tap a button to trigger adding 100 dummy rows. Then the app should trigger the tableview/frc to read the inserted rows right after the context is successfully saved. Even if i split the saving to a single event, and reloading to a separate button tap, that also doesn’t work. I will upload my project file soon – Ahmad Apr 11 '18 at 15:57

3 Answers3

1
override func viewDidLoad() {
    super.viewDidLoad()

    //Always need your delegate for the UI to be set before calling the UI's delegate functions.
    frc.delegate = self

    //First we can grab any already stored values.
    goFetch()

    //This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
    container?.performBackgroundTask { context in
        //We are in a different queue than the main queue, hence "backgroundTask".
        for i in 1...100 {
            let student = Student(context: context)
            student.name = "student \(i)"
        }
        try? context.save()   // this works because count is printed below
        if let count = try? context.count(for: Student.fetchRequest()) {
            print("Number of students in core data: \(count)")  // prints 100
        }
        //Now that we are done saving its ok to fetch again.

        goFetch()

    }

    //goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
    //The reason it works if you restart the app because that data you didn't let finish saving is persisted
    //So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.

}


func goFetch() {

    if let context = container?.viewContext {
        let request:NSFetchRequest<Student> = Student.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        frc = NSFetchedResultsController<Student> (
            fetchRequest: request,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil )

        try? frc?.performFetch()

        //Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
        //To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
        DispatchQueue.main.async { [weak self] in
            self?.tableView.reloadData()
        }
    }
}
NSGangster
  • 2,397
  • 12
  • 22
1

After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.

My code is slightly different since I used a project I had for this. Anyway here is what I did:

In my view controller I had a property for my container

private var persistentContainer = NSPersistentContainer(name: coreDataModelName)

And in viewDidLoad I loaded the persistent store and created my 100 records.

persistentContainer.loadPersistentStores { persistentStoreDescription, error in
  if let error = error {
    print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
  } else {
    self.createFakeNotes() // Here 100 elements get created
    DispatchQueue.main.async {
      self.setupView() // other stuff, not relevant
      self.fetchNotes() // fetch using fetch result controller
      self.tableView.reloadData()
    }
  }
}

Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer

I also delete old notes first to make the testing easier.

private func createFakeNotes() {
  let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
  do {
    try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
  } catch {
    print("Delete error [\(error)]")
    return
  }

  let privateContext = persistentContainer.newBackgroundContext()
  privateContext.automaticallyMergesChangesFromParent = true //Important!!!

  privateContext.perform {
    let createDate = Date()
    for i in 1...100 {
      let note = Note(context: privateContext)
      note.title = String(format: "Title %2d", i)
      note.contents = "Content"
      note.createdAt = createDate
      note.updatedAt = createDate
    }
    do {
      try privateContext.save()
      do {
        try self.persistentContainer.viewContext.save()
      } catch {
        print("Fail saving main context [\(error.localizedDescription)")
      }
    } catch {
      print("Fail saving private context [\(error.localizedDescription)")
  }
}

}

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Hi @joakim, i have used above method, but its crash the app. i have insert and fetch data same time when the response is receive my app performance is slow. what we do? – Iyyappan Ravi Aug 18 '18 at 06:57
  • Joakim, thanks for the great tip with automaticallyMergesChangesFromParent – Fattie Feb 04 '20 at 21:15
0

You should fetch your data by calling it from viewwillappear and then try to reload your tableview.

override func viewWillAppear(_ animated: Bool) {
    getdata()
    tableView.reloadData()
}
 func getdata() {
    let context =  (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    do{
    persons = try context.fetch(Person.fetchRequest()) 
    }
    catch {
        print("fetching failed")
    }
}
Parth Barot
  • 355
  • 3
  • 9