Why is UICollectionView reloadData crashing my app? - ios

I am using uicollectionview to display some photos and I have a button that allows the user to delete the selected photo.
It works perfectly unless I try to delete the last photo in the array of photos being used to populate the uicollectionview. Ie if there are 5 photos then there will be a problem if a user removes the 5th photo but not the 1st, 2nd, 3rd or 4th. When I try to delete the 5th photo it crashes on reloadData() with the following error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 4 from section 0 which only contains 4 items before the update'
I don't understand why this would happen... The error even mentions "attempt to delete" but I never actually told it I was deleting anything. I just changed the source and then asked it to update. Also in the error message it says the section only contains 4 items before the update when their were actually 5 photos. Why?
A little bit more info about what I'm doing and how (Using Swift)...
I've got a ProgressPhotoSheetClass from which I have instantiated the object progressPhotoSheet
this progressPhotoSheet object has an array of photos and the photos can have priorities such as photo id, title etc
in my number of items in section I use
var numPics: Int = progressPhotoSheet.progressPhotos.count
return numPics
in my cellForItemAtIndexPath I use
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoHolder
cell.photoTitle.text = progressPhotoSheet.progressPhotos[indexPath.row].photoName
...etc..
When deleting the item I use
progressPhotoSheet.deletePhoto(progressPhotoSheet.progressPhotos[indexPath.row].photoId)
which deletes the photo from the device and from my core data and then reloads progressPhotoSheet.progressPhotos from using the modified core data so it is now exactly how it was before but without the deleted photo.
I then call
self.collectionView.reloadData()
Which should update the UICollectionView for the new data
I could understand if it felt there was a mismatch between what should be in the collection view and what is in the datasource if I were using
self.collectionView.deleteItemsAtIndexPaths(indexPaths)
because that would be saying ignored to get them to match we need to delete one item - here there is a possibility something could mismatch.. But surely using self.collectionView.reloadData() it doesn't matter what changes were made it should just look at what data is there now and update the UICollectionView accordingly....
So my question is... Why am I getting this error and what should I do to fix things so I don't get it?
Edit to include more info
Here is my telephoto Code
func deletePhoto(photoId: Int) {
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
// Fetch correct photo
let fetchRequest = NSFetchRequest(entityName: "CDProgressPhoto")
fetchRequest.predicate = NSPredicate(format: "photoId = %#", String(photoId))
// Save
if let fetchResults = managedContext.executeFetchRequest(fetchRequest, error: nil) as? [NSManagedObject] {
if fetchResults.count != 0{
// Will only be one photo with this photo id
var photo = fetchResults[0]
photo.setValue(true, forKey: "toDelete")
// Save the object
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
}
}
// Reload from core data
self.loadPhotoSheetFromCoreData()
}
self.loadPhotoSheetFromCoreData() then empties progressPhotoSheet.progressPhotos before getting the new data from core data... Code below...
private func loadPhotoSheetFromCoreData() {
if(self.hasPhotoSheet()) {
// Clear existing photos
self.progressPhotos = []
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "CDProgressPhoto")
let predicate1 = NSPredicate(format: "photoSheetId == %#", String(self.sheetId))
let predicate2 = NSPredicate(format: "toDelete == %#", false)
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false) as NSSortDescriptor]
var predicatesArray: [NSPredicate] = [predicate1, predicate2]
//predicatesArray.append(predicate1)
request.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(predicatesArray)
let existings = managedContext.executeFetchRequest(request, error: nil)
let existingPhotos: [CDProgressPhoto] = existings as! [CDProgressPhoto]
// for each photo make a ProgressPhoto object and add to progress photos array
for photo in existingPhotos {
var newPhoto: ProgressPhoto = ProgressPhoto()
newPhoto.photoSheetId = Int(photo.photoSheetId)
newPhoto.photoId = Int(photo.photoId)
newPhoto.photoName = photo.photoName
newPhoto.date = Int(photo.date)
newPhoto.url = photo.url
newPhoto.filename = photo.filename
newPhoto.height = Float(photo.height)
newPhoto.width = Float(photo.width)
newPhoto.selected = false
self.progressPhotos.append(newPhoto)
}
}
}
As you can see the photo isn't actually deleted at this point I just set a toDelete flag to true and then only re load items where toDelete is set to false. The photos are deleted later asynchronously depending on network connection etc because they are also stored on a server for use on the main website.

