Swift: UITableViewController selecting cell & passing fetched objects to UIViewController

Viewed 97

I’m trying to make a simple note app using Core Data, but I’m running into problem with fetchedResultsController.object(at: indexPath) & passing the data onto my View Controller. using prepare(for segue: UIStoryboardSegue, sender: Any?).

My Core Data Entity is named Notes with the following Attributes

  • dateStamp Integer 64
  • noteName String
  • noteImage Data
  • noteDescription String

However in order to understand the problem I’ve made separate project & limited it to just the noteName & dateStamp.

So there are three Controllers & one Helper file

  • MasterViewController
  • AddViewController
  • DetailViewController
  • EditViewController
  • dateHelper

The MasterView is my UITableController with my NSFetchedResultsControllerDelegate, so the code is as follows….

import UIKit
import CoreData
class MasterViewController: UITableViewController {

private let noteCreationTimeStamp : Int64 = Date().timetoSeconds()
 
    var managedObjectContext: NSManagedObjectContext? {
        return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    }

var fetchedResultsController: NSFetchedResultsController<Notes>!
   
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)      
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

       fetchFromCoreData()
        
        navigationItem.leftBarButtonItem = editButtonItem

        let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
        navigationItem.rightBarButtonItem = addButton   
    }

   @objc
   func insertNewObject(_ sender: Any) { 
let addController = storyboard?.instantiateViewController(withIdentifier: "addNotes") 
as! UINavigationController
        self.present(addController, animated: true, completion: nil)
    }

 private func configureCells(cell: noterViewCell, withEvent note: Notes, indexPath: IndexPath) {
       
        let record = fetchedResultsController.object(at: indexPath)
        
        cell.noteName.text = record.noteName
        cell.dateLabel.text = dateHelper.convertDate(date: Date.init(seconds: record.dateStamp))
    
        if let noteName = record.value(forKey: "noteName") as? String, let dateTime = record.value(forKey: "dateStamp") as? Int64  {
            cell.noteName.text = noteName
            cell.dateLabel.text = dateHelper.convertDate(date: Date.init(seconds: dateTime))
            
}     
    }

 override func numberOfSections(in tableView: UITableView) -> Int {
        return fetchedResultsController.sections?.count ?? 0
      
    }
    
 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        guard let sections = self.fetchedResultsController?.sections else {
                fatalError("No sections in fetchedResultsController")
            }
            let sectionInfo = sections[section]
            return sectionInfo.numberOfObjects
    }

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

guard let object = self.fetchedResultsController?.object(at: indexPath) else {
               fatalError("Attempt to configure cell without a managed object")
           }

configureCells(cell: cell, withEvent: object, indexPath: indexPath)    
        cell.selectbutton.addTarget(self, action: #selector(selectNote(_:)), for: .touchUpInside)
         return cell        
    }

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

 override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let context = fetchedResultsController.managedObjectContext
            context.delete(fetchedResultsController.object(at: indexPath))
                
            do {
                try context.save()
            } catch {
 let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
func fetchFromCoreData()  {
        let fetchRequest: NSFetchRequest<Notes> = Notes.fetchRequest()
        fetchRequest.fetchBatchSize = 800
        let creationDateSortDescriptor  = NSSortDescriptor(key: "dateStamp", ascending: false)
        //let nameSortDescriptor = NSSortDescriptor(key: "noteName", ascending: false)
        fetchRequest.sortDescriptors = [creationDateSortDescriptor]
        
        let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: "dateStamp", cacheName: nil)
        aFetchedResultsController.delegate = self
        fetchedResultsController = aFetchedResultsController
        
      
        do {
            try fetchedResultsController.performFetch()
            self.tableView.reloadData()
        } catch {
             // Replace this implementation with code to handle the error appropriately.
             // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
             let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.localizedDescription), \(nserror.localizedFailureReason ?? "could not retrieve")")
            //print("Could not save note to CoreData: \(error.localizedDescription)")
        }
              
    }
    
}

extension MasterViewController: NSFetchedResultsControllerDelegate  {
   
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        switch type {
            case .insert:
                tableView.insertSections(IndexSet(integer: sectionIndex), with: .fade)
            case .delete:
                tableView.deleteSections(IndexSet(integer: sectionIndex), with: .fade)
            default:
                return
        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
            case .insert:
                tableView.insertRows(at: [newIndexPath!], with: .fade)
            case .delete:
                tableView.deleteRows(at: [indexPath!], with: .fade)
            case .update:
                tableView.reloadRows(at: [newIndexPath!], with: .fade)
             if let updateIndexPath = newIndexPath {
             let cell = self.tableView.cellForRow(at: updateIndexPath) as! noterViewCell
             let event = anObject as! Notes
             cell.dateLabel.text = event.noteName
             cell.dateLabel.text = dateHelper.convertDate(date: Date.init(seconds:  event.dateStamp))
                   
}
            case .move:
               configureCells(cell: tableView.cellForRow(at: indexPath!) as! noterViewCell, withEvent: anObject as! Notes, indexPath: indexPath!)
                tableView.moveRow(at: indexPath!, to: newIndexPath!)
                               
        @unknown default:
        
            fatalError("Unresolved error")      
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
   
}

My AddViewController

import UIKit
import CoreData

class AddViewController: UIViewController {

    @IBOutlet var noteField: UITextField!
    @IBOutlet var dateStamp: UILabel!
    
    
    private let noteCreationTimeStamp : Int64 = Date().timetoSeconds()
    
    
    let masterView = MasterViewController()
    var isExisting: Bool = false
    var note:Notes? = nil
    
    var managedObjectContext: NSManagedObjectContext? {
        return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
   configureView()
        
    }
       
    var detailItem: Notes? {
        didSet {
            // Update the view.
            configureView()
        }
    }

    func configureView() {
        if let detail = detailItem {
            if let noteFieldLabel = noteField, let dateTime = dateStamp {
                
                noteFieldLabel.text = detail.noteName
                dateTime.text = dateHelper.convertDate(date: Date.init(seconds: detail.dateStamp))
            }
        }
    }
    
    @IBAction func saveImageButtonPressed(_ sender: Any) {
       
        let context = self.managedObjectContext
        let newEvent = Notes(context: context!)
        newEvent.noteName = noteField.text
        newEvent.dateStamp = noteCreationTimeStamp
        
        do {
            try context?.save()
            
        } catch {
           
            let error = error as NSError
            fatalError("Unresolved error \(error), \(error.localizedDescription)")
           
        }

            let isPresentingMode = self.presentingViewController is UINavigationController
            
            if isPresentingMode {
                self.dismiss(animated: true, completion: nil)
                
            }
            
            else {
                self.navigationController!.pushViewController(masterView, animated: true)
                   
            }       
    }
    
    
    @IBAction func cancelView(_ sender: AnyObject) {
        let isPresentingMode = self.presentingViewController is UINavigationController
        
        if isPresentingMode {
            self.dismiss(animated: true, completion: nil)
            
        }
    }
   
}

Everything is good there my saveImageButtonPressed saves the data to my managedObjectContext and displays onto the UITableview with no problems.

This code for my DetailViewController

import UIKit
import CoreData

class DetailViewController: UIViewController {
    
    @IBOutlet var dateLabel: UILabel!
    @IBOutlet var noteField: UILabel!
    let masterView = MasterViewController()
    
    
    var notes: Notes?
    var myNotes = [Notes]()
    var isExisting: Bool = false
    var index = IndexPath()
    private let noteCreationTimeStamp : Int64 = Date().timetoSeconds()
   
    
    
    var managedObjectContext: NSManagedObjectContext? {
        return (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
    }
    
    
    var detailItem: Notes? {
        didSet {
            // Update the view.
            configureView()
        }
    }

    func configureView() {
        if let detail = detailItem {
            if let noteFieldLabel = noteField, let dateStamp = dateLabel {
                
                noteFieldLabel.text = detail.noteName
                dateStamp.text = dateHelper.convertDate(date: Date.init(seconds: detail.dateStamp))
                
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

       //fetchNote()
        configureView()     
        
    }

  
    @IBAction func cancelView(_ sender: AnyObject) {
        let isPresentingMode = self.presentingViewController is UINavigationController
        
        if isPresentingMode {
            self.dismiss(animated: true, completion: nil)
            
        }
    }
//
    func fetchNote()  {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Notes")
        
        do {
            myNotes = try managedObjectContext?.fetch(fetchRequest) as! [Notes]
           
        } catch { 
     let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.localizedDescription,\(nserror.localizedFailureReason ?? "could not retrieve")")
 }
        
    }
//
}

So I’m trying to select my cell to pass the saved data to my DetailViewController but it is not selecting. Nothing happens. The fetchNote function in my DetailViewController was just add-on to try out.

This is my code for my prepare(for segue: UIStoryboardSegue, sender: Any?)

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        if segue.identifier == "showDetails" {
            
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
                let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
                let objects = fetchedResultsController.object(at: selectedIndexPath)
                
                controller.detailItem = objects
       }
   }
}

and my tableView.didSelectRowAt: indexPath

 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
       
            let cell = self.tableView.cellForRow(at: indexPath) as! noterViewCell
            
        let event = fetchedResultsController.object(at: indexPath)
            cell.dateLabel.text = event.noteName
                
            cell.dateLabel.text = dateHelper.convertDate(date: Date.init(seconds: event.dateStamp))
            //configureCells(cell: tableView.cellForRow(at: indexPath!) as! noterViewCell, withEvent: anObject as! Notes, indexPath: indexPath!)
               
