Getting duplicate tableview cells after first time - ios

In my app, there is an add button, when clicked will add a new tableviewcell. However the problem is I am getting duplicate cells after the first time. Essentially, I am getting 2 duplicates when I click second time, 3 on third and so on.... Please see the images for the screenshot:
The above image is the first screen. Clicking '+' will add a new cell, which looks like the below images:
Now after saving,
Now if I go out of this view and come back, click add again and create a new cell, it gives me duplicates as mentioned in the above paragraphs (that is, 2 for second time, 3 for third time and so on...).
Here is the screenshot:
Here is the code I am using:
#IBAction func addBasket(_ sender: UIButton) {
let prefs = UserDefaults.standard
let status = prefs.string(forKey: "status")
if(status != nil){
if( status == "pending"){
if(BasketItemList.count < 5 ) {
self.addpost()
} else {
let alert = SCLAlertView()
alert.showWarning("Basket", subTitle: "Upgrade to add more")
}
} else {
if(BasketItemList.count < 50 ) {
self.addpost()
} else {
let alert = SCLAlertView()
alert.showWarning("Basket", subTitle: "Upgrade to add more")
}
}
}
}
func addpost() {
let appearance = SCLAlertView.SCLAppearance(
showCloseButton: false
)
let alert = SCLAlertView(appearance : appearance)
let txt = alert.addTextField("Enter name")
alert.addButton("Save") {
if txt.text?.characters.count != 0 {
let basketname : String = txt.text!
let userID = FIRAuth.auth()?.currentUser?.uid
let postitem : [String :AnyObject] = ["userid" : userID! as AnyObject , "basketname" : basketname as AnyObject ]
let dbref = FIRDatabase.database().reference()
dbref.child("BasketList").childByAutoId().setValue(postitem)
self.Basketdata2()
let appearance = SCLAlertView.SCLAppearance(
kDefaultShadowOpacity: 0,
showCloseButton: false
)
let alertView = SCLAlertView(appearance: appearance)
alertView.showTitle(
"Saved", // Title of view
subTitle: "", // String of view
duration: 2.0, // Duration to show before closing automatically, default: 0.0
completeText: "Done", // Optional button value, default: ""
style: .success, // Styles - see below.
colorStyle: 0xA429FF,
colorTextButton: 0xFFFFFF
)
} else {
let alert = SCLAlertView()
alert.showError("Oops!", subTitle: "Basket name should not be empty")
self.tableviewbasket.reloadData()
}
}
alert.addButton("Cancel"){
}
alert.showEdit("Add basket", subTitle: "Please enter your basket name")
}
func Basketdata2() {
HUD.show(.labeledProgress(title: "Loading...", subtitle: ""))
let databaseref = FIRDatabase.database().reference()
var userID = FIRAuth.auth()?.currentUser?.uid
if userID == nil {
userID = userfbid
}
databaseref.child("BasketList").queryOrdered(byChild: "userid").queryEqual(toValue: userID!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
self.tableviewbasket.backgroundView = nil;
HUD.hide()
} else {
HUD.hide()
self.tableviewbasket.setContentOffset(CGPoint(x : 0, y: -98), animated: true)
if (self.BasketItemList.count == 0) {
// tableView is empty. You can set a backgroundView for it.
let label = UILabel(frame: CGRect(x: 5, y: 0, width: self.tableviewbasket.bounds.size.width, height:self.tableviewbasket.bounds.size.height))
label.text = "The best preparation for tomorrow \n is doing your best today.\n Please create your first basket."
label.textColor = UIColor.black;
label.textAlignment = .center
label.numberOfLines = 4
label.sizeToFit()
label.font = UIFont(name: "AvenirNext-Regular", size: 16.0)
self.tableviewbasket.backgroundView = label;
self.tableviewbasket.separatorStyle = .none;
}
}
})
}
func Basketdata() {
HUD.show(.labeledProgress(title: "Please wait...", subtitle: ""))
self.BasketItemList.removeAll()
self.Basketid.removeAll()
let databaseref = FIRDatabase.database().reference()
let userID = FIRAuth.auth()?.currentUser?.uid
databaseref.child("BasketList").queryOrdered(byChild: "userid").queryEqual(toValue: userID!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
databaseref.child("BasketList").queryOrdered(byChild: "userid").queryEqual(toValue: userID!).observe(.childAdded, with: {
(snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject] {
let basketitem = BasketList(text : "")
basketitem.setValuesForKeys(dictionary)
self.BasketItemList.append(basketitem)
self.Basketid.append(snapshot.key)
DispatchQueue.main.async {
if !self.BasketItemList.isEmpty {
HUD.hide()
self.tableviewbasket.reloadData()
}
}
} else {
self.tableviewbasket.reloadData()
HUD.hide()
}
})
} else {
if (self.BasketItemList.count == 0) {
// tableView is empty. You can set a backgroundView for it.
let label = UILabel(frame: CGRect(x: 5, y: 0, width: self.tableviewbasket.bounds.size.width, height:self.tableviewbasket.bounds.size.height))
label.text = "The best preparation for tomorrow \nis doing your best today"
label.textColor = UIColor.black;
label.textAlignment = .center
label.numberOfLines = 2
label.sizeToFit()
label.font = UIFont(name: "AvenirNext-Regular", size: 16.0)
self.tableviewbasket.backgroundView = label;
self.tableviewbasket.separatorStyle = .none;
}
HUD.hide()
}
})
}
Can someone help me understand what is wrong with my code? Thanks!
Edit: I have referred this thread without any luck: Getting duplicate cells with UITableViewController cellForRowAtIndexPath
Edit 2: Also, when I come out of that view and go to the same view, the duplicate values are vansihed.
Edit 3: Tried the answer without any success.

