1
votes

I have three view controllers(two UIViewControllers and 1 UITableViewController). I want to display all the data on the tableView and add/update data on two separate view controllers.

The two UIViewControllers have three textFields each (for name, email and phone number) and one button to save/update the data in CoreData and on the tableViewController.

One section in the table view consists of three/two rows (number text field can be empty). On swiping a row from a section, the user can delete the whole section or edit the data in the section.

I have created "Person" entity and three attributes ("name","email","number", all of String data type).

But I get the following error on the line

    let objectUpdate = test[0] as! NSManagedObject

Error: Fatal error: Index out of range

import UIKit
import CoreData

class RootTableViewController: UITableViewController {

//Array to display the data in table:

var array_of_person_data_array : [PersonData] = []

//Variable with index of selected row/section:

var index = 0

override func viewDidLoad() {
    super.viewDidLoad()

    let rightBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(nextScreen))

    navigationItem.rightBarButtonItem = rightBarButton

    tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "reuseIdentifier")

}

override func viewWillAppear(_ animated: Bool) {
    retrieveData()
    tableView.reloadData()
}

// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
    return array_of_person_data_array.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if array_of_person_data_array[section].number == nil || array_of_person_data_array[section].number == ""
    {return 2}
    return 3
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) as! TableViewCell

    if indexPath.row == 0
    {cell.personLabel.text = array_of_person_data_array[indexPath.section].name}
    else if indexPath.row == 1
    {cell.personLabel.text = array_of_person_data_array[indexPath.section].email}
    else
    {cell.personLabel.text = array_of_person_data_array[indexPath.section].number}



    return cell
}



//Row actions when swiped:

override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {

    index = indexPath.section


    //Cancel row action:

    let cancelRowAction = UITableViewRowAction(style: .normal, title: "Cancel", handler: {(action : UITableViewRowAction,indexPath : IndexPath) in

    })


    //Update row action:

    let updateRowAction = UITableViewRowAction(style: .default, title: "Update", handler: {(action: UITableViewRowAction, indexPath: IndexPath) in


        let sbObj = UIStoryboard(name: "Main", bundle: nil)
        let svcObj = sbObj.instantiateViewController(withIdentifier: "UpdateViewControllerSB") as! UpdateViewController
        svcObj.index = self.index
        svcObj.personDataObject = self.array_of_person_data_array[indexPath.section]
        self.navigationController?.pushViewController(svcObj, animated: true)

    })


    //Delete row action:

    let deleteRowAction = UITableViewRowAction(style: .destructive, title: "Delete", handler: {(alert : UITableViewRowAction, indexPath : IndexPath) in

        //Delete controller:

        let deleteController = UIAlertController(title: "Delete", message: nil, preferredStyle: .actionSheet)

        //Delete action:

        let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: {(UIAlertAction)in
            self.deleteData()
        })

        deleteController.addAction(deleteAction)

        //Cancel action:

        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

        deleteController.addAction(cancelAction)


        //Present the controller:

        self.present(deleteController,animated: true,completion: nil)

    })

    return [cancelRowAction,updateRowAction,deleteRowAction]
}


override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let view = UIView(frame: CGRect(x: 0, y: 0, width: 1000, height: 1000))
    return view
}

@objc func nextScreen()
{
    let sbObj = UIStoryboard(name: "Main", bundle: nil)
    let svcObj = sbObj.instantiateViewController(withIdentifier: "AddViewControllerSB") as! AddViewController
    self.navigationController?.pushViewController(svcObj, animated: true)
}




//Function to retrieve data from core data:

func retrieveData() {

    //As we know that container is set up in the AppDelegates so we need to refer that container.
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

    //We need to create a context from this container
    let managedContext = appDelegate.persistentContainer.viewContext

    //Prepare the request of type NSFetchRequest  for the entity
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")

    //        fetchRequest.fetchLimit = 1
    //        fetchRequest.predicate = NSPredicate(format: "username = %@", "Ankur")
    //        fetchRequest.sortDescriptors = [NSSortDescriptor.init(key: "email", ascending: false)]
    //
    do {
        let result = try managedContext.fetch(fetchRequest)
        for data in result as! [NSManagedObject] {


            array_of_person_data_array.append(PersonData(personName: data.value(forKey: "name") as! String, personEmail: data.value(forKey: "email") as! String, personNumber: data.value(forKey: "number") as? String))

        }

    } catch {

        print("Failed")
    }
}



