Deleting many to many relationship coredata - ios

I have a many to many relationship of "BillSplitters" to "Items", im trying to. Delete a billsplitter from a tableview of billsplitters, but when i look at my items they still list the billsplitters they are related to and havent been deleted. I have tried changing the delete rule from nullify to cascade on both items and bill splitters but nothing seems to be changing.
Heres the code that i think is relevant:
BillSplitter table view:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let billSplitter = allBillSplitters[indexPath.row]
let currentSplitters = self.bill.mutableSetValueForKey("billSplitters")
let managedContext = self.bill.managedObjectContext
removeBillSplitter(billSplitter)
currentSplitters.removeObject(billSplitter)
do {
try managedContext!.save()
}
catch let error as NSError {
print("Core Data save failed: \(error)")
}
print(currentSplitters)
tableView.reloadData()
}
}
func removeBillSplitter(billSplitter: BillSplitter) {
if let index = allBillSplitters.indexOf(billSplitter) {
allBillSplitters.removeAtIndex(index)
}
}
Items Tableview:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
fetchBillItems()
let cell: NewBillSplitterItemCell = tableView.dequeueReusableCellWithIdentifier("NewBillSplitterItemCell") as! NewBillSplitterItemCell
let item = allItems[indexPath.row]
let numberOfSplitters = item.billSplitters?.count
if numberOfSplitters == 0 {
cell.currentSplitters.text = "No one is paying for this item yet."
} else {
var splitterList = "Split this item with: "
let itemSplitters = item.billSplitters?.allObjects as! [BillSplitter]
for i in 0...Int((numberOfSplitters)!-1) {
splitterList += "\(itemSplitters[i].name!), "
}
cell.currentSplitters.text = splitterList
}
cell.name.text = item.name
cell.price.text = "£\(item.price!)"
return cell
}
The idea is to select items from the item tableview to assign to a billsplitter, which works fine then redirects to the billsplitter table view, when deleting a billsplitter from this table view it works fine and is no longer listed, but when going back to the items table view it still lists the billSplitters when iterating over the itemSplitters for loop and i dont understand why? Do i have to find each item associated with the bill splitter and delete them from there? I though thats what cascade would do?

Related

CoreData app crashes when deleting row from TableView

