ViewController reads data from firestore late - swift

I have username in firestore, whenever person logs in my app, I want to read that data(username) in firestore immediately, but it needs some time, if the problem is in slow internet, what other possible ways would you suggest to get my end result?
as yo can see in the gif above, loading username takes a little bit of time, but it is still a problem, I want it to be already loaded
#IBOutlet weak var welcomeNameLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
readData()
}
func readData(){
self.db.collection("users").getDocuments{ (snapshot, err) in
if let err = err {
print("error happened \(err)")
}else {
if let userId = Auth.auth().currentUser?.uid {
if let currentUserDoc = snapshot?.documents.first(where: { ($0["uid"] as? String) == userId }) {
var welcomeName = currentUserDoc["username"] as! String
self.welcomeNameLabel.text = "Welcome, \(welcomeName)"
}
}
}
}
}

Related

Firestore async issue

I'm calling a Firestore query that does come back, but I need to ensure completion before moving on with the rest of the code. So I need a completion handler...but for the life of me I can't seem to code it.
// get user info from db
func getUser() async {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
}
}
}
}
Called by:
internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
db = Firestore.firestore()
Database.database().isPersistenceEnabled = true
Task {
do {
let userInfo = await getUser()
}
} return true }
I used a Task as didFinishLauncingWithOptions is synchronous and not asynchronous
However, the getUser() still isn't completed before didFinishLauncingWithOptions moves on.
I need the data from getUser as the very next step uses the data in the array, and without it I get an 'out of bounds exception' as the array is still empty.
Also tried using dispatch group within the func getUser(). Again with no joy.
Finally tried a completion handler:
func getUser(completion: #escaping (Bool) -> Void) {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
completion(true)
}
}
}
}
Nothing works. The getUser call isn't completed before the code moves on. Can someone please help. I've searched multiple times, looked at all linked answers but I can not make this work.I'm clearly missing something easy, please help
read this post: Waiting for data to be loaded on app startup.
It explains why you should never wait for data before returning from
function application(_:didFinishLaunchingWithOptions).
To achieve what you need, you could use your first ViewController as a sort of splashscreen (that only shows an image or an activity indicator) and call the function getUser(completion:) in the viewDidLoad() method the ViewController.
Example:
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MyFirestoreDatabaseManager.shared.getUser() { success in
if success {
//TODO: Navigate to another ViewController
} else {
//TODO: Show an error
}
}
}
}
Where obviously MyFirestoreDatabaseManager.shared is the object on which you defined the getUser(completion:) method.
(In your example, I think that you defined that function in the AppDelegate. In that case, you should mark your getUser(completion:) method and all related variables as static. Then replace MyFirestoreDatabaseManager.shared with AppDelegate).
Not 100% sure what you would like to accomplish as I can't see all your code, but try something similar to this, replacing Objects for what you are trying to return from the documents.
You don't want your user's data spread across multiple documents. With Firebase you pay for every document you have to get. Ideally you want all your user's settings within one firebase document. Then create a UserInfo struct that you can decode to using the library CodeableFirebase or the decoder of your choice.
// Create user struct
struct UserInfo: Codable {
var userId: String
var userTrust: String
var userGrade: String
var userDisclaimer: String
}
// get user info from db and decode using CodableFirebase
func getUser() async throws -> UserInfo {
let doc = try await self.db.collection("users").document("userIdHere")
let userInfo = try FirestoreDecoder().decode(UserInfo.self, from: doc.data())
return UserInfo
}
Then you can do this...
Task {
do {
let userInfo = try await getUser()
let userTrust = userInfo.userTrust
let userGrade = userInfo.userGrade
let userDisclaimer = userInfo.userDisclaimer
}
}

swift show loader while reading data from firebase

