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:
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:
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:
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:
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
.