Array index Error when updating or removing data from Firebase - ios

I have an error that occurs when I add or delete data from a node in firebase. When the data is added or deleted on firebase I get an array index out of bounds error. My array has two items, but the tableView thinks that there are three items and thus tries to access a value that doesn't exist. I can't figure out how to prevent this. For more context I am using an alertView with a closure that performs the adding or deleting of information in firebase. This alertView is in the didSelectCellAtIndexPath method. The error is occurring in the cellForRowAtIndexPath Method when accessing the array like so user.id = self.bookRequestors[indexPath.row]
Here is some of the code I wrote:
`
alertVC.addAction(PMAlertAction(title: "Approve this users request",
style: .default, action: {
print("Book Approved")
let user = User()
user.id = self.bookRequestors[indexPath.row]
var users = self.userInfoArray[indexPath.row]
var bookRef = Database.database().reference().child("books").child("-" + self.bookID)
bookRef.observeSingleEvent(of: .value, with: { (snapshot) in
var tempDict = snapshot.value as! [String:AnyObject]
if tempDict["RequestApproved"] == nil || tempDict["RequestApproved"] as! String == "false"{
//bookRef.updateChildValues(["RequestApproved": "true", "ApprovedRequestor": user.id])
bookRef.updateChildValues(["RequestApproved": "true", "ApprovedRequestor": user.id], withCompletionBlock: { (error, ref) in
let userRef = Database.database().reference().child("users").child(user.id!).child("requestedBooks")
// true meaning this book has been approved for this user
userRef.updateChildValues(["-" + self.bookID:"true"])
})
} else{
print("Already Approved!")
let alertVC = PMAlertController(title: "Sorry?", description: "You Already Approved that book for someone Else", image: UIImage(named: "booksandcoffee.jpg"), style: .alert)
alertVC.addAction(PMAlertAction(title: "Ok", style: .default))
self.present(alertVC, animated: true, completion: nil)
}
})`
EDIT: MORE CODE FOR CONTEXT
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "people", for: indexPath) as! RequestTableViewCell
// Configure the cell...
var users = self.userInfoArray[indexPath.row]
var myName = users["name"]
var myPic = users["profileImageUrl"]
let user = User()
print("This is a count: \(self.bookRequestors.count)")
print ("the index is: \(indexPath)")
//This is the array throwing the error, but this array is populated from the previous view and is not modified afterwards.
user.id = self.bookRequestors[indexPath.row]
cell.userImage?.setRounded()
cell.userImage.clipsToBounds = true
let processor = RoundCornerImageProcessor(cornerRadius: 100)
DispatchQueue.main.async {
cell.userImage.loadImageUsingCacheWithUrlString(myPic as! String)
}
cell.userName.text = myName as! String
if user.id == approvedRequestorID{
cell.wasApproved.image = UIImage(named: "icons8-checked_filled.png")
cell.approvedLabel.text = "You approved to swap with: "
}
cell.backgroundColor = .clear
return cell
}
EDIT II : Here are my numberofSections and numberofRowsPerSection Methods
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return userInfoArray.count
}
EDIT III: Function where userInfoArray is updated.
func grabUsers(){
//print(bookRequestors)
for requests in bookRequestors{
let usersRef = Database.database().reference().child("users").child(requests)
usersRef.observe(.value, with: { (snapshot) in
var myDict:[String: AnyObject] = [:]
myDict = snapshot.value as! [String:AnyObject]
self.userInfoArray.append(myDict)
self.tableView.reloadData()
})
}
}

Its hard to tell what exactly is going on without seeing your code, but here is my best guess:
You are backing your cells with self.bookRequestors which you say is a static array assigned by the previous VC.
However you are using userInfoArray.count to back the number of rows in the tableView, and this value changes in your usersRef.observe method.
Specifically you are appending to userInfoArray; so userInfoArray.count strictly increases.
Therefore if the two arrays statrt at the same size, and the one that determines the count is getting bigger but the one you are indexing into is always the same size, then eventually you will index out of bounds.
Back the number of rows by the data you are actually showing in the cell.

