Firebase Data flickers on addition of new values - swift

I am making a social app to which I am fetching some data and flushing it to the collection view. I am flushing the all the posts from firebase to the posts array. I am also fetching the user information that posted the specific image. Both the database are 2 different models. Following is my data model :
posts
|- <post_id>
|- caption
|- ImageURL
|- views
|- spot
|- spot_id
|- sender<user_id>
|- spotted(value)
|- timestamp
|- author(<user_id>)
users
|- <user_id>
|- name
Following is the way I am fetching the post data in collectionVC and storing all to posts array:
func initialiseAllPostsContent(){
FBDataservice.ds.REF_CURR_USER.child("connections/following").observe(.childAdded) { (snapshot) in
if let snapshot = snapshot.value as? String {
self.followerKeys.append(snapshot)
}
}
if uid != nil {
self.followerKeys.append(uid!)
}
FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.childAdded, with: { (snapshot) in
print("post key is ", snapshot.key)
if let postDict = snapshot.value as? Dictionary<String, Any> {
let key = snapshot.key
if let postAuthor = postDict["author"] as? String {
for user in self.followerKeys {
if postAuthor == user {
let post = Posts(postId: key, postData: postDict)
self.posts.append(post)
}
}
}
}
})
reloadCollectionViewData()
}
func reloadCollectionViewData() {
FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.value) { (snapshot) in
self.collectionView.reloadData()
}
}
//I am updating the views on the post after a method is successfull. As soon as this is called, and then if like is pressed, views flicker
func updateViews(postid: String, views: Int) {
let viewref = FBDataservice.ds.REF_POSTS.child(postid)
let newviews = views + 1
viewref.updateChildValues(["views":newviews])
}
// fetching the user data from the post data
func getAllPosts(pid: String, completion: #escaping ((String) -> ())) {
FBDataservice.ds.REF_POSTS.child(pid).observeSingleEvent(of: .value) { (snapshot) in
if let snapshot = snapshot.value as? Dictionary<String, Any> {
if let userid = snapshot["author"] as? String {
completion(userid)
}
}
}
}
func getpostAuthorData(authorId : String, completion: #escaping (User) -> ()) {
FBDataservice.ds.REF_USERS.child(authorId).observeSingleEvent(of: .value) { (snapshot) in
if let snapshot = snapshot.value as? Dictionary<String, Any> {
if let userCredential = snapshot["credentials"] as? Dictionary<String, Any> {
completion(User(userid: authorId, userData: userCredential))
}
}
}
}
This is how I am assigning data in my cellForItemAtIndexPath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
self.posts.sort(by: { $0.timestamp < $1.timestamp})
let post = posts[indexPath.row]
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? SpotGroundCell {
cell.configureCellData(post: post)
getAllPosts(pid: post.postId) { (userid) in
self.getpostAuthorData(authorId: userid, completion: { (userdata) in
cell.configUserData(user: userdata)
})
}
return cell
} else {
return SpotGroundCell()
}
}
The code in my cell :
//Consider this as likes. I allow users to like multiple times. Once the model is loaded, it fetches all the spots according to the timestamp and then siplayer the most recent ones. Even this is doesn't display according to the current image and flickers. I replicate previous cell values even though I am refreshing the view.
var currentUserSpots = [Spot]() {
didSet {
self.currentUserSpots.sort(by: { $0.timestamp < $1.timestamp})
if !self.currentUserSpots.isEmpty {
self.emotionImage.image = UIImage(named: (self.currentUserSpots.first?.spotted)!)
self.emotionImage.alpha = 1
} else {
self.emotionImage.image = UIImage(named: "none")
self.emotionImage.alpha = 0.5
}
}
}
func configUserData(user: User) {
self.user = user
self.name.text = self.user.name
}
func configureCellData(post: Posts) {
print("Config is now called")
self.posts = post
self.caption.text = posts.caption
FBDataservice.ds.REF_POSTS.child(post.postId).child("spot").queryOrdered(byChild: "senderID").queryEqual(toValue: uid!).observeSingleEvent(of: .childAdded) { (snapshot) in
if let spotData = snapshot.value as? Dictionary<String, Any> {
let spot = Spot(id: snapshot.key, spotData: spotData)
if spot.spotted != nil {
self.currentUserSpots.append(spot)
}
}
}
}
Now whenever I am making a change or an event which updates the database(like updating a view). I see a flicker in the user object entities(such as name etc). That event also kills other processes and Notification Observers.
I scrapped the internet for the solutions, but by far just was able to find one, which doesn't solve my problem.
Any help will be greatly appreciated. I am really not sure where am I going wrong.