Follow below steps:
When you'r getting data from firebase db, first remove your array all objects that you'r using in the cellForRow method. In your case i think it should be array of Bucket (not sure).
Assign data to your object
reload tableview.
Reason of replication of data.
let your bucket have 2 values and it is stored inDB. When you fetch data from DB it gives you all the values i.e. 1,2,3. Now you adds these now your array will be 1,2,1,2,3.
Thats what happening in your case.

Related

Hangman Game letters reset each time the button is pressed [duplicate]

This question already exists:
I am making a HangMan game and I'm having problems with letters being doubled each time it loops [closed]
Closed last month.
So I've been creating a hangman game and been having problems with each time a user guesses a correct letter again, the previous correct letter/letters gets removed.??
I tried a lot of if elses to hold each index of the letter and append if its correct but that did not work. So in general let's say a user got their first letter correct which was A. This is what happens..
XXXAXXAX
But the Second letter they get correct, lets say B this happens...
XBXXXXXX
they are supposed to bring A along to the next iteration and so onn.
can anyone see what's the problem? Here's my code below..
class ViewController: UIViewController {
var allWords = [String]()
var usedLetters = [String]()
var startWords = [String]()
var promptWord = String()
var randomWord = ""
override func viewDidLoad() {
super.viewDidLoad()
EnterGuess()
fileWork()
print(randomWord)
title = "GUESS A Letter: ?????????)"
}
func fileWork() {
if let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
if let startWords = try? String(contentsOf: startWordsURL) {
allWords = startWords.components(separatedBy: "\n")
let wordssss = startWords.randomElement()
randomWord = allWords.randomElement()!
}
}
}
func EnterGuess() {
let ac = UIAlertController(title: "Guess a letter", message: nil, preferredStyle: .alert)
ac.addTextField()
var submitGuess = UIAlertAction(title: "Submit", style: .default) {_ in
guard var answer = ac.textFields?[0].text else {return }
answer
self.submit(answer)
self.EnterGuess()
}
ac.addAction(submitGuess)
present(ac, animated: true)
}
func submit(_ answer: String) {
let guessedLetter = answer
let wordArray = randomWord.map(String.init)
var hidden = Array(repeating: "x", count: randomWord.count)
for index in hidden.indices {
if wordArray[index] == answer {
hidden[index] = wordArray[index]
}
}
print(hidden.joined())
// print(wordArray)
title = String(describing: hidden.joined())
}
}
The problem is that you are "re-setting" the hidden string with each letter guess.
Instead, you want to create a class-level var:
var hidden = [String]()
and "re-set" it to "xxxxxx..." when you start a new game.
Then, with each guess, you'll be keeping the old letters and replacing new letters.
Suppose the game word is EASY:
hidden is "xxxx"
guessed letter is "A"
hidden goes from "xxxx" to "xAxx"
guessed letter is "Y"
hidden goes from "xAxx" to "xAxY"
guessed letter is "E"
hidden goes from "xAxY" to "EAxY"
and so on.
Here's a quick example you can run:
class HangmanViewController: UIViewController, UITextFieldDelegate {
var allWords: [String] = [
"easy",
"birthday",
"laboratory",
"hangman",
"iphone",
"requirements",
"gaming",
]
var usedLetters = [String]()
var startWords = [String]()
var promptWord = String()
var randomWord = ""
var hidden = [String]()
let tf = UITextField()
let progressLabel = UILabel()
let usedLabel = UILabel()
let newGameButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
tf.textAlignment = .center
tf.borderStyle = .roundedRect
tf.textAlignment = .center
tf.font = .monospacedSystemFont(ofSize: 24.0, weight: .light)
tf.delegate = self
progressLabel.textAlignment = .center
progressLabel.font = .monospacedSystemFont(ofSize: 24.0, weight: .light)
progressLabel.backgroundColor = .yellow
usedLabel.textAlignment = .center
usedLabel.font = .monospacedSystemFont(ofSize: 16.0, weight: .light)
usedLabel.backgroundColor = .cyan
newGameButton.setTitle("New Game", for: [])
newGameButton.setTitleColor(.white, for: .normal)
newGameButton.setTitleColor(.lightGray, for: .highlighted)
newGameButton.backgroundColor = .systemBlue
newGameButton.layer.cornerRadius = 8
newGameButton.layer.borderColor = UIColor.blue.cgColor
newGameButton.layer.borderWidth = 1
[tf, progressLabel, usedLabel, newGameButton].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tf.widthAnchor.constraint(equalToConstant: 80.0),
tf.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
tf.centerXAnchor.constraint(equalTo: g.centerXAnchor),
progressLabel.widthAnchor.constraint(equalToConstant: 300.0),
progressLabel.topAnchor.constraint(equalTo: tf.bottomAnchor, constant: 40.0),
progressLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
usedLabel.widthAnchor.constraint(equalToConstant: 300.0),
usedLabel.topAnchor.constraint(equalTo: progressLabel.bottomAnchor, constant: 40.0),
usedLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
newGameButton.widthAnchor.constraint(equalToConstant: 200.0),
newGameButton.topAnchor.constraint(equalTo: usedLabel.bottomAnchor, constant: 40.0),
newGameButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
newGameButton.addTarget(self, action: #selector(newGameTapped(_:)), for: .touchUpInside)
newGame()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// upper-case the entered letter
let s = string.uppercased()
// we don't want to process if a string was pasted into the field
if s.count != 1 {
return false
}
// only allow A - Z
if s.rangeOfCharacter(from: .uppercaseLetters) == nil {
return false
}
// replace the current text
textField.text = s
// process the entered letter
submit(s)
// don't let the textfield process the string
return false
}
func newGame() {
// cycle the array of game words
allWords.append(allWords.removeFirst())
// safely unwrap
guard let w = allWords.first else {
fatalError("Bad setup")
}
// upper-case the word
randomWord = w.uppercased()
// set hidden string to "####..."
hidden = Array(repeating: "#", count: randomWord.count)
// clear used letters
usedLetters = []
// update the game progress label
progressLabel.text = String(hidden.joined())
progressLabel.textColor = .black
// update used letters label
usedLabel.text = " "
// hide the new game button
newGameButton.isHidden = true
// re-enable text field
tf.isUserInteractionEnabled = true
// for development
print("New Game Word is:", randomWord)
}
func submit(_ answer: String) {
if usedLetters.contains(answer) {
return
}
usedLetters.append(answer)
usedLabel.text = usedLetters.joined()
let wordArray = randomWord.map(String.init)
for index in hidden.indices {
if wordArray[index] == answer {
hidden[index] = wordArray[index]
}
}
progressLabel.text = hidden.joined()
if hidden == wordArray {
progressLabel.textColor = .red
// clear and disable text field
tf.text = ""
tf.isUserInteractionEnabled = false
// dismiss keyboard
view.endEditing(true)
// show the new game button
newGameButton.isHidden = false
}
}
#objc func newGameTapped(_ sender: UIButton) {
newGame()
}
}
It will look about like this (the cyan label will show the used letters). I'll type T, R, Y, S, M, E, A:

