Empty array causing my app to crash in Swift - ios

I keep coming across this error "fatal error: Index out of range", after researching this I am still unsure of exactly how to fix this issue. To give context I have started off with an empty array var playersArray = [UITextField]() so that users can enter their names in order to play the game. I then make sure that the user has or has not entered a value into the text field for each name slot
if let player1Name = name1.text, !player1Name.isEmpty
{ playersArray.append(name1)
} else {
print("Player 1 Empty")
If the player has entered a value into that textfield then ill append that value to the array.
The issue I have is that if I run the game and no user has entered a name into any of the 10 textfields then the game will crash. Im assuming this is because the array is empty?
The error appears on this line where I randomize the names used for the game with the number of elements in the array:
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
I assume if the array is empty then .count will not work?
How can I make sure the game wont crash if the array is empty?
CODE:
var playersArray = [UITextField]()
override func viewDidLoad() {
super.viewDidLoad()
textColor()
question1View.isHidden = true
questionLabel.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// Alert message on startup
func alertMessageOnStartUp(){
let alert = UIAlertController(title: "Warning!", message: "Please drink responsibly. By continuing, you agree that you are responsible for any consequences that may result from BottomsUp.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Agree", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
// Dismiss keyboard when tapped outside the keyboard
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
// Dimiss keybaord when return button is tapped
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
name1.resignFirstResponder()
name2.resignFirstResponder()
name3.resignFirstResponder()
name4.resignFirstResponder()
name5.resignFirstResponder()
name6.resignFirstResponder()
name7.resignFirstResponder()
name8.resignFirstResponder()
name9.resignFirstResponder()
name10.resignFirstResponder()
return(true)
}
//randomise background colour of each question page
func getRandomBackgroundColor() -> UIColor{
let randomRed:CGFloat = CGFloat(drand48())
let randomGreen:CGFloat = CGFloat(drand48())
let randomBlue:CGFloat = CGFloat(drand48())
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
func textColor(){
name1.textColor = UIColor.white
name2.textColor = UIColor.white
name3.textColor = UIColor.white
name4.textColor = UIColor.white
name5.textColor = UIColor.white
name6.textColor = UIColor.white
name7.textColor = UIColor.white
name8.textColor = UIColor.white
name9.textColor = UIColor.white
name10.textColor = UIColor.white
}
#IBAction func playButton(_ sender: Any) {
alertMessageOnStartUp()
if let player1Name = name1.text, !player1Name.isEmpty
{ playersArray.append(name1)
} else {
print("Player 1 Empty")
}
if let player2Name = name2.text, !player2Name.isEmpty
{ playersArray.append(name2)
} else {
print("Player 2 Empty")
}
if let player3Name = name3.text, !player3Name.isEmpty
{ playersArray.append(name3)
} else {
print("Player 3 Empty")
}
if let player4Name = name4.text, !player4Name.isEmpty
{ playersArray.append(name4)
} else {
print("Player 4 Empty")
}
if let player5Name = name5.text, !player5Name.isEmpty
{ playersArray.append(name5)
} else {
print("Player 5 Empty")
}
if let player6Name = name6.text, !player6Name.isEmpty
{ playersArray.append(name6)
} else {
print("Player 6 Empty")
}
if let player7Name = name7.text, !player7Name.isEmpty
{ playersArray.append(name7)
} else {
print("Player 7 Empty")
}
if let player8Name = name8.text, !player8Name.isEmpty
{ playersArray.append(name8)
} else {
print("Player 8 Empty")
}
if let player9Name = name9.text, !player9Name.isEmpty
{ playersArray.append(name9)
} else {
print("Player 9 Empty")
}
if let player10Name = name10.text, !player10Name.isEmpty
{ playersArray.append(name10)
} else {
print("Player 10 Empty")
}
question1View.isHidden = false
question1View.backgroundColor = getRandomBackgroundColor()
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
#IBAction func nextQuestionButton(_ sender: Any) {
question1View.backgroundColor = getRandomBackgroundColor()
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
}

Breaking this down:
Int(arc4random_uniform(UInt32(playersArray.count)))
This line gets a random number with a minimum value of 0 and a maximum value of the length of the playersArray minus 1.
I'm actually not sure what it does when the argument you pass in is 0, but it doesn't really matter, as we'll see next.
Then you use that random value here:
playersArray[thatRandomNumber]
Because there are no elements in playersArray, no matter what the value is of thatRandomNumber, it's going to be out of bounds.
You probably want something more like this:
let RandomPlayer = <some default value>
if !playersArray.isEmpty {
RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
}
EDIT
Your latest code still doesn't seem to do anything to prevent indexing into the empty array.
You have:
#IBAction func playButton(_ sender: Any) {
...
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
You need:
#IBAction func playButton(_ sender: Any) {
...
if playersArray.isEmpty {
// do something about that
} else {
let RandomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
let RandomQuestion = questionArray[Int(arc4random_uniform(UInt32(questionArray.count)))]
questionLabel.text = RandomPlayer.text! + RandomQuestion
}
}

playersArray.count for an empty array is 0, so you are trying to access playersArray[0] - but the array is empty, so nothing exists at the 0 index.
You should do something like this:
let randomPlayer: Player
if !playersArray.isEmpty {
randomPlayer = playersArray[Int(arc4random_uniform(UInt32(playersArray.count)))]
} else {
randomPlayer = Player() //create a fallback player
}
Alternatively you could make randomPlayer an optional, rather than providing a fallback value. Depends on your needs for that variable.

Related

How to add two numbers in Swift only when both of the text fields are filled in

I'm trying to add two numbers in Swift 5, and I want to add some error checks. I don't want to make it possible for a user to click on the plus button if both of the text fields are not filled in. I tried with the if state below but it did not work.
the whole function:
#IBAction func sum(_ sender: Any) {
let one = input1.text
let oneInt = Int(one!)
let two = input2.text
let twoInt = Int(two!)
let total = oneInt! + twoInt!
label.text = "\(total)"
if(input2.text == nil){
addBtn.isEnabled = false
}
if(input1.text == nil){
addBtn.isEnabled = false
}
}
Try to use guard like this. If your input field does not contain any value that field return blank string and when you try to get integer value from that string it will return nil and your add button will be disable.
#IBAction func sum(_ sender: Any) {
guard let text1 = input1.text, let intValue1 = Int(text1) else {
addBtn.isEnabled = false
return
}
guard let text2 = input2.text, let intValue2 = Int(text2) else {
addBtn.isEnabled = false
return
}
label.text = "\(intValue1 + intValue2)"
}
A nice and simple way is to addTarget to your textFiels. This will enable you to handle the events on the text field. In this scenario we'll use .editingChanged and use a single selector to achieve our goal:
What we'll do : We will listen for when someone types something in the textfield. Whenever a text changed was made, we'll check to see if all the textfields was populated and then if it was we enable the sum button.
A small controller sample :: Make sure to read the comments to understand the code faster
import UIKit
class ViewController: UIViewController {
#IBOutlet var textfield1: UITextField!
#IBOutlet var textfield2: UITextField!
#IBOutlet var sumButton: UIButton!
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
sumButton.isEnabled = false /// Disable the button first thing
[textfield1, textfield2].forEach {
$0.addTarget(self, action: #selector(editingChanged(_:)), for: .editingChanged) /// add targets to handle the events (in your case it listens for the 'editingChanged' event )
}
}
#objc func editingChanged(_ textField: UITextField) {
/// Here we just loop through all our textfields
for each in [textfield1, textfield2] {
if let text = each?.text { /// Just make sure the textfields text is not nil
if text.count < 1 {
// If the textfiels text has no value in, then we keep the button disabled and return
sumButton.isEnabled = false
return
}
} else {
/// Else if the text field's text is nill, then return and keep the button disabled
sumButton.isEnabled = false
return
}
}
sumButton.isEnabled = true /// If the code reaches this point, it means the textfields passed all out checks and the button can be enabled
}
#IBAction func sum(_ sender: Any) {
let one = textfield1.text!
let two = textfield2.text!
guard let oneInt = Int(one), let twoInt = Int(two) else {
print("Whatever was in that text fields, couldn't be converted to an Int")
label.text = "Be sure to add numbers."
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
}
textfields are not nil but empty strings. so make your comparison like :
if input1.text == "" {
// do your check here
}
Seems like you want to start with addBtn.isEnabled = false then update it whenever the user enters two valid integers into the text fields, i.e. Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil. You can do this by adding a target to your textfields (input1 and input2) for .editingChanged events. For example, if you're doing this in a UIViewController, you can do this in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
addBtn.isEnabled = false
input1.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
input2.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
}
Where textFieldDidEdit(_:) action looks like:
#objc func textFieldDidEdit(_ sender: UITextField) {
addBtn.isEnabled = Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil
}
Finally your sum function becomes:
#IBAction func sum(_ sender: UIButton) {
guard let oneInt = Int(input1.text ?? ""), let twoInt = Int(input2.text ?? "") else {
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
As all of the number validation has moved to the textFieldDidEdit(_:) function.

textField Editing Changed not reacting fast enough (Asynchronous calls)

I have a textfield that queries a firebase database for existing users and then display a UIImage according to if the user is available or not. The problem is that once the async code loads, the textfield doesn't react on changed value.
example. If i type 12345 as a username, i don't query the database. Everything ok. If i add a 6 it queries firebase and it shows me the user is free. if i press backspace and have 12345 the textFieldChanged is triggered again, and database is not queried. All OK.
but the problem is, when i have 12345, and i type 6 and very fast back so i have 12345, the query is running and shows me the available icon (because the back was pressed very fast). Is this because of the Simulator or is it a real problem and can i be fixed easily ?
my code:
#IBAction func textFieldChanged(_ sender: UITextField) {
if let username = usernameInputText.text, username.count > 5 {
checkIfUserExists(username: username) { doesExist in //(2)
if doesExist! {
self.completeSignupButton.isEnabled = false
self.ifAvailableImageView.image = UIImage(named: "Close")
} else {
self.completeSignupButton.isEnabled = true
self.ifAvailableImageView.image = UIImage(named: "Check")
}
}
} else {
ifAvailableImageView.image = UIImage(named: "Close")
self.completeSignupButton.isEnabled = false
}
}
func checkIfUserExists(username: String, completion: #escaping (Bool?) -> Void) {
spinner.startAnimating()
self.ifAvailableImageView.image = nil
let docRef = db.collection("users").document(username)
docRef.getDocument { (document, error) in
if error != nil {
self.spinner.stopAnimating()
completion(nil)
} else {
self.spinner.stopAnimating()
if let document = document {
if document.exists {
completion(true)
} else {
completion(false)
}
}
}
}
}
You can just compare the username being processed with the current text in the text field and not process the result if it not the same because you only want to process the latest one.
#IBAction func textFieldChanged(_ sender: UITextField) {
if let username = usernameInputText.text, username.count > 5 {
checkIfUserExists(username: username) { doesExist in //(2)
// Check if current text and the completion being processed are for the same username
if username != sender.text {
return
}
if doesExist! {
self.completeSignupButton.isEnabled = false
self.ifAvailableImageView.image = UIImage(named: "Close")
} else {
self.completeSignupButton.isEnabled = true
self.ifAvailableImageView.image = UIImage(named: "Check")
}
}
} else {
ifAvailableImageView.image = UIImage(named: "Close")
self.completeSignupButton.isEnabled = false
}
}