Whenever there is a change under REF_POSTS you right now:
delete all data from the view
re-add all data (including the change) to the view
Given that most changes will only affect one item in the list, you're making your view to N-1 more than is needed. This causes the flicker.
To solve this problem, you should listen to more granular information from the database. Instead of observing .value, add a listener for .childAdded. The completion block for this listener will be triggered whenever a new child is added, at which point you can just add the new child to your view.
FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.childAdded, with: { (snap) in
if let postDict = snap.value as? Dictionary<String, Any> {
let key = snap.key
if let postAuthor = postDict["author"] as? String {
for user in self.followerKeys {
if postAuthor == user {
let post = Posts(postId: key, postData: postDict)
self.posts.append(post)
}
}
}
}
})
As a bonus .childAdded also immediately fires for all existing child nodes, so you don't need the observer for .value anymore. I like keeping it myself though. As Firebase guarantees that it fires .value after all corresponding child* events, the .value event is a great moment to tell the view that all changes came in.
FBDataservice.ds.REF_POSTS.queryOrdered(byChild: "timestamp").observe(.value, with: { (snapshot) in
self.collectionView.reloadData()
})
You'll need a few more things for a complete implementation:
You should also observe .childChanged, .childMoved and childRemoved to handle those types of changes to the database.
Since a child may be added (or moved) anywhere in the list, you should actually use observe(_, andPreviousSiblingKey: ) to be able to put the item in the right spot in the list.

Related

Retrieving Data From Firebase Auto Id's - Firebase Swift

I am wanting to capture all the values in my childByAutoId in firebase. Essentially, it stores all the items that a person has shortlisted. However, I do not seem to be capturing this, and I assume it is because I am not calling the snapshot correctly to factor the auto id's.
Database:
userID
-> Favourited
-> Auto Id
-> itemName: x
-> Auto Id
-> itemName: x
-> Auto Id
-> itemName: x
Code:
func retrieveItems() {
guard let userId = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("users/\(userId)/Favourited")
ref.observe(.value, with: { (snapshot) in
if snapshot.childrenCount>0 {
self.favUsers.removeAll()
for likes in snapshot.children.allObjects as! [DataSnapshot] {
let likesObject = likes.value as? [String: AnyObject]
let itemName = likesObject!["itemName"]
let likesList = Names(id: likes.key, itemName: itemName as! String?)
self.favUsers.append(likesList)
}
} else {
print("not yet")
}
})
self.favList.reloadData()
}
Could someone have a look and let me know what I may be doing wrong? Thank you!
This happens because Firebase loads data asynchronously, and right now you're calling reloadData before the self.favUsers.append(likesList) has ever run.
The call to reloadData needs to be inside the close/completion handler that is called when the data comes back from Firebase:
ref.observe(.value, with: { (snapshot) in
if snapshot.childrenCount>0 {
self.favUsers.removeAll()
for likes in snapshot.children.allObjects as! [DataSnapshot] {
let likesObject = likes.value as? [String: AnyObject]
let itemName = likesObject!["itemName"]
let likesList = Names(id: likes.key, itemName: itemName as! String?)
self.favUsers.append(likesList)
}
self.favList.reloadData() // 👈 Move this here
} else {
print("not yet")
}
})
I also recommend checking out some of these answers asynchronous data loading in Firebase.

Search for users that haven't been fetch yet