i have a list of music at my firebase real time database and i am retriving them but i have 1000 musics data and i want to show loader when i reading data and stop loader when if there is a error(internet connection, or something else) or reading completed.
when i turn off the internet i couldn't get the data and can't stop loader to show error alert like there is no internet connection.
please help me how to handle that problem.
here is my code
didload function called from viewdidload()
private var musicArray = [ItemModal]() {
didSet {
view?.updateTableView()
}
}
func didLoad() {
view?.showLoader()
getAllMusics { ItemModal in
self.musicArray = ItemModal
self.view?.hideLoader()
}
}
func getAllMusics(completion: #escaping ([ItemModal]) -> Void) {
var musicArray = [ItemModal]()
ref.child("music").observeSingleEvent(of: .value) { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot {
guard let data = try? JSONSerialization.data(withJSONObject: rest.value as Any, options: []) else { return }
if let itemModal = try? JSONDecoder().decode(ItemModal.self, from: data) {
musicArray.append(itemModal)
}
}
completion(musicArray)
}
}
You can use reachability function by using https://github.com/ashleymills/Reachability.swift. To get to notify when the internet is turned off, you can implement reachabilityChanged Notification. In the selector method of reachabilityChanged, you can hide the loader.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged), name: .reachabilityChanged)
}
#objc func changed() {
if reachability?.isReachable {
//Continue success implementation
} else {
view?.hideLoder
//Implement Error handling
}
}

Download single Object of Firestore and save it into an struct/class object

I am coding since January 2019 and this is my first post here.
I am using Swift and Firestore. In my App is a tableView where I display events loaded out of a single Document with an array of events inside as [String: [String:Any]]. If the user wants to get more infos about an event he taps on it. In the background the TableViewController will open a new "DetailEventViewController" with a segue and give it the value of the eventID in the tapped cell.
When the user is on the DetailViewController Screen the app will download a new Document with the EventID as key for the document.
I wanna save this Data out of Firestore in a Struct called Event. For this example just with Event(eventName: String).
When I get all the data I can print it directly out but I can't save it in a variable and print it out later. I really don't know why. If I print the struct INSIDE the brackets where I get the data its working but if I save it into a variable and try to use this variable it says its nil.
So how can I fetch data out of Firestore and save in just a Single ValueObject (var currentEvent = Event? -> currentEvent = Event.event(for: data as [String:Any]) )
I search in google, firebaseDoc and stackoverflow but didn't find anything about it so I tried to save all the singe infos of the data inside a singe value.
// Struct
struct Event {
var eventName: String!
static func event(for eventData: [String:Any]) -> Event? {
guard let _eventName = eventData["eventName"] as? String
else {
print("error")
return nil
}
return Event(eventName: _eventName)
}
// TableView VC this should work
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowEventDetailSegue" {
if let ShowEvent = segue.destination as? DetailEventViewController, let event = eventForSegue{
ShowEvent.currentEventId = event.eventID
}
}
}
// DetailViewController
var currentEvent = Event()
var currentEventId: String?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let _eventID = currentEventId else {
print("error in EventID")
return}
setupEvent(eventID: _eventID) /* currentEvent should be set here */
setupView(event: currentEvent) /* currentEvent has after "setupEvent" the value of nil */
}
func setupEvent(eventID: String) {
let FirestoreRef = Firestore.firestore().collection("events").document(eventID)
FirestoreRef.getDocument { (document, error) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
SVProgressHUD.showError(withStatus: "Error in Download")
}else {
if let document = document, document.exists {
guard let data = document.data() else {return}
let eventData = Event.event(for: data as [String:Any])
print(eventData)
//here all infos are printed out - so I get them
self.currentEvent = eventData!
//Here is the error.. I can't save the fetched Data in my single current Event
} else {
SVProgressHUD.showError(withStatus: "Error")
}
}
}
}
func setupView(event: Event) {
self.titleLabel.text = event.eventName
}
I expect that the function setupEvents will give the currentEvent in the DetailViewController a SINGLEvalue cause its a SINGLE document not an array. So I can use this single Eventvalue for further actions. Like starting a new segue for a new ViewController and just push the Event there not

SearchBar problem while trying to search Firestore and reload the tableview