String letter by letter animation get mixed up when retrieving the next text

I have a pop up that is supposed to show the user instructions with letter by letter animation. The problem is whenever the user clicks "next" the letters get mixed up with the previous text.
The animation code:
extension UILabel {
func animate(newText: String, characterDelay: TimeInterval) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + characterDelay * Double(index)) {
self.text?.append(character)
}
}
}
}
The button action/calling method (as you can see I tried to empty the variable each time the user clicks "next" but it didn't work out)
#IBAction func nextbtt(_ sender: Any) {
var instructions = ["text"]
counter = counter + 1
var w1 = " لكن الوصول إليه يتطلب مواجهة وحل تحديات مختلفة"
var w2 = "هل بإمكانك مساعدتي في الحصول على الكنز؟"
let userId = UserDefaults.standard.object(forKey: "userId") as? String
ref = Database.database().reference()
let userLang = ref.child("users").child(userId!).child("lang")
userLang.observeSingleEvent(of: .value, with: { (snapshot) in
let lang = snapshot.value as? Int
if(lang==1){
////////////// if user's langague is English
w1 = "But finding it requires confronting and solving different challenges"
w2 = " Could you help me in getting the treasure" }
instructions.append(w1)
instructions.append(w2)
if(self.intrCounter < 3){
self.mytext.text = ""
var new = instructions[self.intrCounter]
self.mytext.text = new
self.mytext.animate(newText: new ?? "May the source be with you", characterDelay: 0.1)
self.intrCounter = self.intrCounter + 1
if(self.intrCounter == 3){
if(lang==1){
(sender as AnyObject).setBackgroundImage(UIImage(named: "engready"), for: UIControl.State.normal)
}
else {
(sender as AnyObject).setBackgroundImage(UIImage(named: "ready"), for: UIControl.State.normal)}
}
}
else{
}
})
if ( counter == 4){
status[0] = true
popUp.removeFromSuperview()
}
}
Screenshots:
one: text is showing to the user, user clicks "next" at the middle of the animation
two: when user clicks next before text 1 is complete
Code:
The issue occurs because you are calling an asynchronous block for each character and probably each char takes another amount of time.
Just to test this try this change, if it will help change the code accordingly :
var someCounter = 1
extension UILabel {
func animate(newText: String, characterDelay: TimeInterval) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
someCounter += 1
DispatchQueue.main.asyncAfter(deadline: .now() +
someCounter + characterDelay * Double(index)) {
self.text?.append(character)
}
}
}
}
Let me know if this is indeed the issue, if so I will upload a more optimised code.