Update output as user types in textfield

I have code to sum four text fields and output the total in a label. Currently the code sums the fields after finishing editing, that is, selecting another text field. Is there a way to sum the text fields as the user types?
#IBAction func TankFuelChanged(_ sender: Any) {
let leftMainTankQuantityValue = Int(leftMainTankQuantity.text ?? "") ?? 0
let rightMainTankQuantityValue = Int(rightMainTankQuantity.text ?? "") ?? 0
let auxTankQuantityValue = Int(auxTankQuantity.text ?? "") ?? 0
let tailTankQuantityValue = Int(tailTankQuantity.text ?? "") ?? 0
let total = leftMainTankQuantityValue + rightMainTankQuantityValue + auxTankQuantityValue + tailTankQuantityValue
totalFuelLoad.text = "\(total)"
What you are looking for is an event triggered when text field changes. You can drag an action from storyboard or you can add them programmatically by using addTarget similar to UIButton but need to use event editingChanged. Check the following code:
var allTextFields: [UITextField] {
return [leftMainTankQuantity, rightMainTankQuantity, auxTankQuantity, tailTankQuantity]
}
override func viewDidLoad() {
super.viewDidLoad()
allTextFields.forEach { $0.addTarget(self, action: #selector(onTextFieldChange), for: .editingChanged) }
}
#objc private func onTextFieldChange() {
updateResult()
}
private func updateResult() {
let strings: [String] = allTextFields.compactMap { $0.text } // Will remove all nil texts
let integers: [Int] = strings.compactMap { Int($0) } // Will remove all non-integer texts
let sum = integers.reduce(0, { $0 + $1 }) // Will compute a sum
print(sum) // TODO: update your result here
}
A method must be marked #objc because of the #selector next to that I hope code speaks for itself.

How do I set up the buttons that are linked to didPressNumber to add to each other when pressed

How do I set up the buttons that are linked to didPressNumber to add to each other when pressed so lets say its a calculator and I want set it up where each button is pressed has a letter and number value when it is pressed it adds to the previous one press and I want to set up 2 labels one displaying the number value and one displaying the letter value and how would I set up the value of each number?
enum modes {
case not_set
case addition
case subtraction
case equals
}
#IBAction func didPressNumber(_ sender: UIButton) {
let stringValue:String? = sender.titleLabel?.text
if (lastButtonWasMode) {
lastButtonWasMode = false
labelString = "0"
}
labelString = labelString.appending(stringValue!)
updateText()
}
func updateText() {
guard let labelInt:Int = Int(labelString) else {
return
}
if (currentMode == .not_set) {
savedNum = labelInt
}
let formatter: NumberFormatter = NumberFormatter()
formatter.numberStyle = .decimal
let num:NSNumber = NSNumber(value: labelInt)
label.text = formatter.string(from: num)
}
func changeMode(newMode:modes) {
if (savedNum == 0) {
return
}
currentMode = newMode
lastButtonWasMode = true
}