Related

What am I doing wrong while populating this UITableView in Swift?

I am trying to populate a UITableView using an array and I am unable to do so. Here is what I have so far. This code is for retrieving data and storing it in the array that I am using to populate the UITableView:
func prepareForRetrieval() {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).observe(.value, with: {
(snapshot) in
for snap in snapshot.children.allObjects {
let id = snap as! DataSnapshot
self.keyArray.append(id.key)
}
self.updateCart()
})
}
func updateCart() {
for key in keyArray {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).child(key).observeSingleEvent(of: .value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let itemName = value?["Item Name"] as! String
let itemPrice = value?["Item Price"] as! Float
let itemQuantity = value?["Item Quantity"] as! Int
self.cartArray.append(CartData(itemName: itemName, itemQuantity: itemQuantity, itemPriceNumber: itemPrice))
print(self.cartArray.count)
})
}
}
The data is properly appending into the array and when I print the count of the array, it prints the correct count. This means that the data is there. However, when I try to populate a UITableView, it doesn't detect any data. I have the following code to make sure that there is data in the array before trying to populate the UITableView:
override func viewDidLoad() {
super.viewDidLoad()
cartBrain.prepareForRetrieval()
if cartBrain.cartArray.isEmpty == false{
tableViewOutlet.dataSource = self
tableViewOutlet.reloadData()
}
else {
tableViewOutlet.isHidden = true
tableViewOutlet.isUserInteractionEnabled = false
purchaseButtonOutlet.isEnabled = false
cartEmptyLabel.text = "Your cart is empty. Please add items and check back later."
}
}
When I open the View Controller, the TableView is disabled because it doesn't detect any data. I have already set the data source to self and the thing is that when the count of the array is printed, it again prints the correct amount. I have already set the data source to self for the UITableView. Here is my code for the UITableView:
extension CartViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cartBrain.cartArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cartcustomcell", for: indexPath)
cell.textLabel?.text = cartBrain.cartArray[indexPath.row].itemName
cell.detailTextLabel?.text = String(cartBrain.cartArray[indexPath.row].itemQuantity)
return cell
}
}
I don't understand why the count of the array prints the correct amount meaning that there is data stored in it but when the View Controller is loaded, it detects that the array is empty. Thanks for the help and I'm sorry if the question is a bit unclear.
After appending data to cartArray in updateCart you should reloadData(), like this:
weak var tableViewOutlet: UITableView?
func updateCart() {
for key in keyArray {
Database.database().reference().child("UserCart").child(Auth.auth().currentUser!.uid).child(key).observeSingleEvent(of: .value, with: {
(snapshot) in
let value = snapshot.value as? NSDictionary
let itemName = value?["Item Name"] as! String
let itemPrice = value?["Item Price"] as! Float
let itemQuantity = value?["Item Quantity"] as! Int
self.cartArray.append(CartData(itemName: itemName, itemQuantity: itemQuantity, itemPriceNumber: itemPrice))
DispatchQueue.main.async {
self.tableViewOutlet.reloadData()
}
})
}
}
The updateCart doesn't seem to have any connection to the tableViewOutlet so you need to pass in a reference to it in your viewDidLoad like this:
override func viewDidLoad() {
super.viewDidLoad()
cartBrain.tableViewOutlet = tableViewOutlet
cartBrain.prepareForRetrieval()
Note: Since you're using a for loop to trigger the async call multiple times you can use the array count to check if all the items are appended to do the reload to avoid multiple reloads.

iOS swift app terminating on TableView when scrolled to bottom

I am using Firebase to populate a TableView in my iOS app. The first few objects are loaded but once I get to the third item in my list the app crashes with the exception:
'NSRangeException', reason: '*** __boundsFail: index 3 beyond bounds [0 .. 2]'
I know that this means that I am referring to an array at an index that it does not contain however I do not know why.
I create the TableView with a TableViewController and initialize it like so:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(posts.count)
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row]
print(post)
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
cell.firstTitle.setTitle(post.firstTitle, for: .normal)
cell.secondTitle.setTitle(post.secondTitle, for: .normal)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
// Configure the cell...
return cell
}
I believe that the first function creates an array with the number of objects in posts and that the second function assigns values to the template for the cell. The print statement in the first method prints 4 which is the correct number of objects retrieved from firebase. I assume that means an array is created with 4 objects to be displayed in the TableView. This is what is really confusing because the error states that there are only 3 objects in the array. Am I misunderstanding how the TableView is instantiated?
Here is the code that fills the TableView:
func loadMessages(){
db.collectionGroup("userPosts")
.addSnapshotListener { (querySnapshot, error) in
self.posts = []
if let e = error{
print("An error occured trying to get documents. \(e)")
}else{
if let snapshotDocuments = querySnapshot?.documents{
for doc in snapshotDocuments{
let data = doc.data()
if let firstImage = data[K.FStore.firstImageField] as? String,
let firstTitle = data[K.FStore.firstTitleField] as? String,
let secondImage = data[K.FStore.secondImageField] as? String,
let secondTitle = data[K.FStore.secondTitleField] as? String{
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
self.posts.insert(post, at: 0)
print("Posts: ")
print(self.posts.capacity)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
The app builds and runs and displays the first few items but crashes once I scroll to the bottom of the list. Any help is greatly appreciated.
Edit:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
}
You're getting an out-of-bounds error because you're dangerously populating the datasource. You have to remember that a table view is constantly adding and removing cells as it scrolls which makes updating its datasource a sensitive task. You reload the table on each document iteration and insert a new element in the datasource at index 0. Any scrolling during an update will throw an out-of-bounds error.
Therefore, populate a temporary datasource and hand that off to the actual datasource when it's ready (and then immediately reload the table, leaving no space in between an altered datasource and an active scroll fetching from that datasource).
private var posts = [Post]()
private let q = DispatchQueue(label: "userPosts") // serial queue
private func loadMessages() {
db.collectionGroup("userPosts").addSnapshotListener { [weak self] (snapshot, error) in
self?.q.async { // go into the background (and in serial)
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
return
}
var postsTemp = [Post]() // setup temp collection
for doc in snapshot.documents {
if let firstImage = doc.get(K.FStore.firstImageField) as? String,
let firstTitle = doc.get(K.FStore.firstTitleField) as? String,
let secondImage = doc.get(K.FStore.secondImageField) as? String,
let secondTitle = doc.get(K.FStore.secondTitleField) as? String {
let post = Post(firstImageUrl: firstImage, secondImageUrl: secondImage, firstTitle: firstTitle, secondTitle: secondTitle)
postsTemp.insert(post, at: 0) // populate temp
}
}
DispatchQueue.main.async { // hop back onto the main queue
self?.posts = postsTemp // hand temp off (replace or append)
self?.tableView.reloadData() // reload
}
}
}
}
Beyond this, I would handle this in the background (Firestore returns on the main queue) and only reload the table if the datasource was modified.
After some fiddling around and implementing #bsod's response I was able to get my project running. The solution was in Main.Storyboard under the Attributes inspector I had to set the content to Dynamic Prototypes.

Unexpected non-void return value in void function in Swift using numberOfRowsInSection

This question has already been asked numerous times, but nothing seems to be working in my particular case. As you can see in my code below, I have created an extension of my View Controller for my UITableViewDataSource. I am trying to pull information from my Firebase Realtime Database and depending on what I get, return a certain Int. The issue is that whenever I try and return an Int from within the Firebase Snapshot, I receive this error: "Unexpected non-void return value in void function." I understand the reason for this, but I am not sure how I can make my code work. Any solutions?
My code:
extension LikeOrDislikeViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let uid = Auth.auth().currentUser?.uid
Database.database().reference().child("Num Liked").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let numMoviesLiked = ((dictionary["Number of Movies Liked"] as? Int))!
if numMoviesLiked%4 == 0 {
return numMoviesLiked/4
}
}
})
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.backgroundColor = UIColor.red
tableView.rowHeight = 113
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
Your problem is this return numRows inside the database block , it doesn't consider to return any values , also the call is asynchronous so don't expect it to finish before the return 10 of numberOfRowsInSection, completely remove this part and put it inside viewDidLoad
Database.database().reference().child("Num Liked").child(uid!).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let numMoviesLiked = ((dictionary["Number of Movies Liked"] as? Int))!
if numMoviesLiked/4 == 0 {
numRows = numMoviesLiked/4
self.tableView.reloadData()
}
}
})
logically if you do if only when you compare numMoviesLiked/4 == 0 then numRows will be always 0