I have a dozen table view controllers that all work as expected, then I have this one which crashes with:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
The code doing this has been modified several times with different options but to no effect.
import UIKit
import CoreData
class QTypeVCY: UITableViewController, NSFetchedResultsControllerDelegate
{
let app = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad()
{
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int
{
let sections = frc.sections
return sections!.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
guard let sections = self.frc.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: "QQQQ", for: indexPath)
let qtype = frc.object(at: indexPath)
cell.textLabel?.text = qtype.title
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete
{
do
{
let qtype = frc.object(at: indexPath)
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
tableView.deleteRows(at: [indexPath], with: .fade)
}
catch
{
debugPrint(error)
}
} else if editingStyle == .insert
{
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
lazy var frc: NSFetchedResultsController<Qtype> =
{
let context = self.app.persistentContainer.viewContext
let req: NSFetchRequest<Qtype> = Qtype.fetchRequest()
req.fetchBatchSize = 10
let sortDescriptor1 = NSSortDescriptor(key: #keyPath(Qtype.specialty), ascending:true)
let sortDescriptor2 = NSSortDescriptor(key: #keyPath(Qtype.title), ascending:true)
req.sortDescriptors = [sortDescriptor1, sortDescriptor2]
let afrc = NSFetchedResultsController(fetchRequest: req, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
afrc.delegate = self
do
{
try afrc.performFetch()
}
catch
{
print(error.localizedDescription)
fatalError("Abort while fetching Qtype")
}
return afrc
}()
}
The crash occurs on the tableview.deleteRows statement. I have tried surrounding the code with beginUpdate/endUpdates, with and without the performFetch, even tried re-entering the code in case I had a typo that I missed. This same basic code is working fine on other tables/view controllers, just this one.
The entity is just made up of strings. I had it with and without relationships to other entities.
The row is deleted since the next time I run the app it is missing. Also, one other thing about this table is that calling reloadData after adding a new object does not add the row. I need to leave the tableview and reenter it. I'm sure the two are related but can't say why.
Since originally posting this, I included the entire VC code instead of just the offending code. I have also tried swapping the Entity with a different entity where this issue does not occur, but the program still crashes even with a different entity.
You forgot to remove the item also from the data source array
let qtype = qtypes[indexPath.row]
let context = self.frc.managedObjectContext
context.delete(qtype)
try context.save()
qtypes.remove(at: indexPath.row) // <--
tableView.deleteRows(at: [indexPath], with: .fade)
I wish I could offer the actual solution, but the above code now works. I had updated XCode to the 1/24/18 version but I don't think that is the solution. I also stripped out a few things I had in the original code that crashed, but then I put them back and it still works. I suspect I had a typo somewhere, but after spending a half hour trying to locate it, I'm declaring victory and moving on.
I just solloved a problem similar to yours.
when deleted data in CoreData, TableView data array also delete before appDelegate.saveContext()
Code in func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
self.tableviewArrayData.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
appDelegate.saveContext()

Find custom Object index in array

I have an array of custom object called Service and in didSelectRow I populate my selected array of that object:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
let services:[Service] = self.menu[indexPath.section].services
self.selectedServices.append(services[indexPath.row])
}
}
The problem is that I can't figure out how to retrieve it from didDeselectRow:
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) {
cell.accessoryType = .None
let services = self.menu[indexPath.section].services
let service = services[indexPath.row]
//how can I found the index position of service inside selectedServices?
}
}
I suggest you don't store the selectedServices, but rely on UITableView.indexPathsForSelectedRows.
var selectedServices: [Service] {
let indexPaths = self.tableView.indexPathsForSelectedRows ?? []
return indexPaths.map { self.menu[$0.section].services[$0.row] }
}
This way, you don't need to manually maintain selectedServices and could remove the entire tableView(_:didSelectRowAtIndexPath:) function.
If you must maintain a separate state, you could find the service using index(where:) or index(of:) — see How to find index of list item in Swift?.
if let i = (self.selectedServices.index { $0 === service }) {
// find the index `i` in the array which has an item identical to `service`.
self.selectedServices.remove(at: i)
}

Variable use of multiple custom cells

I'm using a unclickable tableView to display different information of one object.
For this informations I have different custom cell types one where I placed a map, if my object have locations, one have a list with links, and another a multiple line label for a little description...for example.
I manage this cells with:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! MapCell
return cell
} else if indexPath.row == 1 {
let cell: textCell = tableView.dequeueReusableCellWithIdentifier("textCell") as! TextCell
return cell
} else if indexPath.row == 2 {
let cell: listCell = tableView.dequeueReusableCellWithIdentifier("listCell") as! ListCell
return cell
}
}
So far so good, everything working fine. My problem is, not every object needs a map, some of them just need some text and a list, other objects need a map and a list, other all of them. I want my tableView to skip some cells if there is a condition.
I know, I can make an symbolic array for changing the number of cells of my tableView, but that deleting just from the end of my tableView, not specific cells.
One of my ideas is to generate a empty cell, maybe with a height of 0 or 1 so that I can do something like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == 0 {
if mapCellNeeded {
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
} else {
let cell: emptyCell = tableView.dequeueReusableCellWithIdentifier("emptyCell") as! EmptyCell
}
return cell
} else if indexPath.row == 1 {
...
}...
}
put I don't know if there isn't an efficient way. Hope you guys can help me.
Your solution would work. Another approach (very nice and swifty) would be not to hardcode row numbers, but rather use enum instead:
enum InfoCellType {
case Map
case Text
case Links
}
...
var rows = [InfoCellType]()
...
// when you know what should be there or not
func constructRows() {
if (mapCellNeeded) {
rows.append(InfoCellType.Map)
}
rows.append(InfoCellType.Text)
... etc
}
Then in the table view methods just see what's the type for current indexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellType: InfoCellType = self.rows[indexPath.row]
switch cellType {
case .Map:
let cell: mapCell = tableView.dequeueReusableCellWithIdentifier("mapCell") as! mapCell
return cell
case .Text:
...
case.Links:
...
}
}
This solution also allows to easily change order of rows - just change the order of items in rows array.

Two custom tableViewCells in UITableView

