Reload view controller when firebase data is loaded swift - ios

Ok, so my text label should display the name of the user, but the codes runs before the user data is fetched and the label doesn't change when the data is loaded.
First time i print the users name i get nil, but when i print within the firebase call i get the users name. how can i do this so i don't have to change the label with in the firebase call?
var ref: FIRDatabaseReference!
var user: User!
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
fetchUser()
self.titleLabel.text = user?.name
// Returns nil
print(user?.name)
}
func fetchUser() {
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
let name = value?["name"] as? String ?? ""
let birthdate = value?["birthdate"] as? String ?? ""
let gender = value?["gender"] as? String ?? ""
self.user = User(name: name, birthdate: birthdate, gender: gender)
// Now i get the users name
print(self.user.name)
}) { (error) in
print(error.localizedDescription)
}
}

If you do not want to access the label from fetchUser, you can use a simple callback.
override func viewDidLoad() {
//viewDidLoad code
fetchUser() {
self.titleLabel.text = user?.name
}
}
func fetchUser(_ completion: #escaping () -> Void) {
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { (snapshot) in
//fetchUser code
// Now i get the users name
print(self.user.name)
completion()
}) { (error) in
print(error.localizedDescription)
}
}

Related

Returning Nil When Running Method from separate class [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I have a View Controller that attempts to call a method from my UserModel class which gets a user document and fits the return data into a User structure. However, it is telling me it unexpectedly finds nil when unwrapping an optional value.
My UserModel:
class UserModel {
var user:User?
func getUser(userId: String) -> User? {
let docRef = Firestore.firestore().collection("Users").document(userId)
// Get data
docRef.getDocument { (document, error) in
if let document = document, document.exists {
var user:User = User(name: document["name"] as! String, phone: document["phone"] as! String, imageUrl: document["imageUrl"] as! String)
} else {
print("Document does not exist")
}
}
return user!
}
}
My Structure:
struct User {
var name:String
var phone:String
var imageUrl:String
}
My ViewController:
override func viewDidLoad() {
super.viewDidLoad()
userId = Auth.auth().currentUser?.uid
}
override func viewDidAppear(_ animated: Bool) {
let model = UserModel()
user = model.getUser(userId: userId!)
print(user?.name)
}
The method runs fine when it is inside my View Controller, so I know it's getting the uid, the database call works, and the values all exist. I have printed them all separately. However, within its own class it doesn't work.
Any ideas?
It looks like getDocument is an async function. Hence, you should make getUser async:
func getUser(userId: String, completion: #escaping (User?) -> Void) {
let docRef = Firestore.firestore().collection("Users").document(userId)
// Get data
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let user:User = User(name: document["name"] as! String, phone: document["phone"] as! String, imageUrl: document["imageUrl"] as! String)
completion(user)
} else {
completion(nil)
}
}
}
This is how you should call it:
let model = UserModel()
model.getUser(userId: userId!) { user in
print(user?.name)
}

How can I connect a post to user and display that user and post data in a Custom cell in TableView using Firebase

