Issue with iPhone5 UICollectionView.visibleCells index - swift

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.

Related

insertItemsAt collection view crashes

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.

Swift - Why is my application finding nil in this scenario

I have an tableView which loads cells from a xib file. The cells contain 2 labels, 1 shows the quantity and the second shows the price. The quantity is pulled from a static model and works just fine, and the price is pulled from a model which is populated from the result of a URLSession. Below is my cellForRowAt function which works fine in this scenario:
if indexPath.section == 0 {
let item: BasketModel = cellItems[indexPath.row] as! BasketModel
let ordersCell = Bundle.main.loadNibNamed("OrderTableViewCell", owner: self, options: nil)?.first as! OrderTableViewCell
ordersCell.priceLabel.text = item.price
ordersCell.quantityLabel.text = String(basketStruct.getQty(id: item.id!))
return ordersCell
}
My cell shows as follows Qty: 3 Price: 0.70
Now, the problem I have is, when i alter the line above where I set the price label to the following:
ordersCell.priceLabel.text = String(basketStruct.getQty(id: item.id!) * Int(item.price!)!)
I get the error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
What I want, is the qty to be multiplied by the price. However, when I try to use item.price for anything I have to force unwrap it and then I get my error.
How would I go about this ?
It is not that item.price is nil, but Int(item.price!) is nil.
From your question, you said that the price displayed is 0.70, so I suppose item.price must contain the string "0.70".
Well, 0.70 is not a valid Int, so when you try to parse it as an Int, it evaluates to nil.
I think you meant Double(item.price!)!.
As a good practise, you should always check for invalid number strings:
if let price = Double(item.price!) {
// ...
} else {
print("Invalid price: \(item.price!)")
}

Swift: fatal error: Array index out of range on deleting item from a tableview

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
}
}

How do I 'seed' using GKRandomSource from Swift's Gameplaykit to remember the shuffle between sessions

