Unable to persist 'posts' reference for 'users' in Firebase - ios

I'm creating a social app and a common aspect of it is keeping tabs of the posts that users make. I'm using Firebase as the backend for this and here's my schema
SocialApp
posts
-KGsBG5TPYBtzRivZnbf
users
facebook:10154108240254134
cachedProfile
displayName: "Karthik Kannan"
imageURL: "https://scontent.xx.fbcdn.net/v/t1.0-1/p100x100..."
posts
-KGsBG5TPYBtzRivZnbf: true
provider: "facebook"
When i go ahead and add posts from one device(or simulator) this works as planned but when i delete the app from a device and reinstall it, the posts relationship in the users dictionary disappears. I would like it to persist across devices and keep the relationship intact so if a user decides to use another iDevice to login his posts don't get orphaned.
Here's the code I wrote for adding a post to Firebase and how I currently keep track of the user.
func postToFirebase(imgUrl: String) {
var gift: Dictionary<String, AnyObject> = [
"giftName": giftName.text!,
"giftDescription":giftDescription.text!,
"giftAvailableAt": giftAvailableAt.text!,
"giftPrice": Int(giftPrice.text!)!,
"username": NSUserDefaults.standardUserDefaults().valueForKey("uid")!,
"giftImage":imgUrl,
]
let firebasePost = DataService.ds.REF_POSTS.childByAutoId()
firebasePost.setValue(gift)
firebasePost.observeSingleEventOfType(.Value, withBlock: { snapshot in
if let postID = snapshot.key {
DataService.ds.REF_USER_CURRENT.childByAppendingPath("posts").childByAppendingPath(postID).setValue(true)
}
})
}
This is how I set current users:
var REF_USER_CURRENT:Firebase {
if let uid = NSUserDefaults.standardUserDefaults().valueForKey("uid") as? String {
let user = Firebase(url:"\(BASE_URL)").childByAppendingPath("users").childByAppendingPath(did){
return user!
} else {
return Firebase()
}
}
func createFirebaseUser(uid:String, user:Dictionary<String, AnyObject>) {
REF_USERS.childByAppendingPath(uid).setValue(user)
}
And this is the code in my login controller.
NSUserDefaults.standardUserDefaults().setValue(authData.uid, forKey: "uid")
let user = ["provider":authData.provider!,"displayName":authData.providerData["displayName"]!,"cachedProfile":authData.providerData["cachedUserProfile"]!, "imageURL":authData.providerData["profileImageURL"]!]
DataService.ds.createFirebaseUser(authData.uid, user: user)
self.performSegueWithIdentifier("loggedIn", sender: nil)
This is all the code I've written. I suspect it's something to do with NSUserDefaults. Any help would be greatly appreciated by this beginner.

Check your code. I found a typo here:
var REF_USER_CURRENT:Firebase {
if let uid = NSUserDefaults.standardUserDefaults().valueForKey("uid") as? String {
let user = Firebase(url:"\(BASE_URL)").childByAppendingPath("users").childByAppendingPath(did){
return user!
} else {
return Firebase()
}
}
Instead of "...childByAppendingPath(did)"
You might want to change "did" to "uid"
Other than that, I have not tested out the rest of your code.
Seems like You have watched the Mark Price course, for making this app.
So have I :)

Related

Firestore search for String in Array in Document

I want to search for an specific string value in an document which is in an array.
Here is my database:
This is my code so far: But it returns 0 documents:
func changePhotoUrlInPosts(url: String) {
let db = Firestore.firestore()
let user = UserService.currentUserProfile!
db.collection("posts")
.whereField("username", isEqualTo: user.username)
.getDocuments { (snapshot, error) in
if let indeedError = error {
print(indeedError.localizedDescription)
return
}
guard let indeedSnapshot = snapshot else {
print("snapshot is empty")
return
}
for document in indeedSnapshot.documents {
document.setValue(url, forKey: "photoUrl")
}
}
}
How can I go into my array in this document?
Thanks
Your screenshot is showing data in Realtime Database, but your code is querying Firestore. They are completely different databases with different APIs. You can't use the Firestore SDK to query Realtime Database. If you want to work with Realtime Database, use the documentation here.
There is author between posts and username field in your data structure.
Your code means that right under some specific post there is username field.
So such code will work because date right undes post:
db.collection("posts").whereField("date", isEqualTo: "some-bla-bla-date")
In your case you have two options as I see:
duplicate username and place this field on the same level as
date and guests.
re-write you code to check username inside author document.
Hope it will help you in your investigation.
So I changed my code to:
func loadData(url: URL){
let ref = Database.database().reference().child("posts")
let user = UserService.currentUserProfile!
ref.queryOrdered(byChild: "author/username").queryEqual(toValue: user.username).observe(.value, with: { snapshot in
if var post = snapshot.value as? [String : Any] {
print("updated all Posts")
post.updateValue(url.absoluteString, forKey: "photoUrl")
print(post.values)
}else{
print("fail")
}
})
}
It went through and I get the print statement of my values but the data didn't changed in the realtime database

How to store Firebase data to access easily and fast in Swift

I am working on my user profile page on my app and I need to retrieve data from Firebase and display the data on my user profile page. I have managed to retrieve the data successfully from the database but the problem is that every time I go to the user profile page, it takes a little time to access the information in the database, so as soon as you go to the user profile page, the page will be empty. How can I avoid this?
Put another way, is there a way to access the data and store before going to the user profile page and then displaying the data that is stored? Here is my code:
// Setup the name label
func setupNameLabel() {
// Access the database and get the current user's name
Database.database().reference().child("Users").child(userID!).child("Name").observeSingleEvent(of: .value) { (snapshot) in
guard let name = snapshot.value as? String else { return }
self.nameLabel.text = name
}
view.addSubview(nameLabel)
}
// Setup the username label
func setupUsernameLabel() {
// Access the database and get the current user's username
Database.database().reference().child("Users").child(userID!).child("Username").observeSingleEvent(of: .value) { (snapshot) in
guard let username = snapshot.value as? String else { return }
self.usernameLabel.text = username
}
view.addSubview(usernameLabel)
}
// Setup the email label
func setupEmailLabel() {
// Access the database and get the current user's email
Database.database().reference().child("Users").child(userID!).child("Email").observeSingleEvent(of: .value) { (snapshot) in
guard let email = snapshot.value as? String else { return }
self.emailLabel.text = email
}
view.addSubview(emailLabel)
}
Try To make a Model and then using Single Method and Get Data with Completion
func getData(forUserID: String, completion: #escaping (Model) -> Swift.Void, error: #escaping (Bool) -> Void) {
}
Check models of Salada cocoapod, please.
https://github.com/1amageek/Salada
Especially the models like Relation, Disposer, Set, Array, File are very beautiful.

Cache server data globally and refresh views

I'm building an app that uses firebase for authentication and database functionality. Once a user signs up, a database record is stored for that user, containing some basic information like first name, last name etc.
Once a user logs in with his credentials I want to set a global variable (perhaps userDefaults?) which contains the user data for that specific user. Otherwise I have to fetch user data for every time I want to fill a label with for instance, a user's first name.
I managed to set userdefaults upon login and use this info in UIlables. But when I let users make changes to their data, of which some is important for the functioning of the app, I can update the server AND the userdefaults but the app itself doesn't update with the correct data. It keeps the old data in (for example) UIlables.
I would love to get some more insight on what the best work-flow is to manage situations like these.
When opening the app, i have a tabBarController set as rootviewcontroller. In the load of tabbarcontroller I have the following code retrieving the user data from firebase and saving it to userdefaults:
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot.value ?? "")
guard let dictionary = snapshot.value as? [String: Any] else { return }
let firstname = dictionary["First name"] as? String
let lastname = dictionary["Last name"] as? String
print("first name is: " + firstname!)
UserDefaults.standard.set(firstname, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
self.setupViewControllers()
}
Then I continue on loading in all the viewcontrollers in the tabBarController:
self.setupViewControllers()
During that process the labels in those viewcontrollers get filled in with the userdefaults data.
This is an example of a label being filled in with userDefaults but not being updated upon changing of userdefaults:
let welcomeLabel: UILabel = {
let label = UILabel()
let attributedText = NSMutableAttributedString(string: "Welcome ")
attributedText.append(NSAttributedString(string: "\(UserDefaults.standard.string(forKey: "userFirstName")!)"))
label.attributedText = attributedText
label.font = UIFont.systemFont(ofSize: 30, weight: .bold)
return label
}()
this is a function i'm using to update the first name (via a textfield filled in by the user):
#objc func updateName() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("users").child(uid).updateChildValues(["First name" : updateNameField.text ?? ""])
UserDefaults.standard.set(updateNameField.text, forKey: "userFirstName")
print(UserDefaults.standard.value(forKey: "userFirstName"))
}
So you'll have to organize things first. In a new file define constants such as below. These constant will be accessible in global scope unless private
Constants.swift
private let storedusername = "usname"
private let storedName = "uname"
private let displaypic = "udp"
private let aboutme = "udesc"
var myusername : String {
get {
return (UserDefaults.standard.string(forKey: storedusername)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedusername)
}
}
var myname : String {
get {
return (UserDefaults.standard.string(forKey: storedName)!)
} set {
UserDefaults.standard.set(newValue, forKey: storedName)
}
}
var myProfileImage : Data {
get {
return (UserDefaults.standard.data(forKey: displaypic)!)
} set {
UserDefaults.standard.set(newValue, forKey: displaypic)
}
}
var myAboutMe : String? {
get {
return (UserDefaults.standard.string(forKey: aboutme)!)
} set {
UserDefaults.standard.set(newValue, forKey: aboutme)
}
}
Now the next time you want to save anything in UserDefaults, you'll just do the following anywhere throughout your code base :
myusername = "#CVEIjk"
And to retrive it, just call it :
print(myusername)
IMPORTANT NOTE --
Always remember to initialize them. You can do this as the user signs up. As soon as they fill out their details and hit submit, just save them to these variables. That wouldn't cause unnecessary crash.
You'll have to save them at every location you perform updates regarding these nodes in the database.
Now, the refreshing views part. I am taking a scenario where your ProfileView.swift has the view and user goes to EditProfile.swift for updating the content.
You initialize all your observers the place where the update will have the immediate effect. Because the view immediately after the update matters. The rest will be called through the getter of the aboutme
ProfileView.swift
func openEditView() {
NotificationCenter.default.addObserver(self, selector: #selector(fetchUserDetails), name: Notification.Name("update"), object: nil)
//setting this right before the segue will create an observer specifically and exclusively for updates. Hence you don't have to worry about the extra observers.
perform(segue: With Identifier:)// Goes to the editProfile page
}
This function will be initially called in viewDidLoad(). At this time you need to make sure you have all the data, else it will produce no values. But if you are storing everything as the user signs up, you are safe.
#objc func fetchUserDetails() {
if uid != nil {
if myname.count > 0 { // This will check if the variable has anything in the memory or not. Dont confuse this with [Array].count
self.nameLabel = myname
}
}
}
This function also acts an ab observer method. So when the notifications are posted they can run again.
Now, EditProfile.swift
In the block where you are updating the server, save the values and then create a Notification.post and put this method right before you dismiss(toViewController:)
func updateUserCacheData(name: String, username: String, aboutme: String, ProfilePhoto: UIImage? = nil) {
DispatchQueue.global().async {
myname = name
myusername = username
myAboutMe = aboutme
if self.newImage != nil {
myProfileImage = self.newImage!.jpegData(compressionQuality: 1)!
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .refreshProfileViews, object: nil)
}
}
}
func updateToServerAndBackToProfileView() {
self.updateUserCacheData(name: iname!, username: iusername, aboutme: iaboutme!)
self.dismiss(animated: true, completion: nil)
}
}
As long as this goes back to ProfileView, your views will be instantly refreshed. You can keep an observer wherever you view will be first displayed after the dismiss. the rest will fetch updated content always. Also, don't forget to deinit your Observer in ProfileView
//This code is in ProfileView.swift
deinit {
NotificationCenter.default.removeObserver(self, name: Notification.Name("update"), object: nil)
}
Also, in cases where the content might be empty, simply initialize it with empty content. For example, if user doesn't choose to add aboutme while signing up, you can just put
`myaboutme = ""`
This will create a safe environment for you and you are well set.

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

Best way to structure a friend list in Firebase

I am developing an iOS app using Swift 3 and Firebase.
In this app I am creating a friend list with this structure:
Friends
User_id
Friend_user_id_1
Friend_user_id_2
I want to get a list of friends for a specific user and this is possible but since I only store ids of friends I am missing Name and Email.
My idea is to do a second query for each user in the friend list to get their personal data. The option to this is to store the Name and Email along the friend_id but that feels cumbersome since the friend might change their Name or Email.
Friends
User_id
Friend_user_id_1
Name
Email
Is it ok making a second query to get user details on each loop or is that a performance killer?
Do I have to save the Name and Email in list too?
Update
This is how I solve it right now
func setupContent() {
let user = FIRAuth.auth()?.currentUser
if user != nil {
DataService.dataService.FRIEND_REF.child((user?.uid)!).observe(.value, with: { snapshot in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
DataService.dataService.USER_REF.child(snap.key).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? Dictionary<String, AnyObject>
let friend = Friend(key: snap.key, dictionary: value!)
self.friends.insert(friend, at: 0)
self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
}
}
}
})
}
}
Thankful for all help before I start coding.

Resources