Im getting user's input and creating CoreData entity for it, and put in to the tableView, but before i need to designate some properties for entity by making a couple network requests. And there is a problem. First entity is always nil (and create a empty cell for table view) but if i save context and open app again - here it is, entity is ok. If i create second (and so on) entity it works fine. Its also seems like table always create am empty cell for start and then delete it and create proper cell…
I cant get why completion handlers doesn't work properly and network requests makes asynchronously. Please help me to understand.
Creation metod starts in textFieldShouldReturn
import UIKit
import CoreData
class ViewController: UIViewController, UITextFieldDelegate {
var managedObjectContext: NSManagedObjectContext?
lazy var fetchedResultsController: NSFetchedResultsController<Word> = {
let fetchRequest: NSFetchRequest<Word> = Word.fetchRequest()
let createdSort = NSSortDescriptor(key: "created", ascending: false)
let idSort = NSSortDescriptor(key: "id", ascending: false)
fetchRequest.sortDescriptors = [createdSort, idSort]
var fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: AppDelegate.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
private var hasWords: Bool {
guard let fetchedObjects = fetchedResultsController.fetchedObjects else { return false }
return fetchedObjects.count > 0
}
let creator = WordManager.shared
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
managedObjectContext = AppDelegate.viewContext
fetchWords()
updateView()
}
func updateView() {
tableView.isHidden = !hasWords
messageLabel.isHidden = hasWords
}
............
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let managedObjectContext = managedObjectContext else { return false }
guard let title = textField.text, !title.isEmpty, !title.hasPrefix(" ") else {
showAllert(title: "Warning!", message: "Title may not to be nil!")
return false
}
let word = Word(context: managedObjectContext)
guard let fetchedWordsCount = fetchedResultsController.fetchedObjects?.count else { return false }
creator.createWord(title: title, counter: fetchedWordsCount, word: word)
self.updateView()
textField.text = nil
return true
}
func fetchWords() {
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Cant fetch words. Error \(error.localizedDescription)")
}
}
}
Creating entity
import CoreData
import UIKit
class WordManager {
static var shared = WordManager()
private init() {}
private var api = DictionaryAPI()
private var api2 = MeaningAPI()
func createWord(title: String, counter: Int, word: Word) {
self.api.fetch(word: title, completion: { translation in
self.api2.fetchTranscription(word: title, completion: { transcription in
word.id = Int16(counter) + 1
word.title = title
word.translation = translation
word.transcription = "[\(transcription)]"
word.created = NSDate()
})
})
}
}
Nothing interesting in tableView
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchedResultsController.sections else { return 0 }
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = fetchedResultsController.sections?[section] else { return 0 }
return section.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath) as! MainCell
let word = fetchedResultsController.object(at: indexPath)
cell.configure(with: word)
return cell
}
}
Configure cell method
func configure(with word: Word) {
titleField.text = word.title
translationField.text = word.translation
transcriptionLabel.text = word.transcription
totalCounter.text = "\(word.showCounter)"
correctCounter.text = "\(word.correctCounter)"
wrongCounter.text = "\(word.wrongCounter)"
}
Fetched Results Delegate
extension ViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
updateView()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
case .insert:
guard let indexPath = newIndexPath else { return }
tableView.insertRows(at: [indexPath], with: .fade)
case .move:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
guard let newIndexPath = newIndexPath else { return }
tableView.insertRows(at: [newIndexPath], with: .fade)
default:
break
}
}
}
Problem:
The data you have in core data is not in a state that is ready to be shown in the app.
The NSFetchedResultsController just happens to pick this data and shows it in the app.
Approach:
When a user creates a records, the user wants to see the changes immediately in the app and shouldn't have to wait for the app to update in the server to see the changes. So a user would still be able to create records even when there is no network connectivity.
Create a child context from view context
Use a background thread to upload to the server and when update records on to the child context.
Save child context only when the data is in a consistent state.
NSFetchedResultsController should be using the view context so would be unaware of the child context records.
When you save the child context, the NSFetchedResultsController would pick these records.
Related
My application has two tab bars. The first one presents a list of games added on view controller and save them on the core data database. Switching on the second tab/view reads from the database and presents it inside a table view. I implemented the NSFetchedResultsControllerDelegate with a fetch method. When I add the first item to the context on the first tab and switch to second tab, FRC delegate methods (controllerWillChangeContent(_:), controller(_:didChange:at:for:newIndexPath:), controllerDidChangeContent(_:)) are not getting called and the table view is empty while I can see arrayOfGamesCount = 1. But when I add a second item, I can see all FRC delegate methods are getting call when I switch to second tab bar. And TableView display one rows while arrayOfGamesCount = 2
The first tab bar have 2 view controllers.(AddGameViewController and WelcomeViewController) AddGameVC is used to grab data from textfields and send it to welcomeVC.
import UIKit
import CoreData
class WelcomeViewController: UIViewController,SendGameDataDelegate, UIAdaptivePresentationControllerDelegate {
var games : [Game]? = []
var gamesMo: [GameMo]? = []
var gamed: GameMo?
var game : Game?
func ShouldSendGame(game: Game) {
self.game = game
print("\(game)")
games?.append(game)
}
#IBAction func endWLButton(_ sender: UIButton) {
saveDataToCoreData()
print("number of games from gamesMoCount is \(gamesMo?.count ?? 0)")
games?.removeAll()
reloadCollectionViewData()
}
func saveDataToCoreData (){
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
gamed = GameMo(context: appDelegate.persistentContainer.viewContext)
if games != nil {
for game in games! {
gamed?.goal = Int32(game.goal ?? 0 )
gamed?.rivalGoal = Int32(game.rivalGoal ?? 0)
gamed?.shot = Int32(game.shots ?? 0)
gamed?.rivalShot = Int32(game.rivalGoal ?? 0)
gamed?.rivalCorners = Int32(game.rivalsCorner ?? 0)
gamed?.corners = Int32(game.corners ?? 0)
gamesMo?.append(gamed!)
}
print("Saving data to context ....")
appDelegate.saveContext()
}
}
}
}
extension WelcomeViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return games?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let gameIndex = games?[indexPath.row] {
let userGameScore = gameIndex.goal
let rivalGameScore = gameIndex.rivalGoal
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FormCell", for: indexPath) as? FormCollectionViewCell {
cell.setCell(userScores: userGameScore!, rivalScores: rivalGameScore! )
return cell
}
}
return UICollectionViewCell ()
}
}
The second tab bar have only one VC: AllWLeagueController used to display items from the the database.
import UIKit
import CoreData
class AllWLeagueController : UITableViewController {
var fetchRequestController : NSFetchedResultsController<GameMo>!
var arrayOfGamesModel : [[GameMo]]? = []
var gameMo: GameMo?
var gamesMo: [GameMo] = []
override func viewDidLoad() {
validation(object: arrayOfGamesModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchRequest()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("arrayOfGamesModelcount est \(arrayOfGamesModel?.count ?? 0)")
return arrayOfGamesModel?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let weekL = arrayOfGamesModel?[indexPath.row] {
if let cell = tableView.dequeueReusableCell(withIdentifier: "WL") as? AllWLeaguesTableViewCell {
let winCounts = WLManager.winCountMethod(from: weekL)
let lossCounts = WLManager.lossCountMethod(from:weekL)
cell.setOulet(win: winCounts, loss: lossCounts, rankName: rankString)
cellLayer(with: cell)
return cell
}
}
}
extension AllWLeagueController: NSFetchedResultsControllerDelegate {
func fetchRequest () {
let fetchRequest = NSFetchRequest<GameMo>(entityName: "Game")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "win", ascending: true)]
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate){
let context = appDelegate.persistentContainer.viewContext
// fetch result controller
fetchRequestController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchRequestController.delegate = self
do{
try fetchRequestController.performFetch()
if let fetchedObjects = fetchRequestController.fetchedObjects {
gamesMo = fetchedObjects
print("Fetech Request Activated")
print(gamesMo)
}
}catch{
fatalError("Failed to fetch entities: \(error)")
}
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView beginupdates")
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let newIndexPath = newIndexPath {
print("insert")
tableView.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath {
print("delete")
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath {
print("update")
tableView.reloadRows(at: [indexPath], with: .fade)
}
default:
tableView.reloadData()
}
if let fetchedObjects = controller.fetchedObjects {
gamesMo = fetchedObjects as! [GameMo]
print("we are about to append arrayOfGamesModel")
arrayOfGamesModel?.append(gamesMo)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
print("TableView endupdates")
tableView.endUpdates()
}
}
You are making a fatal mistake. In saveDataToCoreData only one instance is created and then it's being overwritten with the game data in each iteration of the array. So your array gamesMo may contain multiple items but it's always the same instance and only one instance is saved into the context.
Replace saveDataToCoreData with
func saveDataToCoreData (){
let appDelegate = UIApplication.shared.delegate as! AppDelegate
guard let games = games else { return }
for game in games {
let newGame = GameMo(context: appDelegate.persistentContainer.viewContext)
newGame.goal = Int32(game.goal ?? 0 )
newGame.rivalGoal = Int32(game.rivalGoal ?? 0)
newGame.shot = Int32(game.shots ?? 0)
newGame.rivalShot = Int32(game.rivalGoal ?? 0)
newGame.rivalCorners = Int32(game.rivalsCorner ?? 0)
newGame.corners = Int32(game.corners ?? 0)
gamesMo.append(newGame)
}
print("Saving data to context ....")
appDelegate.saveContext()
}
Another bad practice is to create new fetch results controllers in viewWillAppear. It's highly recommended to create one controller as lazy instantiated property – as well as the managed object context – for example
lazy var context : NSManagedObjectContext = {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}()
lazy var fetchRequestController : NSFetchedResultsController<GameMo> = {
let fetchRequest = NSFetchRequest<GameMo>(entityName: "Game")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "win", ascending: true)]
// fetch result controller
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
do {
try frc.performFetch()
if let fetchedObjects = frc.fetchedObjects {
self.gamesMo = fetchedObjects
print("Fetech Request Activated")
print(gamesMo)
}
} catch{
fatalError("Failed to fetch entities: \(error)")
}
return frc
}()
Force unwrapping AppDelegate is perfectly fine. Your app won't even launch if AppDelegate was missing.
I recommend also to use less ambiguous variable names. games, gamesMo, gamed and game look very similar and can cause confusion.
I would like to save this kind of arrays with Core Data:
let crypto1 = Cryptos(name: "Bitcoin", code: "bitcoin", symbol: "BTC", placeholder: "BTC Amount", amount: "0.0")
let crypto2 = Cryptos(name: "Bitcoin Cash", code: "bitcoinCash", symbol: "BCH", placeholder: "BCH Amount", amount: "0.0")
Is that even possible?
I know I can create an array to save like that...
let name = "Bitcoin"
let code = "bitcoin"
let symbol = "BTC"
let placeholder = "BTC Amount"
let amount = "0.0"
let cryptos = CryptoArray(context: PersistenceService.context)
cryptos.name = name
cryptos.code = code
cryptos.symbol = symbol
cryptos.placeholder = placeholder
cryptos.amount = amount
crypto.append(cryptos)
PersistenceService.saveContext()
...but this seems pretty inconvenient when a theoretical infinite number of arrays will be created by the user.
What would be the best way for me to save data, load it, edit it and delete it?
This is a question for a tutorial rather than a straight forward answer. I suggest you give some time to read about CoreData. Having said that, your question sounds generic, "Saving array to CoreData in Swift", so I guess it doesn't hurt to explain a simple implementation step by step:
Step 1: Create your model file (.xcdatamodeld)
In Xcode, file - new - file - iOS and choose Data Model
Step 2: Add entities
Select the file in Xcode, find and click on Add Entity, name your entity (CryptosMO to follow along), click on Add Attribute and add the fields you like to store. (name, code, symbol... all of type String in this case). I'll ignore everything else but name for ease.
Step 3 Generate Object representation of those entities (NSManagedObject)
In Xcode, Editor - Create NSManagedObject subclass and follow the steps.
Step 4 Lets create a clone of this subclass
NSManagedObject is not thread-safe so lets create a struct that can be passed around safely:
struct Cryptos {
var reference: NSManagedObjectID! // ID on the other-hand is thread safe.
var name: String // and the rest of your properties
}
Step 5: CoreDataStore
Lets create a store that gives us access to NSManagedObjectContexts:
class Store {
private init() {}
private static let shared: Store = Store()
lazy var container: NSPersistentContainer = {
// The name of your .xcdatamodeld file.
guard let url = Bundle().url(forResource: "ModelFile", withExtension: "momd") else {
fatalError("Create the .xcdatamodeld file with the correct name !!!")
// If you're setting up this container in a different bundle than the app,
// Use Bundle(for: Store.self) assuming `CoreDataStore` is in that bundle.
}
let container = NSPersistentContainer(name: "ModelFile")
container.loadPersistentStores { _, _ in }
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
// MARK: APIs
/// For main queue use only, simple rule is don't access it from any queue other than main!!!
static var viewContext: NSManagedObjectContext { return shared.container.viewContext }
/// Context for use in background.
static var newContext: NSManagedObjectContext { return shared.container.newBackgroundContext() }
}
Store sets up a persistent container using your .xcdatamodeld file.
Step 6: Data source to fetch these entities
Core Data comes with NSFetchedResultsController to fetch entities from a context that allows extensive configuration, here is a simple implementation of a data source support using this controller.
class CryptosDataSource {
let controller: NSFetchedResultsController<NSFetchRequestResult>
let request: NSFetchRequest<NSFetchRequestResult> = CryptosMO.fetchRequest()
let defaultSort: NSSortDescriptor = NSSortDescriptor(key: #keyPath(CryptosMO.name), ascending: false)
init(context: NSManagedObjectContext, sortDescriptors: [NSSortDescriptor] = []) {
var sort: [NSSortDescriptor] = sortDescriptors
if sort.isEmpty { sort = [defaultSort] }
request.sortDescriptors = sort
controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}
// MARK: DataSource APIs
func fetch(completion: ((Result) -> ())?) {
do {
try controller.performFetch()
completion?(.success)
} catch let error {
completion?(.fail(error))
}
}
var count: Int { return controller.fetchedObjects?.count ?? 0 }
func anyCryptos(at indexPath: IndexPath) -> Cryptos {
let c: CryptosMO = controller.object(at: indexPath) as! CryptosMO
return Cryptos(reference: c.objectID, name: c.name)
}
}
All we need from an instance of this class is, number of objects, count and item at a given indexPath. Note that the data source returns the struct Cryptos and not an instance of NSManagedObject.
Step 7: APIs for add, edit and delete
Lets add this apis as an extension to NSManagedObjectContext:
But before that, these actions may succeed or fail so lets create an enum to reflect that:
enum Result {
case success, fail(Error)
}
The APIs:
extension NSManagedObjectContext {
// MARK: Load data
var dataSource: CryptosDataSource { return CryptosDataSource(context: self) }
// MARK: Data manupulation
func add(cryptos: Cryptos, completion: ((Result) -> ())?) {
perform {
let entity: CryptosMO = CryptosMO(context: self)
entity.name = cryptos.name
self.save(completion: completion)
}
}
func edit(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
entity?.name = cryptos.name
self.save(completion: completion)
}
}
func delete(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
self.delete(entity!)
self.save(completion: completion)
}
}
func save(completion: ((Result) -> ())?) {
do {
try self.save()
completion?(.success)
} catch let error {
self.rollback()
completion?(.fail(error))
}
}
}
Step 8: Last step, use case
To fetch the stored data in main queue, use Store.viewContext.dataSource.
To add, edit or delete an item, decide if you'd like to do on main queue using viewContext, or from any arbitrary queue (even main queue) using newContext or a temporary background context provided by Store container using Store.container.performInBackground... which will expose a context.
e.g. adding a cryptos:
let cryptos: Cryptos = Cryptos(reference: nil, name: "SomeName")
Store.viewContext.add(cryptos: cryptos) { result in
switch result {
case .fail(let error): print("Error: ", error)
case .success: print("Saved successfully")
}
}
Simple UITableViewController that uses the cryptos data source:
class ViewController: UITableViewController {
let dataSource: CryptosDataSource = Store.viewContext.dataSource
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "YourCellId", for: indexPath)
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cryptos: Cryptos = dataSource.anyCryptos(at: indexPath)
// TODO: Configure your cell with cryptos values.
}
}
You cannot save arrays directly with CoreData, but you can create a function to store each object of an array. With CoreStore the whole process is quite simple:
let dataStack: DataStack = {
let dataStack = DataStack(xcodeModelName: "ModelName")
do {
try dataStack.addStorageAndWait()
} catch let error {
print("Cannot set up database storage: \(error)")
}
return dataStack
}()
func addCrypto(name: String, code: String, symbol: String, placeholder: String, amount: Double) {
dataStack.perform(asynchronous: { transaction in
let crypto = transaction.create(Into<Crypto>())
crypto.name = name
crypto.code = code
crypto.symbol = symbol
crypto.placeholder = placeholder
crypto.amount = amount
}, completion: { _ in })
}
You can show the objects within a UITableViewController. CoreStore is able to automatically update the table whenever database objects are added, removed or updated:
class CryptoTableViewController: UITableViewController {
let monitor = dataStack.monitorList(From<Crypto>(), OrderBy(.ascending("name")), Tweak({ fetchRequest in
fetchRequest.fetchBatchSize = 20
}))
override func viewDidLoad() {
super.viewDidLoad()
// Register self as observer to monitor
self.monitor.addObserver(self)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.monitor.numberOfObjects()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CryptoTableViewCell", for: indexPath) as! CryptoTableViewCell
let crypto = self.monitor[(indexPath as NSIndexPath).row]
cell.update(crypto)
return cell
}
}
// MARK: - ListObjectObserver
extension CryptoTableViewController : ListObjectObserver {
// MARK: ListObserver
func listMonitorWillChange(_ monitor: ListMonitor<Crypto>) {
self.tableView.beginUpdates()
}
func listMonitorDidChange(_ monitor: ListMonitor<Crypto>) {
self.tableView.endUpdates()
}
func listMonitorWillRefetch(_ monitor: ListMonitor<Crypto>) {
}
func listMonitorDidRefetch(_ monitor: ListMonitor<Crypto>) {
self.tableView.reloadData()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<Crypto>, didInsertObject object: Switch, toIndexPath indexPath: IndexPath) {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didDeleteObject object: Switch, fromIndexPath indexPath: IndexPath) {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didUpdateObject object: Crypto, atIndexPath indexPath: IndexPath) {
if let cell = self.tableView.cellForRow(at: indexPath) as? CryptoTableViewCell {
cell.update(object)
}
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didMoveObject object: Switch, fromIndexPath: IndexPath, toIndexPath: IndexPath) {
self.tableView.deleteRows(at: [fromIndexPath], with: .automatic)
self.tableView.insertRows(at: [toIndexPath], with: .automatic)
}
}
Assuming that you have a CryptoTableViewCell with the function update registered to the CryptoTableViewController.
I've been playing with Core Data for the past 18 hours or so. I'm fetching data with NSFetchedResultsController and shows data with UITableView. Adding a new record and deleting the selected record aren't my problems.
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// MARK: - Instance variables
private let persistentContainer = NSPersistentContainer(name: "Profiles") // core data model file (.xcdatamodeld)
var managedObjectContext: NSManagedObjectContext?
// MARK: - IBOutlets
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// loading persistentContainer //
persistentContainer.loadPersistentStores { (persistentStoreDescription, error) in
if let error = error {
print("Unable to Load Persistent Store")
} else {
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
}
// notifications //
NotificationCenter.default.addObserver(self, selector: #selector(profileDidUpdate), name: NSNotification.Name(rawValue: "HomeViewControllerPictureDidSelect"), object: nil)
}
// MARK: - fetchedResultsController(controller with the entity)
fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Person> = {
// Create Fetch Request with Entity
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
// Configure Fetch Request
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastName", ascending: true)]
// Create Fetched Results Controller
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
// Configure Fetched Results Controller
fetchedResultsController.delegate = self
return fetchedResultsController
}()
// MARK: - fetchedResultsController
// MARK: - Notifications
#objc func profileDidUpdate(notification: NSNotification) {
let profile = notification.object as! Profile
let context = persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Person", in: context)
let newPerson = NSManagedObject(entity: entity!, insertInto: context)
newPerson.setValue(profile.uuid, forKey: "uuid") // uuid is used to make each record unique
newPerson.setValue(profile.firstName, forKey: "firstName")
newPerson.setValue(profile.lastName, forKey: "lastName")
newPerson.setValue(profile.age, forKey: "age")
newPerson.setValue(profile.pictData, forKey: "pictData")
do {
try context.save()
print("saved...")
} catch {
print("failed saving")
}
}
// MARK: - Notifications
}
extension HomeViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
break;
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
break;
default:
print("...")
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
}
}
Shown above, I create a new record from another view controller, which sends an object of a model (Profile) to the current view controller (HomeViewController). I don't have to reload the table thanks to NSFetchedResultsController.
The entity has several attributes (age, firstName, lastName, pictData, uuid). And I want to change the selected record in the list with two attributes: firstName and lastName. The uuid attribute is used to identify a specific record.
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBAction func editTapped(_ sender: UIButton) {
guard let indexPath = tableView.indexPathForSelectedRow else {
return
}
let selectedRow = indexPath.row
if selectedRow >= 0 {
editRecord(index: selectedRow)
}
}
func editRecord(index: Int) {
let indexPath = IndexPath(row: index, section: 0)
let person = fetchedResultsController.object(at: indexPath)
let uuid = person.uuid!
let context = self.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "uuid == %#", uuid)
do {
let result = try context.fetch(fetchRequest)
if (result.count > 0) {
let managedObject = result[0] as! NSManagedObject
managedObject.setValue("Donald", forKey: "firstName")
managedObject.setValue("Washington", forKey: "lastName")
try context.save()
print("Changes saved...")
}
} catch {
print("Failed")
}
}
}
Now, if I click on the edit button, the app won't update the list immediately. If I restart the app, I see changes. So how can I update the table with NSFetchedResultsController when I make changes to the selected record? Thanks.
Since you're using the NSFetchedResultsControllerDelegate, you need to handle (for your particular use case), the following cases for the NSFetchedResultsChangeType in your didChange method:
insert
delete
update
Your function should look something like this:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch (type) {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
break;
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
break;
case .update:
tableView.reloadRows(at: [indexPath], with: .automatic)
break;
default:
print("...")
}
}
I have a simple Todo app. in main screen I show the list of available tasks, every cell show a small UIView for category color, category name, title and date. in first launch all data shows correnct, but when I choose one of them for update and change the category with another category's color and save changes in CoreData, modal view(where I update data) disappear and NSFetchedResultController fetch new data to populate tableView, all data changes, but only color stay as before the update. NOTE: When window disappear, cell first show correct color as I updated, after second it changes to old one. I debugged the app, debugger show, that correct and new data fetched from CoreData and everything must render right, but UIView's background color is wrong.
this is my cell's code:
#IBOutlet weak var title: UILabel!
#IBOutlet weak var category: UILabel!
#IBOutlet weak var date: UILabel!
#IBOutlet weak var progress: UILabel!
#IBOutlet weak var categoryColorView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
}
var viewModel: TaskCellViewModel! {
didSet {
title.text = viewModel.title
category.text = viewModel.categoryName
date.text = convert()
progress.text = viewModel.isDone ? "Done" : "In Progress"
progress.textColor = viewModel.isDone ? UIColor.green : UIColor.red
categoryColorView.backgroundColor = Constants.colors[viewModel.categoryColor]!.color
}
}
and this is a cell:
EDIT:
this is save/update method, called from NewTaskViewModel:
func save(completion: #escaping (AppResult<Task>) -> Void) {
let context = CustomContext.shared.getContext
do {
if _task.id != "" {
let taskFetchRequest: NSFetchRequest<TaskEntity> = TaskEntity.fetchRequest()
taskFetchRequest.predicate = NSPredicate(format: "id == %#", _task.id)
let taskEntity = try context.fetch(taskFetchRequest).first!
taskEntity.title = task.title
taskEntity.date = task.date as NSDate?
taskEntity.content = task.content
taskEntity.isDone = task.isDone
let categoryFetchRequest: NSFetchRequest<CategoryEntity> = CategoryEntity.fetchRequest()
categoryFetchRequest.predicate = NSPredicate(format: "name == %#", _task.category.name)
taskEntity.category = try context.fetch(categoryFetchRequest).first!
} else {
let taskEntity = TaskEntity(context: context)
taskEntity.id = UUID().uuidString
taskEntity.title = task.title
taskEntity.date = task.date as NSDate?
taskEntity.content = task.content
taskEntity.isDone = task.isDone
let categoryFetchRequest: NSFetchRequest<CategoryEntity> = CategoryEntity.fetchRequest()
categoryFetchRequest.predicate = NSPredicate(format: "name == %#", _task.category.name)
taskEntity.category = try context.fetch(categoryFetchRequest).first!
}
try context.save()
completion(.success(task))
} catch {
completion(.error(error.localizedDescription))
}
}
this is the call of save/update from NewTaskViewController(modal view):
#IBAction func save(_ sender: UIBarButtonItem) {
switch viewModel.validation() {
case .success(_):
viewModel.save { result in
switch result {
case .success(_):
DispatchQueue.main.async { [unowned self] in
self.dismiss(animated: true, completion: nil)
}
case .error(let error):
DispatchQueue.main.async { [unowned self] in
self.errorAlert(withMessage: error)
}
}
}
case .error(let errorMsg):
DispatchQueue.main.async { [unowned self] in
self.errorAlert(withMessage: errorMsg)
}
}
}
tableView's cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.taskCell, for: indexPath) as! TaskCell
cell.viewModel = viewModel.getTaskCell(indexPath: indexPath)
return cell
}
getTaskCell:
func getTaskCell(indexPath: IndexPath) -> TaskCellViewModel {
let taskEntity = fetchedResultsController.object(at: indexPath)
return TaskCellViewModel(withTask: taskEntity.convertToModel())
}
taskEntity.convertToModel()
#NSManaged public var id: String?
#NSManaged public var content: String?
#NSManaged public var date: NSDate?
#NSManaged public var isDone: Bool
#NSManaged public var title: String?
#NSManaged public var category: CategoryEntity?
func convertToModel() -> Task {
var task = Task()
task.id = self.id!
task.title = self.title!
task.content = self.content!
task.isDone = self.isDone
task.date = self.date! as Date
task.category.name = self.category!.name!
task.category.color = Constants.colors[self.category!.color!]!
return task
}
and NSFetchedResultControllerDelegates's:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
if let indexPath = newIndexPath {
tableView.insertRows(at: [indexPath], with: .fade)
}
break
case .update:
if let indexPath = indexPath {
let cell = tableView.cellForRow(at: indexPath) as! TaskCell
cell.viewModel = viewModel.getTaskCell(indexPath: indexPath)
}
break
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .fade)
}
break
default:
break
}
}
while updating try
tableView.reloadRows(at: [indexPath], with: .automatic)
instead of
let cell = tableView.cellForRow(at: indexPath) as! TaskCell
cell.viewModel = viewModel.getTaskCell(indexPath: indexPath)
I Hope this will work.
Thanks for considering to help me! so I am getting this error in the console, How should I start going about fixing it?
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a non-nil fetchRequest and managedObjectContext'
*** First throw call stack:
(0x183ff51b8 0x182a2c55c 0x18630cb90 0x1000f4eb0 0x1000f46e8 0x1000f43c8 0x1000f411c 0x1000f40e4 0x101189218 0x10118a048 0x1000f41b4 0x1000fbb94 0x1000fbc94 0x189eaa924 0x189eaa4ec 0x18a2354e4 0x18a1fc6d0 0x18a1f8b44 0x18a13bfdc 0x18a12dd50 0x189e9d0b4 0x183fa20c0 0x183f9fcf0 0x183fa0180 0x183ece2b8 0x185982198 0x189f157fc 0x189f10534 0x1000f69d0 0x182eb15b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
These are the two swift files relevant to my error:
EntryController.swift
import Foundation
import CoreData
class EntryController {
static let shared = EntryController()
var fetchResultsController: NSFetchedResultsController<Entry>
init() {
let request: NSFetchRequest<Entry> = Entry.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "timestamp", ascending: true)
request.sortDescriptors = [sortDescriptor]
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: CoreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
(try? fetchResultsController.performFetch())
}
//CRUD
func add(name: String, text: String) {
_ = Entry(name: name, text: text)
saveToPersistanceStorage()
}
func remove(entry: Entry) {
let moc = CoreDataStack.context
moc.delete(entry)
saveToPersistanceStorage()
}
func update(entry: Entry, name: String, text: String) {
entry.name = name
entry.text = text
saveToPersistanceStorage()
}
func saveToPersistanceStorage() {
let moc = CoreDataStack.context
(try? moc.save())
}
}
EntryTableListViewController.swift
import UIKit
import CoreData
class EntrylistTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
EntryController.shared.fetchResultsController.delegate = self
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let entries = EntryController.shared.fetchResultsController.fetchedObjects else {return 0}
return entries.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "entryCell", for: indexPath)
let entry = EntryController.shared.fetchResultsController.object(at: indexPath)
cell.textLabel?.text = entry.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let entry = EntryController.shared.fetchResultsController.object(at: indexPath)
EntryController.shared.remove(entry: entry)
}
}
//MARK: NSFetchedResultsControllerDelegate
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
guard let newIndexPath = newIndexPath else {return}
tableView.insertRows(at: [newIndexPath], with: .automatic)
case .delete:
guard let indexPath = indexPath else {return}
tableView.deleteRows(at: [indexPath], with: .automatic)
case .move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else {return}
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.insertRows(at: [newIndexPath], with: .automatic)
case .update:
guard let indexPath = indexPath else {return}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toDetailSegue" {
if let detailVC = segue.destination as? EntryDetailViewController,
let selectedRow = tableView.indexPathForSelectedRow {
let entry = EntryController.shared.fetchResultsController.object(at: selectedRow)
detailVC.entry = entry
}
}
}
}
The app creates that error as soon as the button is pressed from the main menu to segue into the EntryTableListViewController.swift. Any help will be greatly appreciated!
Additionally, here's my CoreDataStack code
import Foundation
import CoreData
enum CoreDataStack{
static let container: NSPersistentContainer = {
let appName = Bundle.main.object(forInfoDictionaryKey: (kCFBundleNameKey as String)) as! String
let container = NSPersistentContainer(name: appName)
container.loadPersistentStores() { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
static var context: NSManagedObjectContext { return container.viewContext }
}