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.
adding
at the end of the method viewDidLoad{} solved the problem.