Search code examples
uitableviewcore-dataxcode10ios12

Reorder UITableviewCell where data is stored in CoreData


I have created a iOS application which stores a list (say names). Further I have added functionalities like swipe to delete, search and to reorder table rows. Below is the app screenshot:

enter image description here

The issue I'm facing is, when I reorder the list by clicking edit button at first it seems like the code is working fine. In the below screens I have interchanged first two rows which seem to do as I want it to. Look at the two screenshots below:

enter image description here

enter image description here

But when I perform the search functionality, rows which have been interchanged revert to their original position as shown in the first image. Since I'm using CoreData as persistent storage. I'm trying to find a solution to this but so far there's no success. This is how it looks like after performing search functionality:

enter image description here

This is my code:

import UIKit
import CoreData

class ViewController: UIViewController, UISearchBarDelegate, UISearchDisplayDelegate {

    // IBOutlets
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var editButton: UIBarButtonItem!

    // Global declaration
    var people: [NSManagedObject] = []

    // Below is a computed property
    var appDelegate: AppDelegate {
        return UIApplication.shared.delegate as! AppDelegate
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // The below line is for giving a title to the View Controller.
        // title = "The List"

        searchBar.delegate = self

        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        // The below line hides empty rows in a tableView
        tableView.tableFooterView = UIView()

    }

    //MARK: - IBAction addName implementation
    @IBAction func addName(_ sender: UIBarButtonItem) {
        let alert = UIAlertController(title: "New Name", message: "Add a new name", preferredStyle: .alert)
        let saveAction = UIAlertAction (title: "Save", style: .default) {
            [unowned self] action in
            guard let textField = alert.textFields?.first, let nameToSave = textField.text else {
                return
            }
            self.save(name: nameToSave)
            self.tableView.reloadData() // this is to reload the table data
        }

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

        // The below code handles the validation operation. In this case, we are checking wheather the field is empty and if it is 'save' button is disabled.
        alert.addTextField(configurationHandler: { (textField) in
            textField.text = ""
            textField.placeholder = "Enter something...."
            saveAction.isEnabled = false
            NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: textField, queue: OperationQueue.main) { (notification) in
                saveAction.isEnabled = textField.text!.count > 0
            }
        })

        alert.addAction(saveAction)
        alert.addAction(cancelAction)

        present(alert, animated: true)
    }

    // MARK: - SAVING TO CORE DATA

    // CoreData kicks in here!
    func save(name: String) {

        // 1
        let managedContext = appDelegate.persistentContainer.viewContext

        // 2
        let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!
        let person = NSManagedObject(entity: entity, insertInto: managedContext)

        // 3
        person.setValue(name, forKeyPath: "name")

        // 4
        do {
            try managedContext.save()
            people.append(person)
        } catch let error as NSError {
            print("Could not save. \(error), \(error.userInfo)")
        }
    }

    // MARK: - FETCHING FROM CORE DATA

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // 1
        let managedContext = appDelegate.persistentContainer.viewContext

        // 2
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

        // 3
        do {
            people = try
                managedContext.fetch(fetchRequest)
        } catch let error as NSError {
            print("Could not save. \(error), \(error.userInfo)")
        }
    }

    // MARK: - searchBar functionality implementation

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        if searchText != "" {
            var predicate: NSPredicate = NSPredicate()
            predicate = NSPredicate(format: "name contains[c] '\(searchText)'")
            let context = appDelegate.persistentContainer.viewContext
            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
            fetchRequest.predicate = predicate
            do {
                people = try context.fetch(fetchRequest) as! [NSManagedObject]
            } catch {
                print("Could not get search data!")
            }
        } else {
            let context = appDelegate.persistentContainer.viewContext
            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
            do {
                people = try context.fetch(fetchRequest) as! [NSManagedObject]
            } catch {
                print("Error in loading data.")
            }
        }
        tableView.reloadData() // This line reloads the table whether search is performed or not.
    }

    // This function closes the search bar when cancel button is tapped.
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.resignFirstResponder()
    }

    @IBAction func editButton(_ sender: Any) {
        tableView.isEditing = !tableView.isEditing

        // This switch case is for changing the title when editing
        switch tableView.isEditing {
        case true:
            editButton.title = "Done"
        case false:
            editButton.title = "Edit"
        }
    }

}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return people.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let person = people[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = person.value(forKeyPath: "name") as? String
        return cell
    }

    // This function sets the height for a row programatically.
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 55.0
    }

    // Determine whether a given row is eligible for reordering or not.
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    // Process the row move. This means updating the data model to correct the item indices.
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let itemToMove =  people.remove(at: sourceIndexPath.row)
        people.insert(itemToMove, at: destinationIndexPath.row)
        tableView.reloadData()
    }

}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // handle delete (by removing the data from your array and updating the tableview)

            // MARK: - Delete the person from core data

            let person = people[indexPath.row]
            let managedContext = appDelegate.persistentContainer.viewContext
            managedContext.delete(person)
            try? managedContext.save() // This the short version of do-catch block used in above functions to save and fetch data

            // remove the person from cache / CoreData
            people.remove(at: indexPath.row)

            // delete row from table view
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
    }

}

Any help would be appreciated as I have trying this for weeks.

My CoreData Model:

enter image description here


Solution

  • Your line of code:

    var people: [NSManagedObject] = []
    

    is certainly central to the problem. Core Data does not know about this array. Delete that var. Your table view data source should instead get its data from a fetched results controller, something like this:

    let person = fetchedResultsController()?.object(at: indexPath) as? Person
    

    You should be able to find many examples of working UITableViewDataSource delegates in tutorials like this one, or in Apple sample code. Follow one of them to get yourself a good, conventional table view delegate. The line of code above comes from a little demo project which I forked recently.

    Another issue is that Core Data does not store the managed objects of a given entity in any particular order. Neither of the links I gave in the previous paragraph supports ordering. There are two alternatives to support ordering:

    Alternative 1. Use a Core Data ordered relationship.

    If your Persons are members of a Group entity of some kind, your data model should have a to-many relationship from Group to Person. You can make this an ordered relationship by switching on the Ordered checkbox in the Data Model inspector for this relationship.

    Alternative 2. Use an index attribute.

    For simpler applications, and this applies to your data model as currently in your screenshot, you could add an attribute such as index attribute to your Person entity and make your fetched results controller sort by this attribute:

    let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
    fetchRequest.sortDescriptors = [sortDescriptor]
    let frc = NSFetchedResultsController(fetchRequest: fetchRequest,
                                 managedObjectContext: managedContext,
                                   sectionNameKeyPath: nil,
                                            cacheName: nil)
    

    wherein I have used your symbol name managedContext.