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: