Using Xcode 14.3 and Swift 5

On a view controller, I would like to display a tableview separated by section which contains the information I’m entering on the same view.

The storyboard has 3 text fields for a piece description, id and image, a picker view where the user can choose a storage location and the tableview.

The picker view is populated through another viewer and that seems to work properly.

When the button save is pressed, the 3 fields and the picker view are read and saved, the table should display the location as section title and all pieces with the sections.

There are 2 entities in Core Data, PieceEntity and StorageEntity. The first one has attributes pieceDescription, pieceID and pieceImageNo and a relationship storage to StorageEntity with inverse pieces and type To One. StorageEntity has only one attribute, location and the relationship pieces to PieceEntity.

The items in the table view are deletable.

class AddPieceViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
    var fetchedResultsController: NSFetchedResultsController<PieceEntity>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            fatalError("AppDelegate not found.")
        }
        managedObjectContext = appDelegate.persistentContainer.viewContext
        
        // Set up the fetched results controller
        let fetchRequest: NSFetchRequest<PieceEntity> = PieceEntity.fetchRequest()
        let sortDescriptor = NSSortDescriptor(key: "pieceID", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
        
        /*fetchedResultsController = NSFetchedResultsController(
         fetchRequest: fetchRequest,
         managedObjectContext: managedObjectContext, // Your managed object context
         sectionNameKeyPath: nil,
         cacheName: nil
         )*/
        
        //actually, sectionNameKeyPath: "bin.location" gives the assertion failure
        fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: managedObjectContext, // Your managed object context
            sectionNameKeyPath: "storage.location",
            cacheName: nil
        )
        
        fetchedResultsController.delegate = self // NSFetchedResultsControllerDelegate
        
        // Perform the initial fetch
        do {
            try fetchedResultsController.performFetch()
        } catch {
            print("Error fetching data: \(error)")
        }
        
        // Populate the storageLocations array from Core Data
        …
        storageLocationPickerView.reloadAllComponents()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // Initialize Core Data stack
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            fatalError("AppDelegate not found.")
        }
        managedObjectContext = appDelegate.persistentContainer.viewContext
    }
}

The save function and extensions:

@IBAction func save(sender: UIBarButtonItem) {
    print("save")
    guard
        let pieceDescription = addPieceDescriptionTextField.text,
        let pieceIDText = addPieceIDTextField.text,
        let pieceID = Double(pieceIDText),
        let pieceImageNo = addPieceImageNoTextField.text
    else {
        print("return without save")
        return }
    
    // Access the selected storage location
    let selectedRow = storageLocationPickerView.selectedRow(inComponent: 0)
    let selectedStorageLocation = storageLocations[selectedRow]
    
    // Fetch the existing StorageEntity with the selected location
    /*let fetchRequest: NSFetchRequest<StorageEntity> = StorageEntity.fetchRequest()
     fetchRequest.predicate = NSPredicate(format: "location == %@", selectedStorage)
     */
    print("selectedStorage before newPiece: \(selectedStorageLocation)")
    
    let newPiece = PieceEntity(context: managedObjectContext)
    newPiece.pieceDescription = pieceDescription
    newPiece.pieceID = pieceID
    newPiece.pieceImageNo = pieceImageNo
    
    let fetchRequest: NSFetchRequest<StorageEntity> = StorageEntity.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "location == %@",                         selectedStorageLocation)
    
    do {
        let storageEntities = try managedObjectContext.fetch(fetchRequest)
        if let selectedStorage = storageEntities.first {
            newPiece.storage = selectedStorage
            print("Selected Storage: \(selectedStorage.location ?? "Unknown")")
        } else {
            print("Selected Storage not found.")
        }
    } catch {
        print("Error fetching data: \(error)")
    }
    
    print("selectedStorage after newPiece: \(selectedStorageLocation)")
    print("newPiece.storage.location: \(newPiece.storage!.location ?? "-")")
    
    //Save the managed object context
    do {
        print("try managedObjectContext!.save()")
        try managedObjectContext!.save()
    } catch {
        print("Error saving data: \(error)")
    }
}

extension AddPieceViewController: NSFetchedResultsControllerDelegate {
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        addPieceTableView.beginUpdates()
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                    didChange anObject: Any,
                    at indexPath: IndexPath?,
                    for type: NSFetchedResultsChangeType,
                    newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            if let newIndexPath = newIndexPath {
                addPieceTableView.insertRows(at: [newIndexPath], with: .automatic)
            }
        case .delete:
            if let indexPath = indexPath {
                addPieceTableView.deleteRows(at: [indexPath], with: .automatic)
            }
        case .update:
            if let indexPath = indexPath {
                addPieceTableView.reloadRows(at: [indexPath], with: .automatic)
            }
        case .move:
            if let indexPath = indexPath, let newIndexPath = newIndexPath {
                addPieceTableView.deleteRows(at: [indexPath], with: .automatic)
                addPieceTableView.insertRows(at: [newIndexPath], with: .automatic)
            }
        @unknown default:
            break
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        addPieceTableView.endUpdates()
    }
}

extension AddPieceViewController: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        print("*- numberOfSections")
        //return fetchedResultsController.sections?.count ?? 0
        let sectionCount = fetchedResultsController.sections?.count ?? 0
        print("Number of Sections: \(sectionCount)")
        return sectionCount
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let sectionInfo = fetchedResultsController.sections?[section] {
            return sectionInfo.numberOfObjects
        }
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        print("*- indexPath.row: \(indexPath.row)")
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "PieceTableViewCell", for: indexPath) as? PieceTableViewCell else {
            fatalError("Unexpected Index Path for let cell")
        }
        let item = fetchedResultsController.object(at: indexPath)
        
        print("cell: \(cell)")
        print("item.description: \(String(describing: item.description))")
        print("item.pieceImageNo: \(String(describing: item.pieceImageNo))")
        
        cell.pieceDescriptionRecordLabel.text = item.pieceDescription
        cell.pieceIDRecordLabel.text = String(Int(item.pieceID))
        if item.pieceImageNo != nil {
            cell.pieceImageNoRecordImage?.image = UIImage(named: String(item.pieceImageNo!))
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        print("*- editingStyle")
        //MARK: to delete a row
        if editingStyle == .delete {
            let textToDelete = fetchedResultsController.object(at: indexPath)
            managedObjectContext.delete(textToDelete)
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
                fatalError("AppDelegate not found.")
            }
            appDelegate.saveContext()
        }
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        print("*- titleForHeaderInSection section: \(section)")
        let sectionInfo = fetchedResultsController.sections?[section]
        print("sectionInfo.name: \(String(describing: sectionInfo?.name))")
        return sectionInfo?.name
    }
}

With this code, I get this error:

Error message: Assertion failure in -[UITableView_Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Sections:], UITableView.m:2615

When I replace sectionNameKeyPath: "storage.location" by sectionNameKeyPath: nil the pieces are recorded, displayed without title section. Then I run again with sectionNameKeyPath: "storage.location", everything is displayed correctly, but saving is not possible.

1

There are 1 best solutions below

1
lauk75 On

adding

self.addPieceTableView.reloadData()

at the end of the method viewDidLoad{} solved the problem.