//Function to delete data from Core Data:
func deleteData(){

    //As we know that container is set up in the AppDelegates so we need to refer that container.
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

    //We need to create a context from this container
    let managedContext = appDelegate.persistentContainer.viewContext

    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
    fetchRequest.predicate = NSPredicate(format: "name = %@", array_of_person_data_array[index].name!)
    fetchRequest.predicate = NSPredicate(format: "email = %@", array_of_person_data_array[index].email!)
    fetchRequest.predicate = NSPredicate(format: "number = %@", array_of_person_data_array[index].number!)



    do
    {
        let test = try managedContext.fetch(fetchRequest)

        let objectToDelete = test[0] as! NSManagedObject
        managedContext.delete(objectToDelete)
        array_of_person_data_array.remove(at: index)

        do{
            try managedContext.save()
        }
        catch
        {
            print(error)
        }

    }
    catch
    {
        print(error)
    }
    tableView.reloadData()
}

}

Add View Controller:

import UIKit
import CoreData

class AddViewController: UIViewController {

@IBOutlet weak var nameTF: UITextField!

@IBOutlet weak var emailTF: UITextField!

@IBOutlet weak var numberTF: UITextField!


override func viewDidLoad() {
    super.viewDidLoad()

}

@IBAction func addButtonAction(_ sender: Any) {

    createData()

    navigationController?.popToRootViewController(animated: true)

}


func createData(){

    //As we know that container is set up in the AppDelegates so we need to refer that container.
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

    //We need to create a context from this container
    let managedContext = appDelegate.persistentContainer.viewContext

    //Now let’s create an entity and new user records.
    let userEntity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!

    //Get data ready to be set into CORE DATA:

    let user = NSManagedObject(entity: userEntity, insertInto: managedContext)
    user.setValue(nameTF.text, forKeyPath: "name")
    user.setValue(emailTF.text, forKey: "email")
    user.setValue(numberTF.text, forKey: "number")


    //Save the set data to CORE DATA:

    do {
        try managedContext.save()

    } catch let error as NSError {
        print("Could not save. \(error), \(error.userInfo)")
    }
}

}

Update view controller:

class UpdateViewController: UIViewController {

@IBOutlet weak var nameTF: UITextField!

@IBOutlet weak var emailTF: UITextField!

@IBOutlet weak var numberTF: UITextField!

var index : Int?

var personDataObject=PersonData(personName: "sample", personEmail: "sample@sample", personNumber: "xxxx-xxx-xxx")

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
}

@IBAction func updateButtonAction(_ sender: Any) {

    self.updateData()
    navigationController?.popToRootViewController(animated: true)

}




//Update the data in CoreData:

func updateData(){

    //As we know that container is set up in the AppDelegates so we need to refer that container.
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

    //We need to create a context from this container
    let managedContext = appDelegate.persistentContainer.viewContext

    let fetchRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "Person")
    fetchRequest.predicate = NSPredicate(format: "name = %@", "Ankur1")
    fetchRequest.predicate = NSPredicate(format: "email = %@", "Ankur1")
    fetchRequest.predicate = NSPredicate(format: "number = %@", "Ankur1")


    do
    {
        let test = try managedContext.fetch(fetchRequest)

        let objectUpdate = test[0] as! NSManagedObject

        objectUpdate.setValue(nameTF.text, forKey: "name")
        objectUpdate.setValue(emailTF.text, forKey: "email")
        if let no = numberTF.text
        {objectUpdate.setValue(no, forKey: "number")}
        do{
            try managedContext.save()
        }
        catch
        {
            print(error)
        }
    }
    catch
    {
        print(error)
    }

}
}

PersonData class is defined as:

class PersonData
{
var name : String?
var email: String?
var number : String?

init(personName : String, personEmail : String, personNumber : String?) {
    name = personName
    email = personEmail
    number = personNumber
 }
 }

How do I update the existing data or add new data to CoreData and display the new data on the Table View Controller?

2

2 Answers

0
votes

You should look into NSFetchedResultsController: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/nsfetchedresultscontroller.html

In essence, you want to create/update/delete values on your second screens in your CoreData context, save those changes, and then the fetched results controller on your main screen is setup (by following the above documentation) to automatically listen for those changes and reflect them in the table.

This is good practice because you don't have to worry about updating the view yourself and keeping state synchronized - it's done automatically by the view retrieving data when it is updated.

0
votes
  • Declare a callback property and a property for the person data in AddViewController. Use the NSManagedObject object rather than the custom class PersonData

    var callback : ((Person) -> Void)?
    var person : Person?
    
  • In RootTableViewController assign a closure in nextScreen before presenting the controller

    svcObj.callback = { person in
       // execute the code you need
    }
    
  • In AddViewController at some point assign the modified NSManagedObject to person

  • In viewWillDisappear call the closure

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if let person = person { callback?(person) }
    }