        self.performSegue(withIdentifier: "showDetails", sender: self)
    
       
    }

I added self.performSegue(withIdentifier: "showDetails", sender: self) but the DetailViewController is just blank.

In order to understand further I added button in the cellForRowAt indexPath:…

cell.selectbutton.addTarget(self, action: #selector(selectNote(_:)), for: .touchUpInside)

with the following action

 @objc
    func selectNote(_ sender: noterViewCell)  {
    
        let indexPath = IndexPath()
        
        let objects = fetchedResultsController.object(at: indexPath) as Notes
        let controller = DetailViewController()
        controller.detailItem = objects
       //let detailView =  //storyboard?.instantiateViewController(withIdentifier: "viewNotes") as! //UINavigationController
       //self.present(detailView, animated: true, completion: nil)
    
let detailViews = DetailViewController()
        self.navigationController?.popToViewController(detailViews, animated: true)
    }

and this threw an 'NSInvalidArgumentException', reason: 'no section at index 9223372036854775807' with both methods tried in accessing my Detail View

This was directed exactly on fetchedResultsController.object in my button action @objc MasterViewController_selectNote(_:)

Also listed in the thread was [NSFetchedResultsController objectAtIndexPath:]: scrolling down I found another reason, "cannot access fetched objects before -performFetch:" as well as "NSFetchedResultsController: no object at index %lu in section at index %lu"

I’m calling my fetchFromCoreData() in my viewDidLoad() … I took out the performFetch I pasted it directly in the viewDidLoad(). Still throws the same error if I click on the button. Again blank view selecting using prepareForSegue. I tried using return value in my fetchFromCoreData() function again the same.

I’m not sure if I added to the problem by adding the button but in all cases I’ve attempted to remedy the problem prepareForSegue its still passing a blank view.

I also tried to validate the indexPath using this function

func validateIndexPath(_ indexPath: IndexPath) -> Bool {
        if let sections = self.fetchedResultsController.sections,
        indexPath.section < sections.count {
           if indexPath.row < sections[indexPath.section].numberOfObjects {
              return true
           }
        }
        return false
    }

again to no avail, calling it on the cellForRowAt indexPath & the app still terminates with the same 'NSInvalidArgumentException’ and the prepareForSegue passing blank view.

Then I added my titleForHeaderInSection in my tableView which is set by month & year that is displaying okay when I save the data in my AddViewController. I hoped it might make a bit of difference but…

 func monthDate(string:String?) -> String? {
       
            let date  = Date()
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM"
            let dateString = formatter.string(from: date)
            let monthDate = formatter.date(from: dateString)
            formatter.dateFormat = "MMM, yyyy"
            let dateMonth = formatter.string(from: monthDate!)
            
            return dateMonth
        
             }

 override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        
    guard let sectionInfo = fetchedResultsController.sections?[section] else {
                return nil
        
    }
        return monthDate(string: sectionInfo.name)
            
    }

I really just don't understand where I'm going wrong... I’m using iPhone 11 pro max simulator on IOS14 & Xcode 12.3.(I upgraded about month ago & it did hang on install for about 4 or 5 days, its been bit buggy since)

Although I’m sure I’ve probably missed something quite simple any help will be much appreciated.

On the other hand if I've understood correctly I’m aware NSFetchedResultsController does not like empty sections as per this blog post…

http://www.iosnomad.com/blog/2014/8/6/swift-nsfetchedresultscontroller-trickery

and if that could be the problem, is there simpler solution for Swift 5?

EDIT

In my tableview didSelectRowAt I added

 print(fetchedResultsController.object(at: indexPath))

and the console outputs my saved data but it's still not passing it to my DetailViewController.

I tried also in my prepareForSegue adding

print(fetchedResultsController.object(at: selectedIndexPath))

This returned no saved data, as well as returning 0 sections after printing out on the console the numberOfSections in tableView, after selecting my cell. But as I open my app the data is still saved and viewable in my cell and prints out correct section numbers in the console, as I add new note. Only when I select my cell to pass saved data to my DetailViewController the problem occurs.

This is a screenshot when I select using prepareForSegue & print out on the console

1 Answers

Problem Solved

I added following variables to my UITableViewController

var cellLabel: [Notes] = []
var selectedLabels = String()
var selectedTime = String()

and added variables to my DetailViewController

var receivedString: String = ""
 var recievedTime: String = ""

I changed my code to reflect fetched objects are attached to my selected variables in my didSelectRowAt and equal to my received variables in my prepareForSegue.

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
   
   
        let event = fetchedResultsController.object(at: indexPath)
            cellLabel = [event]
        selectedLabels = event.noteName!
        selectedTime =  dateHelper.convertDate(date: Date.init(seconds: event.dateStamp))
       
   print(selectedLabels)
    print(selectedTime)
   
    self.performSegue(withIdentifier: "showDetails", sender: self)  
   
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        
        if segue.identifier == "showDetails" {
            let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
            controller.receivedString = selectedLabels
            controller.recievedTime = selectedTime
            print(selectedLabels)
            print(selectedTime)
            /*
            if let indexPath = self.tableView.indexPathForSelectedRow {
            let objects = fetchedResultsController.object(at: indexPath)
            controller.noteField.text = objects.noteName
            controller.detailItem = objects
            
            }
                    */
   }
} 

and in my viewDidLoad() in DetailViewController

override func viewDidLoad() {
    super.viewDidLoad()
    
    noteField.text = receivedString
    dateLabel.text = recievedTime

}

screenshot of fixed code

Related