I have a tableView and I use infinite scroll to populate firestore data with batches. Also I have a searched bar and I am trying to query firestore with the text from the text bar and then populate it in the tableview. I have 3 main problems.
When I click search thee first time I get an empty array and an empty tableview, but when I click search the second time everything seems fine.
When I finally populate the searched content I want to stop fetching new content while I am scrolling.
If I text a wrong word and press search then I get the previous search and then the "No Ingredients found" printed twice.
This is my code for searchBar:
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let text = searchBar.text else {return}
searchIngredients(text: text)
self.searchBarIngredient.endEditing(true)
print("\(searchIngredients(text: text))")
}
The code for function when I click search
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
}else{
print("No Ingredients found")
}
}
}
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
Finally the code for scrolling
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
beginBatchFetch()
}
}
}
I don't write the beginBatchFetch() cause its working fine and I don't think is relevant.
Thanks in advance.
The issue in your question is that Firestore is asynchronous.
It takes time for Firestore to return documents you've requested and that data will only be valid within the closure calling the function. The code outside the closure will execute way before the data is available within the closure.
So here's what's going on.
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
}
//the code below here will execute *before* the code in the above closure
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
what's happening is the tableView is being refreshed before there's any data in the array.
You're also returning the ingredientsArray before it's populated. More importantly, attempting to return a value from an asynchronous function can (and should) generally be avoided.
The fix is to handle the data within the closure
class ViewController: NSViewController {
var ingredientArray = [String]()
func searchIngredients(text: String) {
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
//populate the class var array with data from firebase
// self.ingredientArray.append(some string)
//refresh the tableview
}
}
Note that the searchIngredients function should not return a value - nor does it need to

Firebase/Swift two database-connections

I have an application where I would like to connect to two different firebase-databases.
I have two viewControllers which are individually able to connect to each database, but if I connect to the first database, and afterwards connect to the other - the app Crashes
I have 2 info.plist files and 2 ViewControllers connected with a button/segue.
this is my first viewController
import UIKit
import Firebase
class ViewController: UIViewController {
var ref: DatabaseReference!
var handle:UInt!
override func viewDidLoad() {
super.viewDidLoad()
let filePath = Bundle.main.path(forResource: "GoogleService-Info-anden", ofType: "plist")
guard let fileopts = FirebaseOptions.init(contentsOfFile: filePath!)
else { assert(false, "Couldn't load config file") }
FirebaseApp.configure(options: fileopts)
ref = Database.database().reference()
ref?.child("UserVandreture").child("navn").setValue("Inger")
}
override func viewDidDisappear(_ animated: Bool) {
ref.removeObserver(withHandle: handle)
}
}
I can connect to firebase - which happens when I load the app.
Second viewController
import UIKit
import Firebase
class AndenDBViewController: UIViewController {
var ref: DatabaseReference!
var handle:UInt!
let fileName = "Bambusstien"
override func viewDidLoad() {
super.viewDidLoad()
let filePath = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist")
guard let fileopts = FirebaseOptions.init(contentsOfFile: filePath!)
else { assert(false, "Couldn't load config file") }
FirebaseApp.configure(options: fileopts)
ref = Database.database().reference()
response()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//kan læse navn,type, længde og link
func response() {
ref?.child("Vandreture").child(fileName).observe(.value, with: { (snapshot) in
let dict = snapshot.value as? [String: AnyObject]
let navn = dict!["navn"] as? String
print(navn as Any)
let type = dict!["type"] as? String
print(type as Any)
let Længde = dict!["length"] as? String
print(Længde as Any)
let link = dict!["link"] as? String
print(link as Any)
})
}
override func viewDidDisappear(_ animated: Bool) {
ref.removeObserver(withHandle: handle)
}
}
If I out-comment the database call in the previous viewController, this controller is also able to connect to firebase/database and read data, but if I don't outcomment this data, it will set the value in the first database, and when I press the button to go to the second viewController the app crashes.
I have set the targetMemebeship for the 2 plist-files to the same application-name.
The error message says that the app has already been configured - however I thought that ref.removeObserver(handle) in ViewDidDissapear() would disconnect the connection to the database, before connecting to the other database - so what is wrong??
Basically i needed protection of my data, but I had not populated the databases yet so..
The solution to my problem was to work with the rules-part - having just a single database.
Allowing users to have write-access to certain part of the database, and only having read-access to other parts - something like:
{
"rules": {
"myPersonal":{
".read": true,
".write": "auth != null"
},
"myPublic": {
".read": "auth != null",
".write": true
}
}
}