I am building an app that uses a collectionView to display recipes. When the user scrolls to the bottom I call my server and fetch more recipes. Currently I call reloadData() after the server responds with new recipes. This works, but it reloads everything when all I need to do is load the new recipes. I've read similar posts indicating I can use insertItems - but for me this crashes with: libc++abi.dylib: terminating with uncaught exception of type NSException
Here is my code:
func updateRecipes(recipesToAdd: Array<Recipe>) {
let minNewRecipesIndex = (self.recipes.count + 1)
recipes += recipesToAdd
DispatchQueue.main.async {
if recipesToAdd.count == self.recipes.count {
self.collectionView?.reloadData()
} else {
let numberOfItems: [Int] = Array(minNewRecipesIndex...self.recipes.count)
self.collectionView?.insertItems(at: numberOfItems.map { IndexPath(item: $0, section: 0) })
// this crashes, but self.collectionView.reloadData() works
}
}
}
Even a simple hard coded - self.collectionView?.insertItems(at: IndexPath(item: 1, section: 0)) - crashes.
Two issues:
minNewRecipesIndex must be self.recipes.count. Imagine an empty array (.count == 0), the index to insert an item in an empty array is 0, not 1.
numberOfItems must be Array(minNewRecipesIndex...self.recipes.count - 1) or Array(minNewRecipesIndex..<self.recipes.count). Again imagine an empty array. Two items are inserted at indices 0 and 1, minNewRecipesIndex is 0 and self.recipes.count is 2, so you have to decrement the value or use the half open operator.
If the code still crashes use a for loop wrapped in beginUpdates() / endUpdates() and insert the items one by one at the last index.
Related
Good day,
I have an NSCollectionView which requires a lot of adding and delete. I have a function that adds to the end of the NSCollectionView perfectly and that is working effectively but my function to delete items in the NSCollectionView works sometimes but other times it throws an error below is the error
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
2021-02-19 09:08:34.718073+0100 Kokoca[33243:2652479] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
Ive saved a user default to tell me which section and item the user is deleting. I've made it so that the user cannot delete the first item due to it being very essential but when I try to delete other items it works sometimes until it crashes when I keep trying to delete more items. I'm not very familiar with collectionViews so any assistance would be appreciated. Thanks in advance. Below is my code:
#objc func deleteview() {
var newSearch = UserDefaults.standard.integer(forKey: "DeletedTab")
let newSection = UserDefaults.standard.integer(forKey: "DeletedSection")
if newSearch == 0 {
return
}
else {
self.theTabList.remove(at: newSearch)
print(newSearch)
print(newSection)
var set: Set<IndexPath> = [IndexPath(item: newSearch, section: newSection)]
tabCollectionView.deleteItems(at: set)
theViewList.remove(at: newSearch)
theViewList[newSearch].removeFromSuperview()
var defaults = UserDefaults.standard
var theCurrentTab = tabCollectionView.item(at: IndexPath(item: theTabList.count - 1, section: 0)) as! TabItem
theCurrentTab.isSelected = true
print("TheTabList: " + String(theTabList.count))
print(theTabList)
}
}
}
Any ideas why its buggy? would appreciate it.
While trying to retrieve subdata of a Data object, the application crashes issuing the following error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Below you can see the code. It's a Data extension. Hope someone can explain why this crashes.
public extension Data {
/// Removes and returns the range of data at the specified position.
/// - Parameter range: The range to remove. `range` must be valid
/// for the collection and should not exceed the collection's end index.
/// - Returns: The removed data.
mutating func remove(at range: Range<Data.Index>) -> Self {
precondition(range.lowerBound >= 0, "Range invalid, lower bound cannot be below 0")
precondition(range.upperBound < self.count, "Range invalid, upper bound exceeds data size")
let removal = subdata(in: range) // <- Error occurs here
removeSubrange(range)
return removal
}
}
EDIT - added the caller functions:
This extension is called from the following function:
func temporary(data: inout Data) -> Data {
let _ = data.removeFirst()
return data.remove(range: 0 ..< 3)
}
Which in turn is called like this:
var data = Data([0,1,2,3,4,5])
let subdata = temporary(data: &data)
You haven't provided enough information for us to know the reason of your crash. One thing that I know that is wrong in your method is your precondition. You wont be able to pass a range to remove all elements of your collection. Besides that you should implement a generic method that would take a RangeExpression instead of a Range. This is how I would implement such method:
extension Data {
/// Removes and returns the range of data at the specified position.
/// - Parameter range: The range to remove. `range` must be valid
/// for the collection and should not exceed the collection's end index.
/// - Returns: The removed data.
mutating func remove<R>(_ range: R) -> Data where R: RangeExpression, Index == R.Bound {
defer { removeSubrange(range) }
return subdata(in: range.relative(to: self))
}
}
Usage:
var data = Data([0,1,2,3,4,5])
let subdata = data.remove(0..<6)
print(Array(data), Array(subdata)) // "[] [0, 1, 2, 3, 4, 5]\n"
To check if your data indices contains a specific range before attempting to remove you can use pattern-matching operator:
var data = Data([0,1,2,3,4,5])
let range = 0..<7
if data.indices ~= range {
let subdata = data.remove(range)
print(Array(data), Array(subdata))
} else {
print("invalid subrange") // "invalid subrange\n"
}
If you would like to do the same with a ClosedRange you would need to implement your own pattern-matching operator on Range:
extension Range {
static func ~=(lhs: Self, rhs: ClosedRange<Bound>) -> Bool {
lhs.contains(rhs.lowerBound) && lhs.contains(rhs.upperBound)
}
}
Usage:
var data = Data([0,1,2,3,4,5])
let range = 0...5
if data.indices ~= range {
let subdata = data.remove(range)
print(Array(data), Array(subdata)) // "[] [0, 1, 2, 3, 4, 5]\n"
} else {
print("invalid subrange")
}
The error is caused by the removeFirst function. The documentation clearly states:
Calling this method may invalidate all saved indices of this collection. Do not rely on a previously stored index value after altering a collection with any operation that can change its length.
It appears that is exactly what is causing my error. I have replaced removeFirst with remove(at:) and it now works.
I am trying to move the bottom post in my array of posts to the top of my array of posts. I created the following code:
self.posts.insert(contentsOf: tempPosts, at: 0)
let element = self.posts.remove(at: tempPosts.endIndex)
self.posts.insert(element, at: 0)
let newIndexPaths = (0..<tempPosts.count).map { i in
return IndexPath(row: i, section: 0)
}
This code gives the error: Index out of range for the second line of the code chunk.
I tried the same code for moving elements, but I replaced tempPosts.endIndex with tempPosts.endIndex - 1. This works to move the second to last post in the array to the top. But when I change the code back to tempPosts.endIndex, it will not move the bottom post to the top of the array.
I tried adding the if statement:
if self.posts.count > 2 {
let element = self.posts.remove(at: tempPosts.endIndex)
self.posts.insert(element, at: 0)
}
But I got the same fatal error.
What is going wrong in my code and how can I trouble shoot it?
If your intent is to move the last element of your collection to the beginning of it you can insert the resulting element returned by the removeLast method. Just make sure to not call this method if the array is empty:
posts.insert(posts.removeLast(), at: 0)
You can also extend RangeReplaceableCollection and create a custom method as follow:
extension RangeReplaceableCollection where Self: BidirectionalCollection {
mutating func moveLastElementToFirst() {
insert(removeLast(), at: startIndex)
}
}
var test = [2,3,4,5,6,7,8,9,1]
test.moveLastElementToFirst()
test // [1, 2, 3, 4, 5, 6, 7, 8, 9]
var string = "234567891"
string.moveLastElementToFirst()
string // "123456789"
I'm having an issue with iPhone 5 specifically. In my UICollectionView's didSelectItemAt function, I have this snippet which iterates through all visible cells, and stops at the first cell which has a nil imageView.image.
for cell in heroPickerCollectionView.visibleCells {
let customIndex = heroPickerCollectionView.indexPath(for: cell)
let nilCell = heroPickerCollectionView.cellForItem(at: customIndex!) as! HeroPickerCell
if nilCell.imageView.image == nil {
selectedHeroArray.insert(heroData.heroes[(indexPath as NSIndexPath).row], at: (customIndex! as NSIndexPath).row)
nilCell.imageView.image = UIImage(named: heroData.heroes[(indexPath as NSIndexPath).row].imageName!)
checkForHeroes()
break
}
}
This works as intended on every device except iPhone 5. On iPhone 5, the customIndex always starts 0, 3 instead of 0, 0 (as it should be). Obviously this crashes because it attempts to insert at an index out of range.
I'm completely at a loss on this one. heroPickerCollectionView.visibleCells.count returns the correct count on iPhone 5, but the indexes are off.
I've setup a table to pull data from a database. The user can manually delete items from the table (and thus the database) via checkbox (table.editing = true, iirc) and a delete button. This can be done one at a time, or all at a time.
Unfortunately, whenever I check everything for deletion, the app crashes with the following error:
fatal error: Array index out of range
This does not happen if I select and delete only one or any number of the table rows, as long as I don't select everything.
Here's my code for the delete button:
func deleteButtonPressed(sender: AnyObject) {
if (self.pureSteamFormView.tableCalibration.editing == true) {
if (self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows!.count >= 1) {
for indexPath in self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows!.sort({ $0.row < $1.row}) {
let calibTable : FormSteamPurityCalibration = self.steamPurityCalibrationTableList[indexPath.row] /* <--- ERROR HERE */
DatabaseManager.getInstance().deleteData("FormSteamPurityCalibration", "ID = \(calibTable.ID)")
self.steamPurityCalibrationTableList.removeAtIndex(indexPath.row)
}
self.pureSteamFormView?.tableCalibration.reloadData()
}
}
}
Near as I can figure, it is attempting to remove the row at an index, an index that may no longer exist (?) due to the previous row also being deleted, but I'm not sure about that.
I tried putting the following code:
self.steamPurityCalibrationTableList.removeAtIndex(indexPath.row)
In its own for-loop block, and the error promptly move there.
I also tried removing the removeAtIndex part completely, relying on the reloadData() to perhaps update the table automatically, but it doesn't work - the data is deleted from the database, but remains on the table (although moving away from that view and going back there updates the table).
Any suggestions please? Thanks.
Your problem here is that you are deleting the lowest indexes before the bigger ones. Let me explain with an example:
Image you have 4 elements in your array:
let array = ["Element1", "Element2", "Element3", "Element4"]
You are trying to remove the elements at index 1 et 3:
for index in [1, 3] {
array.removeAtIndex(index)
}
Your program will first remove element at index 1, leaving you with the following array:
["Element1", "Element3", "Element4"]
On the second pass of the loop it will try to remove the element at index 3. Which does not exist anymore because it has moved to index 2.
One solution to this is to start removing element with the greater index before, so in your code you could change
for indexPath in self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows!.sort({ $0.row < $1.row}) {
to
for indexPath in self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows!.sort({ $0.row > $1.row}) {
A better solution would be to filter your data array to include only the elements you whish to keep, so instead of:
for indexPath in self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows!.sort({ $0.row < $1.row}) {
let calibTable : FormSteamPurityCalibration = self.steamPurityCalibrationTableList[indexPath.row]
DatabaseManager.getInstance().deleteData("FormSteamPurityCalibration", "ID = \(calibTable.ID)")
self.steamPurityCalibrationTableList.removeAtIndex(indexPath.row)
}
you could do:
self.steamPurityCalibrationTableList.filter {
if let index = self.steamPurityCalibrationTableList.indexOf ({ $0 })
{
for indexPath in self.pureSteamFormView.tableCalibration.indexPathsForSelectedRows! {
if indexPath.row == index { return false }
}
return true
}
}