I'm new to programming and have been learning Swift by doing a number of online courses. In one of the courses we built a basic trivia game and I've been progressively trying to improve it by doing my own coding (best way to learn!).
Recently I came across what's called a Fisher-Yates shuffle and, after much trial and error (and with the help of the stack overflow community) was able to use GKRandomSource from Swift's Gameplaykit to shuffle my trivia questions around so that they were being asked randomly. This was an improvement on the original arc4random code I was using because the shuffle removed questions already asked from the overall pool of questions, thereby ensuring that they did not repeat (at least in iOS9).
This works well within the session, but once the user quits the app and relaunches it, the shuffle starts from scratch. So I was looking at a way to have the app 'remember' the questions already asked between sessions. My research led me to the idea of seeding and I've been trying to get this to work with my GKRandomSource code, but I'm obviously missing something.
Any advice etc would be most welcome - especially since I'm not entirely sure that this 'seeding' approach will achieve my ultimate aim of not repeating questions already asked in previous sessions of the app.
Below are what I believe to be the relevant bits of my revised code.
All questions and potential answer choices are stored in a .json file as such:
{
"id" : "1",
"question": "Earth is a:",
"answers": [
"Planet",
"Meteor",
"Star",
"Asteroid"
],
"difficulty": "1"
}
I use the following code to load the .json file:
func loadAllQuestionsAndAnswers()
{
let path = NSBundle.mainBundle().pathForResource("content", ofType: "json")
let jsonData : NSData = NSData(contentsOfFile: path!)!
allEntries = (try! NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.MutableContainers)) as! NSArray
//println(allEntries)
}
And below is my most recent code for trying to achieve the shuffle of all questions and replicate it in future sessions):
var allEntries : NSArray!
var shuffledQuestions: [AnyObject]!
var nextQuestion = -1
var mySeededQuestions : [AnyObject]
loadAllQuestionsAndAnswers()
if #available(iOS 9.0, *) {
let lcg = GKLinearCongruentialRandomSource(seed: mySeededQuestions)
let shuffledQuestions = lcg.arrayByShufflingObjectsInArray(allEntries)
nextQuestion++
loadQuestion(nextQuestion)
// Fallback on earlier versions
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know at the very least I have a problem with the above code, but I'm at a loss. I'm also thinking that maybe I'm missing a step in terms of storing the seed?
For the sake of completeness, I use a label to display the question and four images to display the potential answers, using the following code:
func loadQuestion(index : Int)
{
let entry : NSDictionary = shuffledQuestions[index] as! NSDictionary
let question : NSString = entry.objectForKey("question") as! NSString
let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray
//println(question)
//println(arr)
labelQuestion.text = question as String
let indices : [Int] = [0,1,2,3]
//let newSequence = shuffle(indices)
let newSequence = indices.shuffle()
var i : Int = 0
for(i = 0; i < newSequence.count; i++)
{
let index = newSequence[i]
if(index == 0)
{
// we need to store the correct answer index
currentCorrectAnswerIndex = i
}
let answer = arr.objectAtIndex(index) as! NSString
switch(i)
{
case 0:
buttonA.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 1:
buttonB.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 2:
buttonC.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 3:
buttonD.setTitle(answer as String, forState: UIControlState.Normal)
break;
default:
break;
}
}
buttonNext.hidden = true
// we will need to reset the buttons to reenable them
ResetAnswerButtons()
}
func loadQuestionPreiOS9(index : Int)
{
let entry : NSDictionary = allEntries.objectAtIndex(index) as! NSDictionary
let question : NSString = entry.objectForKey("question") as! NSString
let arr : NSMutableArray = entry.objectForKey("answers") as! NSMutableArray
//println(question)
//println(arr)
labelQuestion.text = question as String
let indices : [Int] = [0,1,2,3]
//let newSequence = shuffle(indices)
let newSequence = indices.shuffle()
var i : Int = 0
for(i = 0; i < newSequence.count; i++)
{
let index = newSequence[i]
if(index == 0)
{
// we need to store the correct answer index
currentCorrectAnswerIndex = i
}
let answer = arr.objectAtIndex(index) as! NSString
switch(i)
{
case 0:
buttonA.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 1:
buttonB.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 2:
buttonC.setTitle(answer as String, forState: UIControlState.Normal)
break;
case 3:
buttonD.setTitle(answer as String, forState: UIControlState.Normal)
break;
default:
break;
}
}
buttonNext.hidden = true
// we will need to reset the buttons to reenable them
ResetAnswerButtons()
}
Finally, I use the following code to present the user with a 'Next' button after they've answered a question:
#IBAction func PressedButtonNext(sender: UIButton) {
print("button Next pressed")
if #available(iOS 9.0, *) {
nextQuestion++
loadQuestion(nextQuestion)
}else{
let randomNumber = Int(arc4random_uniform(UInt32(allEntries.count)))
loadQuestionPreiOS9(randomNumber)
}
I know my coding is probably quite verbose and unnecessary, but up until this latest improvement it's been working fine and I actually understand most of it (I think!)
There are really two questions here: what you're asking about and what you seem to want. They're both worth answering for different reasons, so...
How to seed a GK(Whatever)RandomSource
(All the GKRandomSource subclasses have seeds, even though the superclass GKRandomSource itself doesn't... that's because each class has its own data type for seeds. But the usage is the same.)
The critical bits of the code you've posted don't even compile due to a type mismatch: the seed/init(seed:) value for GKLinearCongruentialRandomSource is an integer, not an array of objects. The documentation for that value spells out what it's for (emphasis added) and how to use it:
Any two random sources initialized with the same seed data will generate the same sequence of random numbers. To replicate the behavior of an existing GKLinearCongruentialRandomSource instance, read that instance’s seed property and then create a new instance by passing the resulting data to the initWithSeed: initializer.
So, if you want to replicate a sequence of random numbers:
Create a random source with the plain initializer.
let source = GKLinearCongruentialRandomSource()
Save off that source's seed value.
let seed = source.seed // -> some UInt64 value
// write seed to user defaults, a file, a web service, whatever.
Use that random source for whatever.
Later, when you launch again and want the same sequence, read in the seed value and create a random source using the seed.
let seed = // read in seed value from wherever you saved it
let source = GKLinearCongruentialRandomSource(seed: seed)
This still doesn't get you what you're actually looking for, though: If source in step 1 produced the sequence 1, 6, 3, 9, 2, 7, source from step 4 will also produce the sequence 1, 6, 3, 9, 2, 7 — the seed doesn't record where you "left off" in a sequence. Or, since you're using it for an array shuffle, it'll produce the same shuffled ordering of the array as the first shuffle, but it doesn't remember what you did with the shuffled array thereafter.
How to use a shuffled ordering across multiple app launches
If you want to shuffle an array, walk through it in order, and then on a later run of your app continue walking through the same shuffled array from where you left off, you need to design around that requirement.
Shuffle on the first launch.
Record something about the ordering produced. (Say, a mapping of indices in the shuffle to indices in the original data.)
When walking through the shuffled array, record how far you've gone through it.
On later runs of the app, use the record of the ordering and the record of progress to decide where you are.
Here's a rough pass at that. (Note that I'm not touching your data model — this is a program design question, and SO is not a coding service. You'll need to think about how to flesh out this design to match your model and its use cases.)
struct Defaults {
static let lastQuestionIndex = "lastQuestionIndex"
static let questionOrder = "questionOrder"
}
let questions: [Question] // array of model objects, always in fixed order
func nextQuestion() -> Question {
let defaults = NSUserDefaults.standardUserDefaults()
if let lastIndex = defaults.integerForKey(Defaults.lastQuestionIndex) {
// we've run before, load the ordering
guard let shuffledOrder = defaults.arrayForKey(Defaults.questionOrder) as? [Int]
else { fatalError("save questionOrder with lastQuestionIndex") }
// advance the saved index so the next call to this function
// will get the next question
if lastIndex + 1 < count {
defaults.setInteger(lastIndex + 1, forKey: Defaults.lastQuestionIndex)
} else {
// ran out of shuffled questions, forget the order so we
// can reshuffle on the next call
defaults.removeObjectForKey(Defaults.questionOrder)
defaults.removeObjectForKey(Defaults.lastQuestionIndex)
}
// map lastQuestionIndex from sequential to shuffled
// and return the corresponding answer
let shuffledIndex = shuffledOrder[lastIndex]
return questions[shuffledIndex]
} else {
// first run, shuffle the question ordering (not the actual questions)
let source = GKRandomSource()
let sequentialOrder = Array(0..<questions.count)
let shuffledOrder = source.arrayByShufflingObjectsInArray(sequentialOrder)
// save the ordering, and the fact that we're asking the first question
defaults.setObject(shuffledOrder, forKey: Defaults.questionOrder)
defaults.setInteger(0, forKey: Defaults.lastQuestionIndex)
// return the first question in the shuffled ordering
let shuffledIndex = shuffledOrder[0]
return questions[shuffledIndex]
}
}
That's probably a bit pseudocode-ish (so you might need to worry about casting arrays to work with NSUserDefaults, etc), but as a general design it should be enough to give you some food for thought.
You can also use the following to drop off a certain amount of values so if you keep a roll count dropping that many on next start is as easy as :
arc4.dropValues(rollCount)

Wrong selection in ios 7 with same code

I am Using This code to select my default index title for table in my drop down.
When i run this code on ios 6 it enters in the else part and when i run it on ios 7 it enters in the if part whereas it should enter in the else part.
Please help me on this.
-(void)setSelectedIndexPath:(NSIndexPath *)selectedIndexPath
{
_selectedIndexPath = selectedIndexPath;
if (_selectedIndexPath.row == NSNotFound) {
[self.selectedValueLabel setText:self.title];
}
else{
[self.selectedValueLabel setText:[self.dataSource dropDown:self optionTitleForRowAtIndexPath:_selectedIndexPath]];
}
}
There are few changes in iOS 7 where nil is being used instead of a sentinel value. Set a break point and check if the indexPath is nil for some reason. If it is, [nil row] would return 0 and not NSNotFound.