How can I create multiple button by SwiftyJson and Almofire in swift 4?

These are my effort:
var cats = [[String:AnyObject]]()
func getAllCats(){
_ = EZLoadingActivity.show("Loading...", disableUI: true)
let param: [String: AnyObject] = ["apiKey": "???" as AnyObject]
_ = Alamofire.request(APIRouters.GetAllCats(param)).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let getJson = JSON(responseData.result.value!)
if (getJson["status"].stringValue == "200"){
if let resData = getJson["data"].arrayObject {
self.cats = resData as! [[String:AnyObject]]
}
} else if (getJson["status"].stringValue == "404") {
} else {
}
}
_ = EZLoadingActivity.hide()
}
}
var buttonY: CGFloat = 20 // our Starting Offset, could be 0
for villain in cats {
print("ok",villain["pt_name_Fa"])
let villainButton = UIButton(frame: CGRect(x: 50, y: buttonY, width: 250, height: 30))
buttonY = buttonY + 50 // we are going to space these UIButtons 50px apart
villainButton.layer.cornerRadius = 10 // get some fancy pantsy rounding
villainButton.backgroundColor = UIColor.darkGray
villainButton.setTitle("Button for Villain: \(villain["pt_name_Fa"] ?? "" as AnyObject))", for: UIControlState.normal) // We are going to use the item name as the Button Title here.
villainButton.titleLabel?.text = "\(villain)"
villainButton.addTarget(self,action:#selector(villainButtonPressed),
for:.touchUpInside)
// villainButton.addTarget(self, action: Selector(("villainButtonPressed:")), for: UIControlEvents.touchUpInside)
self.view.addSubview(villainButton) // myView in this case is the view you want these buttons added
}
It did not work, but if i use an array such below it works!
var arrayOfVillains = ["santa", "bugs", "superman", "batman"]
Thanks for your helping in advance
I found the solution. Adding button should be run after Json load completely:
var cats = [[String:AnyObject]]()
_ = EZLoadingActivity.show("Loading...", disableUI: true)
let param: [String: AnyObject] = ["apiKey": "???" as AnyObject]
_ = Alamofire.request(APIRouters.GetAllCats(param)).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
print(responseData.request!) // original URL request
print(responseData.response!) // URL response
let getJson = JSON(responseData.result.value!)
if (getJson["status"].stringValue == "200"){
if let resData = getJson["data"].arrayObject {
self.cats = resData as! [[String:AnyObject]]
}
if self.cats.count > 0 {
self.addButton()
}
} else if (getJson["status"].stringValue == "404") {
_ = SweetAlert().showAlert("Ok", subTitle: "Api Error!", style: AlertStyle.warning)
} else {
_ = SweetAlert().showAlert("Error", subTitle: getJson["message"].stringValue, style: AlertStyle.warning)
}
}
_ = EZLoadingActivity.hide()
}
func addButton(){
var buttonY: CGFloat = 20 // our Starting Offset, could be 0
for villain in self.cats {
let villainButton = UIButton(frame: CGRect(x: 50, y: buttonY, width: 250, height: 30))
buttonY = buttonY + 50 // we are going to space these UIButtons 50px apart
villainButton.layer.cornerRadius = 10 // get some fancy pantsy rounding
villainButton.backgroundColor = UIColor.darkGray
villainButton.setTitle("Button for Villain: \(villain["pt_name_Fa"] ?? "" as AnyObject))", for: UIControlState.normal) // We are going to use the item name as the Button Title here.
villainButton.titleLabel?.text = "\(villain)"
villainButton.addTarget(self,action:#selector(villainButtonPressed),
for:.touchUpInside)
self.view.addSubview(villainButton) // myView in this case is the view you want these buttons added
}
}
Please attention to this part of code :
if self.cats.count > 0 {
self.addButton()
}
Thanks for your attendance.

