I have a collectionView and ı am getting firebase database data. My collectionView create send firebase indexpath.row. This row 0 , 1, 2, 3, ... but firebase response irregular data and ı see debug mod indexpath.row 16 , 3, 7 , 11...
what is the problem ?
I share my sample code.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print(indexPath.row) // 1, 2, 3, 4
self.ref.child(userID!).child(self.islemString).child(String(indexPath.row + 1)).observeSingleEvent(of: .value, with: { (snapshot) in
// Get user value
let value = snapshot.value as? NSDictionary
let asd = value?["xxx"] as? Int ?? 0
print(indexPath.row) // 16, 3, 7 ,9
if(asd == 0) {
cell.imageKilit.image = UIImage(named: "kilit")
}
else{
cell.imageKilit.image = UIImage(named: "")
}
}
)
}
In response to the comment from the OP asking for example code:
Here's one example but I don't know what's stored in your Firebase so we will just use a messages structure as an example
messages
msg_0
msg: "Hello!"
msg_1
msg: "Another msg
and when the app starts we will have this code in the viewDidLoad to initially populate an array which will be used as a datasource for our collectionView, tableView etc.
var messagesArray = [String]()
var ref = //set to your firebase database
func viewDidLoad() {
let messagesRef = self.ref.child("messages")
messagesRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children {
let msgDict = child.value as! [String: AnyObject]
let msg = msgDict["msg"] as! String
self.messagesArray.append(msg)
}
self.myCollectionView.reloadData()
})
}
then, in the collection view delegate methods
func collectionView(_ collectionView: UICollectionView, cellForItemAt in...
let msg = self.messagesArray[indexPath.row]
//do something with the msg
}
As you can see, as your scroll through the tableView, it's only pulling data from the dataSource array and not constantly hitting Firebase for each row of data. In this case we are using a simple string but it could be an array of message objects which could contain the message, the name of the sender and even their image.
Related
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.
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.
I have searched every source I know of for help on this problem. I want to make individual rows within a tableview disappear after a certain amount of time expires. Even if the app is not open, I want the rows to delete as soon as the timer reaches zero. I have been trying to arrange each post into an dictionary with timer pairings to handle the row deletion when time occurs. I have looked at this post for guidance but no solutions. Swift deleting table view cell when timer expires.
This is my code for handling the tableview and the timers:
var nextID: String?
var postsInFeed = [String]()
var postTimer = [Timer: String]()
var timeLeft = [String: Int]()
(in view did load)
DataService.ds.REF_FEED.observe(.value, with: { (snapshot) in
self.posts = []
self.postsInFeed = []
self.nextID = nil
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] { //Gets us into our users class of our DB
for snap in snapshot { // Iterates over each user
if let postDict = snap.value as? Dictionary<String, Any> { // Opens up the dictonary key value pairs for each user.
let key = snap.key //
let post = Post(postID: key, postData: postDict) // Sets the properties in the dictionary to a variable.
self.posts.append(post) // appends each post which contains a caption, imageurl and likes to the empty posts array.
self.nextID = snap.key
let activeID = self.nextID
self.postsInFeed.append(activeID!)
print(self.postsInFeed)
print(activeID!)
}
}
}
self.tableView.reloadData()
})
//////////////////////////////////////////////////////////////////////////////////////////////
// Sets up our tableview
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postsInFeed.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let post = posts[indexPath.row] // We get our post object from the array we populate when we call the data from the database up above.
if let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell") as? TableViewCell { //Specifies the format of the cell we want from the UI
// cell.cellID = self.postsInFeed[indexPath.row]
cell.cellID = self.postsInFeed[indexPath.row]
cell.homeVC = self
if let img = HomeVC.imageCache.object(forKey: post.imageUrl as NSString){
cell.configureCell(post: post, img: img as? UIImage)
print(postTimer)
print(self.timeLeft)
} else {
cell.configureCell(post: post)
print(postTimer)
print(self.timeLeft)
}
return cell
} else {
return TableViewCell()
}
}
func handleCountdown(timer: Timer) {
let cellID = postTimer[timer]
// find the current row corresponding to the cellID
let row = postsInFeed.index(of: cellID!)
// decrement time left
let timeRemaining = timeLeft[(cellID!)]! - 1
timeLeft[cellID!] = timeRemaining
if timeRemaining == 0 {
timer.invalidate()
postTimer[timer] = nil
postsInFeed.remove(at: row!)
tableView.deleteRows(at: [IndexPath(row: row!, section: 0)], with: .fade)
} else {
tableView.reloadRows(at: [IndexPath(row: row!, section: 0)], with: .fade)
}
}
In the tableviewcell:
weak var homeVC: HomeVC?
var cellID: String!
func callTime() {
homeVC?.timeLeft[cellID] = 25
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(homeVC?.handleCountdown(timer:)), userInfo: nil, repeats: true)
homeVC?.postTimer[timer] = cellID
}
Any help would be really appreciated!
Timers don't run when your app is not running or is suspended.
You should rework your code to save a date (to UserDefaults) when you start your timer, and then each time the timer fires, compare the current date to the saved date. Use the amount of elapsed time to decide how many entries in your table view's data model to delete. (In case more than one timer period elapsed while you were suspended/not running.)
You should implement applicationDidEnterBackground() in your app delegate (or subscribe to the equivalent notification, UIApplicationDidEnterBackground) and stop your timer(s).
Then implement the app delegate applicationDidBecomeActive() method (or add a notification handler for the UIApplicationDidBecomeActive notification), and in that method, check the amount of time that has elapsed, update your table view's data model, and tell the table view to reload if you've removed any entries. Then finally restart your timer to update your table view while your app is running in the foreground.
I am able to retrieve results from a Firebase query but I am having trouble retrieving them as a dictionary to populate a tableView. Here's how I'm storing the query results:
var invites: Array<FIRDataSnapshot> = []
func getAlerts(){
let invitesRef = self.rootRef.child("invites")
let query = invitesRef.queryOrderedByChild("invitee").queryEqualToValue(currentUser?.uid)
query.observeEventType(.Value, withBlock: { snapshot in
self.invites.append(snapshot)
print(self.invites)
self.tableView.reloadData()
})
}
Printing self.invites returns the following:
[Snap (invites) {
"-KKQWErkyuehmbxom8NO" = {
invitedBy = T2k7Bm9G9RNLcHLvLlKApRbnas23;
invitee = dRJ1FqctSfTlLF8iO2ddlc9BANJ3;
role = guardian;
};
}]
I'm having trouble populating the tableView. The cell labels don't show anything:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let inviteDict = invites[indexPath.row].value as! [String : AnyObject]
let role = inviteDict["role"] as? String
cell.textLabel!.text = role
return cell
}
Any ideas?
Thanks!
EDIT: Printed the dictionary to console after my function is run, and it's printing []. Why is it losing it's values?
override func viewWillAppear(animated: Bool) {
getAlerts() // inside of function prints values
print(self.invites) //prints []
}
EDIT 2: Paul's solution worked!! I added the following function to have Firebase "listen" for results! I may have to edit this to only show alerts for the logged in user, but this has at least pointed me in the right direction:
override func viewWillAppear(animated: Bool) {
getAlerts()
configureDatabase()
}
func configureDatabase() {
// Listen for new messages in the Firebase database
let ref = self.rootRef.child("invites").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
self.invites.append(snapshot)
self.tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: self.invites.count-1, inSection: 0)], withRowAnimation: .Automatic)
})
}
Check out the friendlychat Firebase codelab for an example of populating a table view from an asynchronous call. Specifically, see viewDidLoad and configureDatabase in FCViewController.swift.
Your array will always return [ ] because Firebase is async. If you add var invites: Array<FIRDataSnapshot> = [] inside of the Firebase closure you will print the data you are looking for, but im assumming you would like to use the variable outside of the closure. in order to complete that you must create a completion within getAlerts() function. If you are not sure how to complete that do a google search and that will solve your question.
I'm trying to recreate a new firebase project where you populate a table view with data from firebase realtime database that contain links to images in firebase storage.
I can populate the tutorial project which is a table view with firebase data. But with my current project it is a collection view inside an extension.
I've narrowed down the issue to my variables
var ref: FIRDatabaseReference!
var messages: [FIRDataSnapshot]! = []
var msglength: NSNumber = 10
private var _refHandle: FIRDatabaseHandle!
specifically
var messages: [FIRDataSnapshot]! = []
Which I think is an array of my data I get from firebase
I then call a function that should populate that array in my viewdidload()
func loadPosts(){
self.messages.removeAll()
// Listen for new messages in the Firebase database
_refHandle = self.ref.child("messages").observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
//print("1")
self.messages.append(snapshot)
//print(self.messages.count)
})
}
The issue happens when I try to populate my collections view since I want horizontal scrolling I use an extension. In the extension I find that my array of values is always 0, but in my loadPosts() function the count of my >array is the same value as the amount of posts I have in firebase.
extension HomeViewController : UICollectionViewDataSource
{
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
print(messages.count)
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(StoryBoard.CellIdentifier, forIndexPath: indexPath) as! InterestCollectionViewCell
// Unpack message from Firebase DataSnapshot
let messageSnapshot: FIRDataSnapshot! = self.messages[indexPath.row]
let message = messageSnapshot.value as! Dictionary<String, String>
let name = message[Constants.MessageFields.name] as String!
if let imageUrl = message[Constants.MessageFields.imageUrl] {
if imageUrl.hasPrefix("gs://") {
FIRStorage.storage().referenceForURL(imageUrl).dataWithMaxSize(INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
cell.featuredImageView?.image = UIImage.init(data: data!)
}
} else if let url = NSURL(string:imageUrl), data = NSData(contentsOfURL: url) {
cell.featuredImageView?.image = UIImage.init(data: data)
}
cell.interestTitleLabel?.text = "sent by: \(name)"
}
return cell
}
}
Should I not be using FIRDataSnapshot? If so which is the correct one to use? Or should I approach the project in another form not using extensions?
You are correctly inserting the items into your array within the completion block, but you are missing a call to reload your collectionView.