I have a quick question, I build a block of code that fetches all the users in the database for searching purposes. Preparing for thousands of users in the future, I programmed the method in a pagination way fetching a certain amount of users at a time. That is where the problem lies when I search for a user if the user hasn't been retrieve from the database yet through scrolling I can't search there profile. Does anyone have a suggestion on how I can tackle this?
Here is the code I use to fetch the users:
//create a method that will fetch a certain mount of users
func fetchUsers() {
if userCurrentKey == nil {
USER_REF.queryLimited(toLast: 21).observeSingleEvent(of: .value) { (snapshot) in
self.collectionView?.refreshControl?.endRefreshing()
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let uid = snapshot.key
Database.fetchUser(with: uid, completion: { (user) in
self.users.append(user)
self.collectionView?.reloadData()
})
})
self.userCurrentKey = first.key
}
} else {
USER_REF.queryOrderedByKey().queryEnding(atValue: userCurrentKey).queryLimited(toLast: 22).observeSingleEvent(of: .value, with: { (snapshot) in
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let uid = snapshot.key
if uid != self.userCurrentKey {
Database.fetchUser(with: uid, completion: { (user) in
self.users.append(user)
self.collectionView?.reloadData()
})
}
})
self.userCurrentKey = first.key
})
}
}
}
Here is the code I used to paginate the users:
//once the users pass a certain amount of cells paginate to fetch the next set of users
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if users.count > 20 {
if indexPath.item == users.count - 1 {
print("Fetching...")
fetchUsers()
}
}
}
Lastly here is the code I used to filter through the users:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty{
//if search text is empty fetch the users but display nothing
inSearchMode = false
filteredUsers = users
self.collectionView?.refreshControl = refreshController
} else {
//if search text is not empty search for the users
inSearchMode = true
self.collectionView?.refreshControl = nil
filteredUsers = self.users.filter { (user) -> Bool in
return user.username.lowercased().contains(searchText.lowercased())
}
}
//reload the table view data to update the displayed user
self.collectionView?.reloadData()
}
Thank you in advance!
The up front issue is you should not be in a situation where you have to load all users to search through them.
If you have a lot of data in the users node, loading that much data can overwhelm the device and secondly it's going to get very laggy for the user as the loaded data is iterated over.
Your best move is to denormalize your data and let the server do the heavy lifting by performing those queries and delivering only the data you need. Way faster and much easier to maintain. You can also add additional nodes to get to the data you want.
Looking at the code (I don't know your structure) it appears your goal is to have a searchfield where the users can type a username and the goal is to query for that username and return it lowercased.
A solution is to update your Firebase structure. Suppose it's like this
users
uid_0
userName: "MyCrazyUserNAME"
if you want to search, lowercased, add another node to your structure that's a lowercased version of the name
users
uid_0
userName: "MyCrazyUserNAME"
lowerCased: "mycrazyusername"
Then, perform a partial string query on the lowerCased node as the user types
func searchFor(thisPartialString: String) {
let userRef = self.ref.child("users")
let startString = thePartialString
let endString = thisPartialString + "\\uf8ff"
let query = ref.queryOrdered(byChild: "lowerCased")
.queryStarting(atValue: startString)
.queryEnding(atValue: endString")
query.observe....
}
The "\uf8ff" is a character at a very high code level in Unicode - because of that it encompasses all of the preceeding characters.

Why .childAdded is not called when new data is added? Firebase

I am trying to read data from media when data is updated on /media node, but .observe(.childAdded is not called.
For example, I update data at /media/-LKN1j_FLQuOvnhEFfao/caption , but I never receive the event in observeNewMedia .
I can read the data with no problem the first time when ViewDidLoad completes.
The first step is to download the user data, second is to get the locality from currentUser and the last step is to attach a listener .childAdded on media.
I suspect that the event is not triggered because fetchMedia is called inside DDatabaseRReference.users(uid: uid).reference().observe(.value
media
-LKNRdP4ZsE3YrgaLB30
caption: "santa"
mediaUID: "-LKNRdP4ZsE3YrgaLB30"
locality: "barking"
users
Q6Dm3IMLNLgBH3ny3rv2CMYf47p1
media
-LKNReJCxgwtGRU6iJmV: "-LKNRdP4ZsE3YrgaLB30"
email: "john#gmail.com"
locality: "barking"
//enables the programmer to create references to different childs in Firebase
enum DDatabaseRReference {
case root
case users(uid:String)
case media //to store photos
func reference() -> DatabaseReference {
return rootRef.child(path)
}
//return root reference to our database
private var rootRef: DatabaseReference {
return Database.database().reference()
}
private var path: String {
switch self { //self is the enum DDatabaseReference
case .root:
return ""
case .users(let uid):
return "users/\(uid)"
case .media:
return "media"
}
}
}//end of enum DatabaseReference
class NewsfeedTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
//observe ~/users/uid
DDatabaseRReference.users(uid: uid).reference().observe(.value, with: { (snapshot) in
DispatchQueue.main.async {
if let userDict = snapshot.value as? [String : Any] {
self.currentUser = UserModel(dictionary: userDict)
self.fetchMedia()
self.tableView.reloadData()
}
}
})
}
func fetchMedia() {
Media.observeNewMedia((currentUser?.locality)!) { (newMedia) in
//check if newly downloaded media is already in media array
if !self.media.contains(newMedia) {
self.media.insert(newMedia, at: 0)
self.tableView.reloadData()
}else {
//remove old media and add the newly updated one
guard let index = self.media.index(of: newMedia) else {return}
self.media.remove(at: index)
self.media.insert(newMedia, at: 0)
self.tableView.reloadData()
}
}
}
}//end of NewsfeedTableViewController
class Media {
class func observeNewMedia(_ userLocality: String, _ completion: #escaping (Media) -> Void) {
DDatabaseRReference.media.reference().queryOrdered(byChild: "locality").queryEqual(toValue: userLocality).observe(.childAdded, with: { snapshot in
guard snapshot.exists() else {
print("no snap ")
return}
print("snap is \(snapshot)")
let media = Media(dictionary: snapshot.value as! [String : Any])
completion(media)
})
}
} //end of class Media
Let's first update the structure so make it more queryable
assume a users node
users
-Q6Dm3IMLNLgBH3ny3rv2CMYf47p1 //this is each users uid
email: "john#gmail.com"
locality: "barking"
and a media node that contains media for all users
media
-abcdefg12345 //node created with childByAutoId
caption: "santa"
for_uid: -Q6Dm3IMLNLgBH3ny3rv2CMYf47p1 //matches the uid in the /users node
Then our main viewController which contains a reference to Firebase and logs the user in
class ViewController: UIViewController {
var ref: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
self.ref = Database.database().reference()
//log user in which will populate the Auth.auth.currentUser variable
}
.
.
.
We need an object to store the media in and then an array to hold those objects
class MediaClass {
var key = ""
var caption = ""
init(k: String, c: String) {
self.key = k
self.caption = c
}
}
var mediaArray = [MediaClass]()
then set up the observers which will add, update or remove from the array when media for this user is added, changed or removed.
let thisUid = Auth.auth().currentUser?.uid
let mediaRef = self.ref.child("media")
let queryRef = mediaRef.queryOrdered(byChild: "for_uid").queryEqual(toValue: thisUid)
queryRef.observe(.childAdded, with: { snapshot in
let dict = snapshot.value as! [String: Any]
let key = snapshot.key
let caption = dict["caption"] as! String
let m = MediaClass.init(k: key, c: caption)
self.mediaArray.append(m)
self.tableView.reloadData()
})
queryRef.observe(.childChanged, with: { snapshot in
let dict = snapshot.value as! [String: Any]
let key = snapshot.key
let caption = dict["caption"] as! String
let index = self.mediaArray.index { $0.key == key } //locate this object in the array
self.mediaArray[index!].caption = caption //and update it's caption
self.tableView.reloadData()
})
//leaving this an an exercise
queryRef.observe(.childRemoved....
Note we added .childAdded, .childChanged and .childRemoved events to the media node via a query so the only events the app will receive are the ones that pertain to this user.
Also note there's no error checking so that needs to be added.

Firebase Query Returning Nil When Given Parameter

Im using this function to return the top 9 results in a data set in firebase. The array is then suppose to populate data fro a collection view. However when I go try to segue to the VC I get error "Index out of range".
var userHighlights = [Post]()
func loadUserHighlightPosts(_ userID: String, update: #escaping() -> Void) {
STORAGE_REF.child(userID).queryOrdered(byChild: "likes").queryLimited(toLast: 9).observeSingleEvent(of: .value, with: { (snapshot) in
for snap in snapshot.children.allObjects as! [DataSnapshot] {
let post = Post(postID: snap.key, postData: snap.value as! Dictionary<String,Any>)
self.userHighlights.insert(post, at: 0)
update()
}
if snapshot.childrenCount == 0 {
update()
}
})
}
However, when I run the same function for the current user, using a different reference, everything works fine.
var highlights = [Post]()
func loadCurrentUserHighlightPosts(_ update: #escaping() -> Void) {
currentUserHighlightHandle = CURRENT_USER_STORAGE_REF.queryOrdered(byChild: "likes").queryLimited(toLast: 9).observe(.value, with: { (snapshot) in
for snap in snapshot.children.allObjects as! [DataSnapshot] {
let post = Post(postID: snap.key, postData: snap.value as! Dictionary<String, Any>)
self.highlights.insert(post, at: 0)
update()
}
if snapshot.childrenCount == 0 {
update()
}
})
}
I am using prepareforsegue() to pass over the user's UID that is used in the functions parameter however the app continues to crash. Any help would be appreciated in advance, thanks!

Showing just one image at the time from database (Swift and Firebase)

First of all, I am new in this, so please do not make fun of me :)
Basically, I am trying to show and Image of a product but if the client refuses the product this item will not appear on his account. That is why I am creating another table Rejected (setAcceptedOrRejected) where I put the ID of the product and the Id of the client so I wont see the item he rejected before.
What I tried here it was to get the List (Good) with all the items and the (Bad) with the rejected items. Then compare it to display the picture of the item again.
My problem is that I want to show only 1 picture at the time, if the client refuses then it will show the next one and so on but it wont show that picture again.
I hope you can really help me with this one.
Thank you
func updateImage() {
createListProductsBad ()
var badnot = ""
for bad2 in listProductsBad{
badnot = bad2
}
Database.database().reference().child("Products").child(bad2)queryOrderedByKey().observe(.childAdded, with: { snapshot in
let userInfo = snapshot.value as! NSDictionary
let storageRef = Storage.storage().reference(forURL: profileUrl)
storageRef.downloadURL(completion: { (url, error) in
do {
let data = try Data(contentsOf: url!)
let image = UIImage(data: data as Data)
self.productPhoto.image = image
}
catch _ {
print("error")
}
})
})
}
func setAcceptedOrRejected() {
let notThankyou = [ "ProductID": ProductId,
"UserID": userUID
] as [String : Any]
self.storyboard?.instantiateViewController(withIdentifier: "Home")
self.refProducts.child("Rejected").childByAutoId().setValue(notThankyou)
}
func createListProductsGood () {
Database.database().reference().child("Products").queryOrderedByKey().observe(.childAdded, with: { snapshot in
if !snapshot.exists() { return }
let userInfo = snapshot.value as! NSDictionary
let goodID = String(snapshot.key)
for prod in self.listProductsBad{
if (prod == goodID){
print("Not good **********************")
}else{
if (goodID != "" ){
self.listProductsGood.append(prod)
}
}
}
})
}
func createListProductsBad () {
Database.database().reference().child("Rejected").queryOrderedByKey().observe(.childAdded, with: { snapshot in
let userInfo = snapshot.value as! NSDictionary
let currentID = userInfo["UserID"] as! String
let badProduct = userInfo["ProductID"] as! String
if (self.userUID == currentID ){
self.listProductsBad.append(badProduct)
}
})
}
}
//These can also be swift's dictionaries, [String: AnyObject] or possibility arrays if done correctly. All depends on your style of programming - I prefer NSDictionaries just because.
let availableKeys: NSMutableDictionary = [:]
let rejectedKeys: NSMutableDictionary = [:]
//Might be a better way for you. Depends on what you are looking for.
func sortItems2() -> NSMutableDictionary{
for rejKey in rejectedKeys.allKeys{
//Removes if the rejected key is found in the available ones
availableKeys.remove(rejKey)
}
return availableKeys
}