Have you tried calling invalidateLayout() on the collectionView? That might help incase your view is empty i.e. 0 elements are present.

Related

Fatal Error When Trying To Save Core Data Objects Via Relationship

I am adding a newly created user exercise to an existing user routine via a save command. To explain the code below, I set the objectcontext, then is the exercise is new (userExercise is nil) I execute a new save block.
I then check if this new exercise is being added to an existing routine (associatedRoutineToAddTo is a string routine name from the previous VC).
This is where the issue is. I attempt to get the UserRoutine object that it needs to be added to, using a predicate based on the routines name from the string i passed from the previous VC. then i attempt to add the exercise to the routine. This causes the crash.
The rest of the code isnt too relevant as it works fine in terms of saving an edit or new exercise with no parent routine, its just this one part.
Here is what I am trying:
func getMainContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
func createExercise() {
print("SAVE EXERCISE PRESSED")
let managedObjectContext = getMainContext()
if userExercise == nil {
print("SAVING THE NEW EXERCISE")
let newUserExercise = UserExercise(context: self.managedObjectContext!)
newUserExercise.name = userExerciseName.text
newUserExercise.sets = Int64(userSetsCount)
newUserExercise.reps = Int64(userRepsCount)
newUserExercise.weight = Double(self.userExerciseWeight.text!)!
newUserExercise.dateCreated = NSDate()
if self.associatedRoutineToAddTo != nil {
let existingUserRoutine = UserRoutine(context: managedObjectContext)
let request: NSFetchRequest<UserExercise> = UserExercise.fetchRequest()
request.predicate = NSPredicate(format: "usersroutine.name == %#", self.associatedRoutineToAddTo!)
existingUserRoutine.addToUserexercises(newUserExercise)
do {
try managedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
} else if self.associatedRoutineToAddTo == nil {
print("THIS IS A FRESH EXERCISE WITHOUT A PARENT ROUTINE")
}
}
The error reads:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'usersroutine' between objects in different contexts (source = <UserExercise: 0x618000280e60>
edit: revised my fetch code for review:
let fetchRequest: NSFetchRequest<UserRoutine> = UserRoutine.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "usersroutine.name == %#", self.associatedRoutineToAddTo!)
do {
existingUserRoutine = try managedObjectContext.fetch(fetchRequest) as! UserRoutine
print("Routine Below Fetched")
print(existingUserRoutine)
} catch {
print("Fetch Failed")
}
existingUserRoutine.addToUserexercises(newUserExercise)
You create the object here:
let newUserExercise = UserExercise(context: self.managedObjectContext!)
And fetch the related object here:
let existingUserRoutine = UserRoutine(context: managedObjectContext)
You've created a local variable called managedObjectContext here:
let managedObjectContext = getMainContext()
And your error states:
attempt to establish a relationship 'usersroutine' between objects in different contexts
Therefore, your property managedObjectContext is not the same as that returned by getMainContext()
In addition to all that, you're creating a brand new UserRoutine, and assigning it to a value called existingRoutine, then creating a fetch request that you don't do anything with, which suggests you're a little confused about what is supposed to be happening here.
WHen you make UserExcercise you are referencing a context saved on your VC:
let newUserExercise = UserExercise(context: self.managedObjectContext!)
Then
let existingUserRoutine = UserRoutine(context: managedObjectContext)
The getMainContext() acquire context here?

Weird issue with WatchKit and CoreData

I am playing around with WatchKit and CoreData and the demo app I wrote is working but sometimes gives unexpected results. Basically, it's a toDo app. You enter the toDo items on the iPhone and it stores them using CoreData in an entity called ToDoItem with 2 attributes: name (string) and completed (boolean). The name of items entered on the iPhone are assigned item.completed = false in the CoreData entity.
On the Apple Watch it queries CoreData on the iPhone and lists the names of the items that are not completed (item.completed == 0) in a WatchKit table view. When an item on the watch is selected the font changes to red, the row is deleted from the table, and it changes the completed value in CoreData to true (item.completed = true). The next time the Watch app is launched, the completed items no longer are listed in the table.
This works fine most of the time, but sometimes when an item is selected on the watch, it does not update CoreData with the item as item.completed = true. And once this happens, it no longer updates the items in CoreData when selected. No errors are given. If I reset the simulator, it starts working again but after a while does the same thing.
My code is below:
class InterfaceController: WKInterfaceController {
#IBOutlet var table: WKInterfaceTable!
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
var context = CoreDataStack.sharedInstance.managedObjectContext!
let request = NSFetchRequest(entityName: "ToDoItem") //CoreData entity ToDoItem contains two attributes - name (string) and completed (boolean)
let fetchItems: Array = context.executeFetchRequest(request, error: nil) as! [ToDoItem] //fetches the items in the entity
var counter = 0
for element in fetchItems{
counter++
println("The item name is \(element.name) and completed is \(element.completed)") //prints the items to the console
}
self.table.setNumberOfRows(fetchItems.count, withRowType: "ToDoRow")
var theCount = 0
for element in fetchItems {
if element.completed == 0 {
let row = self.table.rowControllerAtIndex(theCount) as? ToDoTableRowController
row?.nameLabel.setText(element.name) //populates the table with names of items that are not completed
theCount++
}
}
}
override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
//self.pushControllerWithName("ToDoDetail", context: nil)
let row = table.rowControllerAtIndex(rowIndex) as! ToDoTableRowController
row.nameLabel.setTextColor(UIColor.redColor()) //change the color of font to red when row is selected
var myIndex = NSIndexSet(index: rowIndex)
self.table.removeRowsAtIndexes(myIndex) //removes selected row from table
var context = CoreDataStack.sharedInstance.managedObjectContext!
let request = NSFetchRequest(entityName: "ToDoItem")
let items = context.executeFetchRequest(request, error: nil) as! [ToDoItem]
let item = items[rowIndex]
item.setValue(true, forKey: "completed") //changes the selected item to completed and saves it in coredata
//item.completed = true
var error: NSError?
if !context.save(&error) {
println("Cannot save due to \(error) \(error?.localizedDescription)")
}
}
How are you coordinating data between the host app and the watch extension? I'm using a Core Data store in the app group container and I found that the watch, the today widget, and the host app didn't get updates to managed objects until a save forced a merge to happen. (The managed object context doesn't notice the objects in the store have changed.) So it might be that the watch's context has the completed item but the host app still has an uncompleted item in the context.
The fix for me was to use MMWormhole so all three binaries can update each other when they touch a NSManagedObject subclass. I pass the object ID in the message and when a binary receives the message a call to
context.refreshObject(managedObject, mergeChanges: true) gets everyone back in sync.

Core Data array

I am using the code below to fetch the objects in an array. In my program, I have an array displayed in a tableview and when a cell is tapped, it leads to another array displayed in a tableview. The user can add cells in both of these tableviews. What is happening is that when I create new rows in my second tableview, go back, and tap the same cell that got me there, I notice that the objects I created are not there (they were reassigned to another cell). I believe that the problem lies in the line: routines = results. What I think is happening, is that when I tap back in my second view, the line routines = results is called again, and because results is by nature unordered, it messes up the order of my previously established routines array.
var routines = [NSManagedObject]()
override func viewWillAppear(animated: Bool) {
self.navigationItem.leftBarButtonItem = self.editButtonItem()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let fetchRequest = NSFetchRequest(entityName: "Routine")
var error: NSError?
let fetchedResults = managedContext.executeFetchRequest(fetchRequest, error: &error) as! [NSManagedObject]?
if let results = fetchedResults {
routines = results
} else {
println("Could not fetch \(error), \(error!.userInfo)")
}
}
I agree with the comment from Jonathan. You are just making work for yourself holding onto everything in arrays. Use the tools that are designed to work with both Core Data and UITableView controllers and you will have a much easier (and more maintainable) time.

Update CoreData Object

I'd like to update a CoreData Object.
Backgrund: I made an app which includes a UITableView. In the textLabel of the UITableViewCell is a name. In the detailTextLabel of this cell is a date which can be changed/updated. Now I'd like to change this date.
I wrote the following code:
var people = [NSManagedObject]()
func saveDate(date: NSDate) {
//1
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let managedContext = appDelegate.managedObjectContext!
//2
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext:managedContext)
let person = people[dateIndexPath.row]
//3
person.setValue(date, forKey: "datum")
//4
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
//5
people.append(person)
tableView.reloadData()
}
Now, if I run this code:
The date is successfully updated but the cell in which the date has been updated is displayed 2 times. For example if I added 3 cells and changed the date in the 3rd cell, I now get 4 cells displayed and 2 of them have the same content/are duplicated.
Does someone knows how to solve this problem?
You're adding an additional object to your array each time. Your updated Person is already in the array and will display the new information when you reload your table data. To fix this, just take out this line:
people.append(person)
You're going to want to associate some kind of unique identifier attribute to your Person class. This allows to retrieve that same object later using it identifier. I would suggest using a UUID string value, called personID, identifier, or something similar.
You can override the awakeFromInsert method on your Person class like so:
// This is called when a new Person is inserted into a context
override func awakeFromInsert()
{
super.awakeFromInsert()
// Automatically assign a randomly-generated UUID
self.identifier = NSUUID().UUIDString
}
When you want to edit an existing Person, you want to retrieve it by the UUID. I suggest a class function like so (in the Person class):
class func personWithIdentifier(identifier: String, inContext context: NSManagedObjectContext) -> Person?
{
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "identifier ==[c] %#", identifier)
fetchRequest.fetchLimit = 1 // Only want the first result
var error : NSError?
let results = context.executeFetchRequest(fetchRequest, error: &error) as [Person]
// Handle error here
return results.first?
}
This way you can use the following function:
let identifier = ...
let context = ...
var person = Person.personWithIdentifier(identifier, inContext: context)
if let person = person
{
// Edit the person
person.value = // change the values as you need
}
else
{
// Person does not exist!
person = // possibly create a person?
}

Delete Object from CoreData (error)

I'd like to delete an object from core data. I tried this code but it's not working. What's wrong? I'm working with Swift and Xcode 6.1.1.
Code:
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
var daten = [TaskModel]() //CoreData Entity
managedObjectContext!.deleteObject(daten[indexPath.row])
appDelegate.saveContext()
let fetchRequest = NSFetchRequest(entityName: "TaskModel")
daten = managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as [TaskModel]
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
daten is an empty array.
Therefore, daten[x] will crash.
In your case, x is indexPath.row, but it will crash with any value.
If you try to access an element of an empty array by index, you will get the above error.
To get an object from your fetched results controller and delete it,
let task = self.fetchedResultsController.objectAtIndexPath(indexPath) as TaskModel
self.fetchedResultsController.managedObjectContext.deleteObject(task)
self.fetchedResultsController.managedObjectContext.save(nil)
The standard implementation of the fetched results controller from the Xcode template will take care of updating your table view automatically, you even get animation for free.
If you are not using a NSFetchedResultsController, stop here and refactor.

Resources