Activity Indicator for UICollectionView [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
I have a UICollectionView that reads data from Firebase, and I want some kind of activity indicator while it's reading data.
I want the activity indicator to start running as soon as the UICollectionView tab in the TabBar is hit, and I want it to stop and the view/uicollectionView to load once loading from Firebase is done.
I saw this post:
Show Activity Indicator while data load in collectionView Swift
But I could not understand it fully because I did not know how to integrate my UICollectionView there.
EDIT:
This is my code that reads from Firebase:
self.ref = Database.database().reference()
let loggedOnUserID = Auth.auth().currentUser?.uid
if let currentUserID = loggedOnUserID
{
// Retrieve the products and listen for changes
self.databaseHandle = self.ref?.child("Users").child(currentUserID).child("Products").observe(.childAdded, with:
{ (snapshot) in
// Code to execute when new product is added
let prodValue = snapshot.value as? NSDictionary
let prodName = prodValue?["Name"] as? String ?? ""
let prodPrice = prodValue?["Price"] as? Double ?? -1
let prodDesc = prodValue?["Description"] as? String ?? ""
let prodURLS = prodValue?["MainImage"] as? String
let prodAmount = prodValue?["Amount"] as? Int ?? 0
let prodID = snapshot.key
let prodToAddToView = Product(name: prodName, price: prodPrice, currency: "NIS", description: prodDesc, location: "IL",
toSell: false, toBuy: false, owner: currentUserID, uniqueID: prodID, amount: prodAmount, mainImageURL: prodURLS)
self.products.append(prodToAddToView)
DispatchQueue.main.async
{
self.MyProductsCollection.reloadData()
}
}
) // Closes observe function
let activityView = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
// waiy until main view shows
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// create a hover view that covers all screen with opacity 0.4 to show a waiting action
let fadeView:UIView = UIView()
fadeView.frame = self.view.frame
fadeView.backgroundColor = UIColor.whiteColor()
fadeView.alpha = 0.4
// add fade view to main view
self.view.addSubview(fadeView)
// add activity to main view
self.view.addSubview(activityView)
activityView.hidesWhenStopped = true
activityView.center = self.view.center
// start animating activity view
activityView.startAnimating()
self.ref = Database.database().reference()
let loggedOnUserID = Auth.auth().currentUser?.uid
if let currentUserID = loggedOnUserID
{
// Retrieve the products and listen for changes
self.databaseHandle = self.ref?.child("Users").child(currentUserID).child("Products").observeSingleEvent(.value, with:
{ (snapshot) in
// Code to execute when new product is added
let prodValue = snapshot.value as? NSDictionary
let prodName = prodValue?["Name"] as? String ?? ""
let prodPrice = prodValue?["Price"] as? Double ?? -1
let prodDesc = prodValue?["Description"] as? String ?? ""
let prodURLS = prodValue?["MainImage"] as? String
let prodAmount = prodValue?["Amount"] as? Int ?? 0
let prodID = snapshot.key
let prodToAddToView = Product(name: prodName, price: prodPrice, currency: "NIS", description: prodDesc, location: "IL",
toSell: false, toBuy: false, owner: currentUserID, uniqueID: prodID, amount: prodAmount, mainImageURL: prodURLS)
self.products.append(prodToAddToView)
DispatchQueue.main.async
{
self.MyProductsCollection.reloadData()
// remove the hover view as now we have data
fadeView.removeFromSuperview()
// stop animating the activity
self.activityView.stopAnimating()
}
}
) // Closes observe function
I mean UIActivityIndicatorView isn't hard to work with, you display it when fetching data, and stop it when done.
private lazy var indicator : UIActivityIndicatorView = { // here we simply declaring the indicator along some properties
let _indicator = UIActivityIndicatorView()
// change the color
_indicator.color = .black
// when you call stopAnimation on this indicator, it will hide automatically
_indicator.hidesWhenStopped = true
return _indicator
}()
Now where you want to place it? you can either placed it into your parent's view, or into your navigationBar. (I choose to place into the right side of the navigationBar )
self.navigationItem.rightBarButtonItem = UIBarButtonItem.init(customView: indicator)
Now say you have this function that return data (via callbacks) from some apis.
// this callback emits data from a background queue
func fetchPost(completion:#escaping(Array<Any>?, Error?) -> ()) {
DispatchQueue.global(qos: .background).async {
// ... do work
completion([], nil) // call your completionHandler based either error or data
}
}
/* now let's called that fetchPost function and load data into your
collectionView, but before let's started this indicator */
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem.init(customView: indicator)
indicator.startAnimating()
fetchPost { [weak self] (data, err) in
// go to the main queue to update our collectionView
// and stop the indicator
DispatchQueue.main.async { [weak self] in
// stop animation
self?.indicator.startAnimating()
// do something we have an error
if let _err = err {}
if let _data = data {
// fill array for collectionView
// reload the collectionView
self?.collectionView?.reloadData()
}
}
}
}

Empty array causing my app to crash in Swift

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.

Resources