Issue with first Firebase query item out of order until refresh

I have a firebase database to pull 50 users with the highest integer value and to display them from highest to lowest. The issue will arise when I enter the leaderboard view for the first time. The order should show jlewallen18 at the top AND THEN appledev. But on first load appledev is at the top, until I back out and open the leaderboard again (code at the bottom).
Leaderboard code:
class LeaderboardViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var leaderboardTableView: UITableView!
var userModel : [User] = [User]()
let pipeline = ImagePipeline {
//config settings for image display removed for brevity
}
override func viewDidLoad() {
super.viewDidLoad()
leaderboardTableView.delegate = self
leaderboardTableView.dataSource = self
fetchUsers()
}
func fetchUsers() {
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 50)
queryRef.observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String : AnyObject]{
let user = User(dictionary: dictionary)
self.userModel.append(user)
}
DispatchQueue.main.async(execute: {
self.leaderboardTableView.reloadData()
})
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "leaderboardTableCell", for: indexPath) as! LeaderboardTableCell
if userModel.count > indexPath.row {
if let profileImageURL = userModel[indexPath.row].photoURL {
let url = URL(string: profileImageURL)!
var options = ImageLoadingOptions()
options.pipeline = pipeline
options.transition = .fadeIn(duration: 0.25)
Nuke.loadImage(
with: ImageRequest(url: url).processed(with: _ProgressiveBlurImageProcessor()),
options: options,
into: cell.userImage
)
}
}
cell.userName.text = userModel[indexPath.row].username
cell.watchTime.text = "\(String(describing: userModel[indexPath.row].watchTime!))"
cell.ranking.text = "\(indexPath.row + 1)"
cell.userImage.layer.cornerRadius = cell.userImage.frame.size.width/2
return cell
}
}
I thought it might be because I am using the same model name userModel in both my Profile page view and my Leaderboard view but when i changed the model name in my leaderboard view nothing changed. What else can I share to help? Thanks!
EDIT: here's my console output after printing out watchTime which is the integer I have rankings for:
HERES WHERE I OPEN LEADERBOARD PAGE FIRST:
Optional(28)
Optional(247)
Optional(0)
Optional(0)
Optional(0)
Optional(0)
AFTER I GO BACK AND CLICK TO VIEW LEADERBOARD AGAIN:
Optional(247)
Optional(28)
Optional(0)
Optional(0)
Optional(0)
Optional(0)
The issue here is related to this line of code...
let queryRef = Database.database().reference().child("users").queryOrdered(byChild: "ranking").queryLimited(toLast: 50)
Changing this limit to 10 makes the app work as expected, which is a temporary 'fix'.
If we figure out why that limit is causing issues I'll be sure to update this answer.