How do I update textfield2 based on editing textfield1?

I like to update textfield2 as soon as totalAmount has been edited, I am trying the following:
totalAmount.addTarget(self, action: "changeTextField2:", forControlEvents: UIControlEvents.EditingDidEndOnExit)
func changeTextField2(textField: UITextField) {
print("Total amount: \(totalAmount.text)")
//amount vat - calculate vat based on textField
amountVAT.text = "88"
}
And i am saving the text field values with
func newItem(){
let context = self.context
let ent = NSEntityDescription.entityForName("List", inManagedObjectContext: context)
let nItem = List(entity: ent!, insertIntoManagedObjectContext: context)
let numberFormatter = NSNumberFormatter()
nItem.amountVAT = numberFormatter.numberFromString(amountVAT.text!)
.....more values
nItem.invoiceImage = UIImagePNGRepresentation(imageHolder.image!)
do {
try context.save()
} catch {
print(error)
}
}
func saveValues(sender: AnyObject) {
if nItem != nil {
editItem()
} else {
newItem()
}
dismissVC()
}
This can be done using UITextFieldDelegate Method use this function
1.)
textField1.delegate = self
2.)
func textFieldDidEndEditing(textField: UITextField)
{
if textField == textField1
{
textField2.text = textField1.text
}
}
now its upto you how you change the totalAmount.
Use the delegate function of UITextField
- textFieldDidEndEditing: for your textField1. Once in this function, you can get value of textField1 and you can edit textField2.
Apple Documentation for UITextFieldDelegate
Change the code to this totalAmount.addTarget(self, action: "changeTextField2:", forControlEvents: UIControlEvents.EditingChanged)
i.e change the UIControlEvents to "EditingChanged"

Resources