I am building a social media app and since I wasn't able to solve this by myself, I need help.
I have connected my Xcode project to the Firebase and made it possible for my users to register/sign in and publish Posts to the Firebase which are then shown all together in one group TableView but none of the data is connected to the user which posted that Post. The idea is that it looks similar to Instagram posts, but every post in my app would have to include only: Photo and Caption(optional) which are part of a Post Class, and CraftName which is a part of User Class. I believe that the problem lies in "denormalization" and incorrect populating of tableView.
Here is a photo of my Firebase tree which currently has 2 users signed in. One has posted 1 Post, and another has posted 2 Posts
https://i.imgur.com/fc2LEMk.jpg
I successfully register Users( with email, username and CraftName) to firebase database and I have made it possible for them to sign in and sign out so I believe the problem is somewhere else to look. I have also made it possible to post a Post to Firebase which includes Photo and Caption and to populate the tableView with that two objects. Only thing left is to connect user with it's Post and while displaying the Post, provide that User's CraftName as a TextView above the Post.
This is AuthService class which deals with registration
static func register(username: String, email: String, password: String, craftName: String, onSuccess: #escaping () -> Void, onError: #escaping (_ errorMessage: String?) -> Void) {
Auth.auth().createUser(withEmail: email, password: password, completion: { (user, error) in
if error != nil {
onError(error!.localizedDescription)
return
}
let uid = Auth.auth().currentUser!.uid
self.setUserInformation(username: username, email: email, craftName: craftName, uid: uid, onSuccess: onSuccess)
})
}
This is a registration class
#IBAction func registerButtonPressed(_ sender: Any) {
view.endEditing(true)
AuthService.register(username: usernameTextField.text!, email: emailTextField.text!, password: passwordTextField.text!, craftName: craftNameTextField.text!, onSuccess: {
self.performSegue(withIdentifier: "registerToTabBar", sender: self)
}, onError: {error in
ProgressHUD.showError(error!)
})
}
}
//Stvaranje reference na Firebase Realtime bazu podataka i spremanje svih podataka svakog korisnika zasebno u tu bazu podataka
static func setUserInformation(username: String, email: String, craftName: String, uid: String, onSuccess: #escaping () -> Void){
let ref = Database.database().reference()
let usersReference = ref.child("users")
let newUserReference = usersReference.child(uid)
newUserReference.setValue(["username": username, "email": email, "craftName": craftName])
onSuccess()
}
}
This is my User Class
struct User {
var username: String?
var email: String?
var craftName: String
init(craftNameString: String, emailString : String, usernameString: String){
craftName = craftNameString
username = usernameString
email = emailString
}
}
This is my Post Class
class Post {
var caption: String?
var photoUrl: String?
var numberOfClaps: Int?
init(captionText: String, photoUrlString: String) {
caption = captionText
photoUrl = photoUrlString
}
}
This is my Post custom cell class
class PostCell: UITableViewCell {
#IBOutlet weak var postImageView: UIImageView!
#IBOutlet weak var captionTextViev: UITextView!
#IBOutlet weak var craftNameTextField: UITextView!
var post : Post! {
didSet{
self.updatePostUI()
}
}
var user : User! {
didSet{
self.updateUserUI()
}
}
func updatePostUI() {
captionTextViev.text = post.caption
}
func updateUserUI(){
craftNameTextField.text = user.craftName
}
}
Now when my users are Registered, they can post a Post in CameraClass.
#IBAction func shareButtonPressed(_ sender: Any) {
if let postImg = selectedImage, let imageData = postImg.jpegData(compressionQuality: 1) {
let photoIDString = NSUUID().uuidString
print(photoIDString)
let storageRef = Storage.storage().reference(forURL: Config.STORAGE_ROOT_REF).child("posts").child(photoIDString)
storageRef.putData(imageData, metadata: nil, completion: { (metadata, error) in
if error != nil {
ProgressHUD.showError(error?.localizedDescription)
return
}
storageRef.downloadURL(completion: { (url, error) in
if error != nil {
ProgressHUD.showError(error!.localizedDescription)
}else {
if let photoURL = url?.absoluteString{
self.sendDataToDatabase(photoUrl: photoURL)
}
}
})
})
}
}
func sendDataToDatabase(photoUrl: String) {
let ref = Database.database().reference()
let uid = Auth.auth().currentUser!.uid
let postsReference = ref.child("posts")
let userUID = postsReference.child(uid)
let newPostID = userUID.child(userUID.childByAutoId().key!)
newPostID.setValue(["photoUrl": photoUrl, "caption": captionTextView.text!], withCompletionBlock: { (error, ref) in
if error != nil {
ProgressHUD.showError(error!.localizedDescription)
return
}
ProgressHUD.show("UspjeĆĄno ste objavili fotografiju")
ProgressHUD.dismiss()
self.clean()
self.tabBarController?.selectedIndex = 0
})
This is my HomeViewController in which is the tableView that is supposed to display Posts
class HomeViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var posts = [Post]()
struct Storyboard {
static let postCellDefaultHeight : CGFloat = 578.0
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.estimatedRowHeight = Storyboard.postCellDefaultHeight
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorColor = UIColor.clear
loadPosts()
}
//Function which is supposed to retrieve data from database and populate the TableView
func loadPosts() {
let ref = Database.database().reference()
let posts = ref.child("posts")
posts.observe(.value) { (snapshot) in
for currentUser in (snapshot.children) {
let cUSer = currentUser as! DataSnapshot
for postInfo in (cUSer.children) {
let postSnap = postInfo as! DataSnapshot
let dict = postSnap.value as? [String: Any]
let captionText = dict!["caption"] as! String
let photoUrlString = dict!["photoUrl"] as! String
let post = Post(captionText: captionText, photoUrlString: photoUrlString)
self.posts.append(post)
self.tableView.reloadData()
}
}
}
}
It looks like this right now:
https://i.imgur.com/lMxZYLW.jpg
This is how I would like it to look:
https://i.imgur.com/721mEXm.jpg

Calling a reloadtableview() only once while using Firebase observe function

As the title says, I only want reloadtableview() to be called once. How do i approach this?
Here is my code:
var posts = [Post]() {
didSet {
posts.reverse()
self.tableView.reloadData()
}
}
func fetchData(){
currentQuery.observe(.value, with: { (snapshot) in
if let snapshot = snapshot.children.allObjects as? [DataSnapshot]{
var foundPosts = [Post]()
for snap in snapshot {
if let postDict = snap.value as? Dictionary<String, Any>{
let key = snap.key
let post = Post.init(postKey: key, postData: postDict)
foundPosts.append(post)
}
}
self.posts = foundPosts
geoQuery?.removeAllObservers()
}
})
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchData()
// if I put "tableView.reloadData()" here, it wont load when the app opens for the first time
}
With this, the tableview is reloading everytime something is changed in the database. I want to only reload it once, but still have that observer there.
If I understood you right, the code below should do what you wanted. (Couldn't squeeze it into the comments)
var shouldReloadTableView = true
var posts = [Post]() {
didSet {
posts.reverse()
guard shouldReloadTableView else { return }
shouldReloadTableView = false
self.tableView.reloadData()
}
}

Swift 3 - Save Firebase Data In Array

I am trying to save my Firebase Data in an array but the array count is everytime 0.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
getAllSkateShops()
}
func getAllSkateShops() {
ref = FIRDatabase.database().reference()
ref.child("Shops").observeSingleEvent(of: .value, with: { (snapshot) in
//var newShops: [SkateShop] = []
for item in snapshot.children {
let skateShopItem = SkateShop(snapshot: item as! FIRDataSnapshot)
self.shops.append(skateShopItem)
DispatchQueue.main.async(execute: {
print("OBSERVE SHOP COUNT: \(self.shops.count)")
})
}
})
}
And in the function viewDidLoad() is self.shop.count is zero but I need this array with all Shops.
I hope anybody can help me ;)
I had the same problem, idk why this works but in DispatchQueue.main.async do (edited):
func getAllSkateShops() {
ref = FIRDatabase.database().reference()
ref.child("Shops").observeSingleEvent(of: .value, with: { (snapshot) in
//var newShops: [SkateShop] = []
for item in snapshot.children {
let skateShopItem = SkateShop(snapshot: item as! FIRDataSnapshot)
self.shops.append(skateShopItem)
self.shops2.append(skateShopItem)
DispatchQueue.main.async(execute: {
var temporaryArray = self.shop2
self.shop = temporaryArray
})
}
})
}
If that doesn't work comment, I'll give another option that might work.