Get value inside NSURLSession

My codes doesnt work, do you have an idea why? I want to display some data to my UITable from the requested HTTP
class contactView : UITableViewController{
var values: NSMutableArray!
#IBOutlet var tbview: UITableView!
override func viewDidLoad() {
if(signed != 1){
self.navigationItem.title = ""
}else{
let outbtn = UIBarButtonItem(title: "Sign out", style: .Plain, target: self, action: #selector(contactView.out_action))
navigationItem.leftBarButtonItem = outbtn
let reloadData = UIBarButtonItem(title: "Reload", style: .Plain, target: self, action: #selector(contactView.loadData))
navigationItem.rightBarButtonItem = reloadData
//Check Connection
if(reachability.isConnectedToNetwork() == true) {
loadData()
}else{
let alert = UIAlertController(title: "Error Connection", message: "Not Internet Connection", preferredStyle: .ActionSheet)
let alertAct = UIAlertAction(title: "I'll connect later !", style: .Destructive){
(actions) -> Void in
}
alert.addAction(alertAct)
self.presentViewController(alert, animated: true, completion: nil)
}
}
}
func loadData(){
let url = NSURL(string: url_friends)
let to_post = "user=iam&pin=101218"
let request = NSMutableURLRequest(URL: url!)
request.HTTPMethod = "POST"
request.HTTPBody = to_post.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request){
(let data,let response,let error) -> Void in
dispatch_async(dispatch_get_main_queue(),{
do{
self.values = try! NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSMutableArray
print(self.values)
}catch{
print(error)
return
}
})
}
task.resume()
}
I want to display the variable "value" data in my table but error keep occuring, saying it is nil when call in my table function cellForRowAtIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellidentity") as UITableViewCell!
let mainData = values[indexpath.row] as! String
let x = cell.viewWithTag(2) as! UILabel
if(signed != 1){
print("No people")
}else{
let x = cell.viewWithTag(2) as! UILabel
x.text = mainData["name"]
}
return cell
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let x : Int
if(reachability.signed() != 1){
x = 1
}else{
x = values.count
}
return x
}
Yes, the first time you load the table, it would be nil. The dataTaskWithRequest completion block has to explicitly call self.tableview.reloadData(), to tell the table to update itself now that the network request has finished. Remember, dataTaskWithRequest runs asynchronously, meaning that it finishes after the table is already presented. So you have to tell the table to reload itself (and therefore call the UITableViewDataSource methods again).
So you probably want something like:
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard data != nil && error == nil else {
print(error)
return
}
do {
if let values = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? NSMutableArray {
dispatch_async(dispatch_get_main_queue()) {
self.value = values
print(values)
self.tableview.reloadData()
}
}
} catch let parseError {
print(parseError)
return
}
}
task.resume()
Note, before doing forced unwrapping of data with data!, I first guard to make sure it's not nil. Never use ! unless you've know it cannot possibly be nil.
In terms of why your UITableView methods are failing the first time they're called, it's because they're relying upon reachability.signed() or signed. But the real question is whether values is nil or not.
So, perhaps:
var values: NSMutableArray? // make this a standard optional, not an implicitly unwrapped one
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellidentity") as UITableViewCell!
let x = cell.viewWithTag(2) as! UILabel // btw, using `UITableViewCell` subclasses are more elegant than using cryptic `tag` numbers
if let mainData = values?[indexPath.row] as? [String: String] {
x.text = mainData["name"]
} else {
print("No people")
x.text = "(retrieving data)" // perhaps you want to tell the user that the request is in progress
}
return cell
}
// if `values` is `nil`, return `1`, otherwise returns `values.count`
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values?.count ?? 1
}
Your code was configured to return one cell if the data was not available, so I've repeated that here, but generally I return zero rows if there's no data. If you do that, it simplifies cellForRowAtIndexPath even more, as it doesn't have to even worry about the "no data" condition at all. But that's up to you.
Now, I've made some assumptions above (e.g. that mainData was really a dictionary given that you are subscripting it with a string "name"). But less important than these little details is the big picture, namely that one should scrupulously avoid using ! forced unwrapping or using implicitly unwrapped optionals unless you know with absolute certainty that the underlying optional can never be nil.

Resources