I am trying to create a contacts page where you can see all your contacts with a friend request cell showing up when you receive a friend request, but not there when you do not have any. At the moment, both custom cells work fine. The issue I have is that the contactRequestTableViewCell overlaps the first cell of the contactListTableViewCell.
I have researched other questions about two custom tableviewcells and none are quite having the same issues that I am facing.
Here is my executing code at the moment, I am returning 2 sections in the table view.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! ContactListTableViewCell
let requestCell = tableView.dequeueReusableCellWithIdentifier("requestCell", forIndexPath: indexPath) as! ContactRequestsTableViewCell
let user = OneRoster.userFromRosterAtIndexPath(indexPath: indexPath)
if (amountOfBuddyRequests > 0) {
if (indexPath.section == 0) {
requestCell.hidden = false
cell.hidden = false
requestCell.friendRequestLabel.text = "test"
} else if (indexPath.section >= 1) {
cell.contactNameLabel!.text = user.displayName;
cell.contactHandleLabel!.text = "# " + beautifyJID(user.jidStr)
cell.contactHandleLabel!.textColor = UIColor.grayColor()
OneChat.sharedInstance.configurePhotoForImageView(cell.imageView!, user: user)
}
return cell;
}
else { // if buddy requests == 0
requestCell.hidden = true
cell.contactNameLabel!.text = user.displayName;
cell.contactHandleLabel!.text = "# " + beautifyJID(user.jidStr)
cell.contactHandleLabel!.textColor = UIColor.grayColor()
print ("This is how many unreadMessages it has \(user.unreadMessages)")
// If there is unread messages for a person highlight it blue
// However this feature isn't working right now due to unreadMessages bug
if user.unreadMessages.intValue > 0 {
cell.backgroundColor = .blueColor()
} else {
cell.backgroundColor = .whiteColor()
}
OneChat.sharedInstance.configurePhotoForCell(cell, user: user)
return cell;
}
}
This is the current output that I have right now, my cells that have "test" are covering up other contactListTableViewCells.
The function tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell should always return one and the only one TableViewCell you want at indexPath, so you don't want to always return cell of type ContactListTableViewCell.
According to documentation, the cellForRowAtIndexPath tableView method asks for the cell at the indexPath, which means literally there can only be one cell at certain row of a certain section, so returning two cells is not an option.
I suggest you use two arrays to store the requests and contacts information. For example, you have arrays requests and contacts. Then you can tell the tableView how many rows you want:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return requests.count + contacts.count
}
and then in cellForRowAtIndexpath you do something like:
override func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row < requests.count {
// return a request cell
}
else {
// return a contact cell
}
}
I'm only using one tableView section here. If you still want two sections you can simply return 2 in numberOfSections function and add if statements in cellForRowAtIndexPath for indexPath.section.
Hope this helps.
It turns out that the issue was dealing with the data sources. My data sources were not pointing to the correct tableviewcell. This resulted in them pointing to an incorrect cell. This issue was fixed by remaking the data sources system that was in place. This issue will not affect most as the data sources will point to the correct tableviewcell by default.
Contrary to what another poster said, you can in fact display two or more custom cells in a single table. This is how I fixed the tableView display issues:
var friendRequests = ["FriendRequest1", "FriendRequest2"]
var contacts = ["User1","User2","User3","User4"]
var amountOfBuddyRequests = 1
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if (amountOfBuddyRequests > 0) {
return 2
}
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (amountOfBuddyRequests > 0) {
if (section == 0) {
return friendRequests.count
}
}
return contacts.count
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if (amountOfBuddyRequests > 0) {
if (indexPath.section == 0) {
let requestCell = tableView.dequeueReusableCellWithIdentifier("requestCell") as! ContactRequestsTableViewCell
requestCell.friendRequestLabel.text = friendRequests[indexPath.row]
requestCell.onButtonTapped = {
self.friendRequests.removeAtIndex(indexPath.row)
self.tableView.reloadData()
}
requestCell.addButtonTapped = {
self.addUser(self.friendRequests[indexPath.row])
self.friendRequests.removeAtIndex(indexPath.row)
self.tableView.reloadData()
}
return requestCell
}
}
let friendCell = tableView.dequeueReusableCellWithIdentifier("FriendCell") as! ContactListTableViewCell
friendCell.contactNameLabel.text = contacts[indexPath.row]
return friendCell
}

iOS: Editing items in a TableView Issues

So I'm trying to write the delete edit behavior for the rows in a tableview. However, when I hit the delete key after selecting a row, the row is not deleted from the tableView. When I try to do it a second time, I get an error saying an unexpected nil value was found.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete { // Handle the Delete action
// Obtain the name of the genre of movie to be deleted
let genre: String = genres[indexPath.section]
// Obtain the list of movies in the genre as AnyObject
let movies: AnyObject? = applicationDelegate.dict_Genres_dict2[genre]
let movArray: [String] = movies?.allKeys as! [String] //The nil value is unwrapped on this line
// Typecast the AnyObject to Swift array of String objects
var moviesOfGenre: Array<String> = movArray
// Delete the identified movie at row
moviesOfGenre.removeAtIndex(indexPath.row)
if moviesOfGenre.count == 0 {
// If no movie remains in the array after deletion, then we need to also delete the genre
applicationDelegate.dict_Genres_dict2.removeObjectForKey(genre)
// Since the dictionary has been changed, obtain the genre names again
genres = applicationDelegate.dict_Genres_dict2.allKeys as! [String]
// Sort the genre names within itself in alphabetical order
genres.sortInPlace { $0 < $1 }
}
else {
// At least one more movie remains in the array; therefore, the genre stays.
// Update the new list of movie for the genre in the NSMutableDictionary
applicationDelegate.dict_Genres_dict2.setValue(moviesOfGenre, forKey: genre)
}
// Reload the rows and sections of the Table View
tableView.reloadData()
}
}
I have marked which line I am receiving the nil value. Any push in the right direction would be most helpful. Thanks!
After removing the array item. You need remove that item from the table too.When you try to delete the array item again, it shows nil, because that item is not available, but the table didn't remove that item from the view.So you need to remove it from table too...
genres.removeAtIndex(indexPath.row) // or section , delete according to your app
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
why do you take section of the item that you want to delete. You didn't delete the genre item from the table. You only deleted the moviesOfGenre. You need to delete the table view item also( genre ). Remove the genre also from the table.
Also you can use swipe to delete function :-
func tableView(tableView: UITableView!, canEditRowAtIndexPath indexPath: NSIndexPath!) -> Bool {
return true
}
func tableView(tableView: UITableView!, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath!) {
if (editingStyle == .Delete) {
// handle delete (by removing the data from your array and updating the tableview)
self.tableView.beginUpdates()
self.genres.removeObjectAtIndex(indexPath.row) // also remove an array object if exists.
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Left)
self.tableView.endUpdates()
}

Resources