Apologies
You must be signed in to watch this lesson.
Auto UI Updates NSFetchedResultsController
Intermediate Training Core Data
Having implemented so many different features with Core Data such as creating, updating, and deleting objects in our app, you should be familiar with how difficult it can be in term so updating our UI to reflect these changes. Today we look at a very easy to use component called NSFetchedResultsController that helps us monitor for changes. In addition to monitoring, it will also give us the correct IndexPath objects for which these changes are occurring inside a UITableView and UICollectionView. Please look at the link in the resources for more information on how this awesome component works.

Comments (14)
Muriel
6 years ago
Hi Brian, Thanks for the amazing course. I have a question. When using NSFetchedResultsController the trick to toggle the height of heightForFooterInSection to display a “no rows message” no longer works because when the Object is empty the section also doesn't exist. Is there a way to solve this?
Bugger
4 years ago
I found the same problem. Can you find the way to solve it?
Muriel
4 years ago
I solved it like this: override func numberOfSections(in tableView: UITableView) -> Int { let countSections = fetchedResultsController.sections?.count ?? 0 if countSections == 0 { tableView.backgroundView = noRowsView } else { tableView.backgroundView = nil } return countSections }
Paul Dong
6 years ago
HI Brain, When I modify companies' name, such as from 'A' to 'Z', the row of the company did move to the bottom, but the name keeps no change. What I found is that the new name did update to Core Data, but somehow the NSFetchedResultsController treads this change as '.move' only, so I have to add 'tableView.reloadData()` at the end of '.move' case, like below: ``` func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { print("type: \(type.rawValue) indexPath: \(indexPath!) newIndexPath: \(newIndexPath!)") switch type { case .insert: tableView.insertRows(at: [newIndexPath!], with: .fade) case .delete: tableView.deleteRows(at: [indexPath!], with: .fade) case .update: tableView.reloadRows(at: [indexPath!], with: .fade) case .move: tableView.moveRow(at: indexPath!, to: newIndexPath!) tableView.reloadData() } } ``` I am sure this is not a proper fix, what is your suggestion? Paul
Paul Dong
6 years ago
Hi Brain, Sorry for having many questions. I spent quite long time to figure out how to save the company photos. It does work now, but I am not sure if it is efficient. Basically, my idea is to loop through jsonCompanies, downloading imageData as first step, and then put everything inside main thread. Inside main thread, memerise how many companies have been proceessed and onee it equal to number of jsonCompany inside jsonCompanies, do context.save(). the `downloadCompaniesFromServer` should be inside `Service.swift` ``` func downloadCompaniesFromServer(){ print("downloadCompaniesFromServer") let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = CoreDataManager.shared.persistenContainer.viewContext if let url = URL(string: urlString) { URLSession.shared.dataTask(with: url) { (data, response, error) in if let err = error { print("Failed to download companies", err) return } guard let data = data else { return } let jsonDecoder = JSONDecoder() do { let jsonCompanies = try jsonDecoder.decode([JSONCompany].self, from: data) let numberOfCompanies = jsonCompanies.count var processed = 0 jsonCompanies.forEach({ (jsonCompany) in print(jsonCompany.name) if let url = URL(string: jsonCompany.photoUrl) { URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in if let err = error { print(err) return } DispatchQueue.main.async { let company = Company(context: privateContext) company.name = jsonCompany.name print("get photo url for \(company.name!)") company.founded = Date.get(dateString: jsonCompany.founded, format: "MM/dd/yyyy") company.imageData = data if let nsData = data as NSData? { CompanyCell.imageCache.setObject(nsData, forKey: company.name as NSString!) print("save imageData for \(company.name!)") } jsonCompany.employees?.forEach({ (jsonEmployee) in print(" - \(jsonEmployee.name)") let employee = Employee(context: privateContext) employee.name = jsonEmployee.name employee.type = jsonEmployee.type employee.detail = EmployeeInformation(context: privateContext) employee.detail?.birthday = Date.get(dateString: jsonEmployee.birthday, format: "MM/dd/yyyy") employee.company = company }) processed += 1 if processed >= numberOfCompanies { do { try privateContext.save() try privateContext.parent?.save() print("context saved") }catch let err { print(err) } } } }).resume() } }) }catch let err { print(err) } }.resume() } } ```
tobitech
5 years ago
Hello Brian, Please what's the most effective way to save unique objects in Core Data, or most effective way to check if an object already exists of not. My first solution was to perform a fetch request and check against the result when I want to add a new object, if it exists, i don't save, else save, how effective is this?
dclawson
5 years ago
Not Brian, but I think you're wanting to make sure BMW or any other company doesn't get added twice. My guess would be that you'd perform the fetch request in the CreateCompanyController, and then throw some kind of "already exists" alert (like the form validation from the birthday) based on if the name matched the name of an existing object.
Gabriel
5 years ago
I used this do { context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy try context.save() print("saved") } it works really well for me.
Gabriel
5 years ago
So we spend time cleaning the code into helper files now its all back in a vc. can we still put the tableview stuff into an extension?
Brian Voong
5 years ago
Gabriel
5 years ago
Well it doesn't work for me putting it into an extension - i get error message saying it doesn't recognise the tableview... for any of the override table functions
Gabriel
5 years ago
Brian I am at a loss to follow this. Why aren't the edit and delete buttons in the table row as swipe action. What has happened to the insert new company view controller. Very disappointed.
Brian Voong
5 years ago
Gabriel
5 years ago
Don't you think I have already tried that. In addition, a I mentioned before, putting the tableview functions into an extension causes problems. with tableview not being recognised.
Brian Voong
5 years ago
Gabriel
5 years ago
Use of undeclared type 'UITableView' . this is the error in addition to Use of undeclared type 'CGFloat'; did you mean to use 'CGFloat'? for tableheader
yongzhan
5 years ago
// edit and delete override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { let context = CoreDataManager.shared.persistentContainer.viewContext let destructiveAction = UITableViewRowAction(style: .destructive, title: "Delete", handler: { (_, indexPath) in print("delete item") let company = self.fetchResultController.object(at: indexPath) context.delete(company) do { try context.save() } catch let err { print("delete company err \(err)") } }) let editAction = UITableViewRowAction(style: .default, title: "Edit", handler: handlerEditAction) editAction.backgroundColor = .darkBlue return [destructiveAction, editAction] } func handlerEditAction(_: UITableViewRowAction, indexPath: IndexPath){ print("indexPath", indexPath) let editCompanyController = CreateCompanyController() editCompanyController.delegate = self editCompanyController.company = self.fetchResultController.object(at: indexPath) present(CustomNavigationController(rootViewController: editCompanyController), animated: true) }
Gabriel
5 years ago
Thank you.Appreciated.
Paweł Liczmański
5 years ago
Hi where i can find link to code that you copy and paste to CompaniesAutoUpdateController.swift ?
Brian Voong
5 years ago
Paweł Liczmański
5 years ago
Thank you Brian, btw. im so happy that im found your videos. You are great teacher, I like a lot that first you make something in one way and show us that its wrong and why, then you refactor code and explain everything one more time. Im watching your movies on speakers and my wife agree with me that you have a very relaxing voice. Thanks one more time.
Haohong Zhao
5 years ago
Hi Brian, Since the EmployeesController is similar to CompaniesController, is it possible to use FRC in EmployeesController as well? Are there any specific difficulties in doing so?
Brian Voong
5 years ago
emiejagz
5 years ago
Hi Brian, This is my first question, I really hope you can help. I love the auto UI updates but please am currently using it with collectionview using perform batch updates. Please is there a way to paginate safely without hitting cellForItemAt and executing my logic for paginating: if indexPath.item == (fetchResultsController.fetchedObjects?.count)! - 1 && !isfinishedPaging Or is there a way to know when the blockoperations is done with all batches or the sections it ran: collectionView?.performBatchUpdates({ for operation in self.blockOperations{ operation.start() } }, completion: { (isComplete) in //not this completion, as this runs for every individual updates Your response will be greatly appreciated. Thanks. })
ravikanth.marri@gmail.com
5 years ago
Great Lesson , Thanks.
Brian Voong
5 years ago
Michael DeBoisblanc
5 years ago
Your videos don't reliably play, sometimes getting stuck in a never ending buffering state.
Cinquain
5 years ago
Man... this video took me three days to get through. Thats the longest I ever took for any of Brian's videos. Good stuff tho! Keeping pumping that fire.
Brian Voong
5 years ago
Asaad Mirza
4 years ago
Hi brian, I am using fetchlimit with NSFetchedResultsController but the issue is displaying like it in the collectionview 5 4 3 2 1 and i need it in reverse 1 2 3 4 5 What is the solution without using nssortdescriptor because i already used it but not solve
Brian Voong
4 years ago
Asaad Mirza
4 years ago
I am not using array , i am using NSFetchedResultsController direct so is any solution to reverse the result
Iulian David
4 years ago
You are by default using array , see: fetchedResultsController.objects
archid04
4 years ago
Great video Brian! You are really the messiah of iOS development. Keep it coming. I'm sure one day your educational venture is going to blow up to be the best in iOS programming. There is honestly no one who does the job you do.
HELP & SUPPORT