PickerInlineRow, PickerRow - Best Practices for populating options

Fairly new to iOS. I am using Firebase for the backend data, and I'm wondering about the recommended approach for populating option lists for things such as a PickerInlineRow. What I have typically done is the following;
Create variables to hold the data used in my form
Call Firebase to retrieve the data
Load the values from Firebase into my local variables
In the closure for the Firebase call, load the form
In the form, populate the values by using my variables
Update the variables using .onchange events
When the user saves, the variables are used to update the database. This all works, but the problem comes about when trying to populate dropdowns within the form. I know how to set options for the picker, but unclear as to how to structure the sequence so that the array I use for options is populated prior to use. If I set the options to an array, but the array hasn't finished populating, the picker has no values.
What's the recommended way to coordinate these events? I've pasted an example Eureka form below.
import UIKit
import Firebase
import GeoFire
import Eureka
class QuestDetailsViewController: FormViewController {
let ref: FIRDatabaseReference = FIRDatabase.database().reference()
var key = String()
var isNew = Bool()
var locationKeys = [String]()
var locationNames = [String]()
var locationKey: String?
var locationName: String?
var startDate: Date?
var endDate: Date?
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
func loadData(){
// load lookup values
if isNew == false {
ref.child("Quests").child(key).observeSingleEvent(of: .value, with: {(snapshot) in
if let item = snapshot.value as? [String:AnyObject]{
self.locationName = item["LocationName"] as? String
self.locationKey = item["LocationKey"] as? String
self.startDate = DateFromInterval(interval: (item["StartDate"] as? Double)!)
self.endDate = DateFromInterval(interval: (item["EndDate"] as? Double)!)
}
self.loadDropdowns()
} , withCancel: {error in
print("Error : \(error.localizedDescription)")
})
}
else {
self.loadDropdowns()
}
}
func loadDropdowns() {
ref.child("Places").queryOrdered(byChild: "PlaceName").observeSingleEvent(of: .value, with: {(snapshot) in
for item in (snapshot.children.allObjects as? [FIRDataSnapshot])! {
let thisPlace = item.value as! [String: AnyObject]
self.locationKeys.append(item.key)
self.locationNames.append(thisPlace["PlaceName"] as! String)
}
self.loadForm()
}, withCancel: {error in
})
}
func loadForm() {
form +++ PickerInlineRow<String>() {
$0.tag = "locationPicker"
$0.title = "Location"
$0.options = locationNames
$0.value = self.locationName
}.onChange({ (row) in
self.locationName = row.value
let itemIndex = self.locationNames.index(of: self.locationName!)
self.locationKey = self.locationKeys[itemIndex!]
})
<<< DateTimeRow() {
$0.tag = "startDate"
$0.title = "From"
$0.value = self.startDate
}.onChange({ (row) in
self.startDate = row.value
})
<<< DateTimeRow() {
$0.tag = "endDate"
$0.title = "To"
$0.value = self.endDate
}.onChange({ (row) in
self.endDate = row.value
})
+++ ButtonRow() {
$0.title = "Challenges"
$0.presentationMode = PresentationMode.segueName(segueName: "segueChallenges", onDismiss: nil)
}
+++ ButtonRow() {
$0.title = "Save Changes"
}.onCellSelection({ (cell, row) in
self.saveChanges()
})
}
func saveChanges() {
let childUpdates = ["LocationKey": locationKey!, "LocationName": locationName!, "StartDate": IntervalFromDate(date: startDate!), "EndDate": IntervalFromDate(date: endDate!)] as [String : Any]
if isNew == true {
key = ref.child("Quests").childByAutoId().key
}
ref.child("Quests").child(key).updateChildValues(childUpdates, withCompletionBlock: {(error, ref) in
self.navigationController?.popViewController(animated: true)
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueChallenges" {
let vc = segue.destination as? ChallengesTableViewController
vc?.questKey = key
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You want to load the form without any values, then when the values come back from firebase, you can reload the rows using row tags
//Completion handler of Firebase {
if let row = self.form.rowBy(tag: "rowTag") as? PickerInlineRow<RowType> {
row.reload()
}
}

Resources