How to wait for asynchronous Firebase function before resuming code? - ios

I want to load the required data in the AppDelagate so this is what I currently have in place:
// Override point for customization after application launch.
FirebaseApp.configure()
FirebaseFunctions().getCompanies()
DispatchGroup().wait()
return true
}
The function FirebaseFunctions().getCompanies() looks like the following:
func getCompanies(){
DispatchGroup().enter()
let db = Firestore.firestore()
db.collection("companies").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let company = Company(name: document.documentID, discount: document.data()["discount"] as! String, website: document.data()["website"] as! String, code: document.data()["code"] as! String, categories: document.data()["categories"] as! String)
LocalData.companies.companyList.append(company)
}
}
DispatchGroup().leave()
}
}
However, when I use these functions and attempt to access my LocalData Class where the data is being stored, I am met with an error because they are loaded asynchronously. How can I delay the app in the AppDelagate until the data is loaded so that my UI can have access to the data?

Related

Firestore query completion

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.
As advised by comments I have tried to use the async / await calls:
function:
// 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

Load data from Firebase once

I am using Firebase as my backend service & fetching data using the SnapshotListener ( which keep listening for any data changes ). I want to remove that listener (only run and fetch data on first load).
private func loadShops(_ category: String) {
db.collection(category).addSnapshotListener { snapshot, error in
guard error == nil else {
print(error!.localizedDescription)
return
}
if let docs = snapshot?.documents {
for doc in docs {
self.shops = []
let shopData = doc.data()
var newShop = Shop()
newShop.shopName = shopData["name"] as? String ?? "Empty Name"
self.shops.append(newShop)
self.shops.shuffle()
}
self.shopsView.tableView.reloadData()
}
}
}
Based on the API example above - I'm assuming you are using the Firestore API and not the older Firebase.
If its firestore, you could just use query fetch instead of using a snapshot listener.
Refer to https://firebase.google.com/docs/firestore/query-data/queries#execute_a_query
private func loadShops(_ category: String) {
db.collection(category).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else if let querySnapshot = querySnapshot {
for document in querySnapshot.documents {
print("\(document.documentID) => \(document.data())")
// do the conversion to model object.
}
}
}
Using the snapshot listener is only if you want to continue monitoring the cloud for new documents. It is also costly as per the pricing documentation.

Table view not showing data from realm database on first launch Swift

My goal is to display data fetched from realm database on the table view that I previously asynchronously added whenever I launch the app.
The problem is that when I first launch my app the table view doesn't show any data fetched from realm database but if I pull it to refresh the data is displayed.
Here is what I do in the following code:
I fetch data (contacts) from apple's contacts framework and pass them through a closure to a helper method where I:
Add them to realm database
fetch the contacts and store them in the data source array (setDataSource)
create the section titles for the table view
So far I made the debugging and noticed that the problem maybe with threading and concurrency, that is, on first launch the data is being added to realm asynchronously so when I fetch the contacts there's nothing to fetch.
Saying that, I haven't been able to solve the above identified problem so far because I don't know how to. I am learning to code with threading and concurrency and to use realm.
I've also seen some posts but in none of them were a similar problem.
I'd be very grateful for your help.
Thank you!
Table view controller:
override func viewDidLoad() {
super.viewDidLoad()
populateTableView() // TODO: Contacts don't show up on first launch of the app after installation.
}
func populateTableView() {
contactManager.populateDataSource(for: self)
tableView.reloadData()
}
Contact manager Class:
let store = CNContactStore()
let dataBase = DataBase()
var contactsDataSource = [[Contact]]()
var sectionTitles = [String]()
var deletedContactsDataSource = [[Contact]]()
var sectionTitlesForDeletedContactsTable = [String]()
func fetchUserContacts(completion: #escaping ContactsFetchingResult) {
print("Attempting to fetch contacts today...")
store.requestAccess(for: .contacts) { (granted, error) in
if let errorToCatch = error {
print("Failed to request access: ", errorToCatch)
} else if granted {
print("Access granted")
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactThumbnailImageDataKey, CNContactImageDataAvailableKey, CNContactIdentifierKey, CNContactOrganizationNameKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try self.store.enumerateContacts(with: request, usingBlock: { (contact, stopPointerIfYouWantToStopEnumerating) in
completion(contact, nil)
})
} catch {
completion(nil, error)
}
} else {
print("Access denied")
}
}
}
func populateDataSource(for viewController: UIViewController?) {
fetchUserContacts { (result, error) in
if let errorToCatch = error {
guard let vc = viewController else { return }
UITableViewController.Alert.showFetchingErrorAlert(on: vc, message: errorToCatch.localizedDescription)
} else if let contact = result {
self.dataBase.insert(each: Contact(contact: contact, wasDeleted: false))
self.setDataSource()
self.setSectionTitles()
}
}
}
func setDataSource(from searchTerm: String = "") {
//Not deleted contacts
if !searching {
contactsDataSource = formatResults(from: dataBase.fetchContacts(), using: sectionTitles)
} else {
setDataSourceForFilteredContacts(from: searchTerm)
}
//Deleted contacts
deletedContactsDataSource = formatResults(from: dataBase.fetchDeletedContacts(), using: sectionTitlesForDeletedContactsTable)
}
func setSectionTitles() {
sectionTitles = generateSectionTitles(from: dataBase.fetchContacts())
sectionTitlesForDeletedContactsTable = generateSectionTitles(from: dataBase.fetchDeletedContacts())
}
Database:
func insert(each contact: Contact) {
let realm = try! Realm()
try! realm.write {
realm.add(contact, update: .modified)
}
}
func fetchContacts() -> Results<Contact> {
let realm = try! Realm()
return realm.objects(Contact.self).filter("wasDeleted = false").sorted(byKeyPath: "firstName", ascending: true)
}

How to load all data from firebase, before running UITableView functions in xcode/swift

I am trying to load data from my firestore database, so that when my Table View Controller loads, it will present data that is currently stored in the database.
To do this I call a function in ViewDidLoad() that populates an array of values retrieved from the database. However, when I run my code, I am getting an index out of bounds error in my cellForRowAt table View function. I assume that the code to set up the view is running before the actual array is getting populated. So there are no values in the array to set up the table. The data is being accessed because I am able to print the data to the console. Also when I print out my array.count it is returning 0. So I know the data is not getting put in the array.
I have searched for solutions but none are working for me. I have found solutions that suggest using GroupDispatch asynch functions to have it wait for the retrieval of my data. None of this seems to be working for me. Here is the code that I am trying.
var didfinishloading = false{
didSet{
self.tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
initialGet()
//populateDatabase()
}
func initialGet(){
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
db.collection("PlayerStats").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let stats = PlayerStats(name:document.data()["name"] as! String, points: document.data()["points"] as! Int, assists: document.data()["assists"] as! Int, rebounds: document.data()["rebounds"] as! Int, image: document.data()["image"] as! String)
self.playerArray.append(stats)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main){
self.didfinishloading = true
}
}
}
}
I use the didFinishLoading variable to tell when all the data has been retrieved, and then I want it to reload the data when it is set to true.
If anyone could provide help, or point me in the right direction with this?
You are misusing DispatchGroup. In your case you don't need it at all.
And reload the table view directly inside the closure.
func initialGet(){
db.collection("PlayerStats").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let data = document.data()
let stats = PlayerStats(name:data["name"] as! String,
points: data["points"] as! Int,
assists: data["assists"] as! Int,
rebounds: data["rebounds"] as! Int,
image: data["image"] as! String)
self.playerArray.append(stats)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
If you still get an out-of-bounds exception then there's something wrong with your table view datasource methods
override func viewDidLoad() {
super.viewDidLoad()
self.playerArray = [PlayerStats]()
initialGet()
//populateDatabase()
}
func initialGet(){
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
db.collection("PlayerStats").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let stats = PlayerStats(name:document.data()["name"] as! String, points: document.data()["points"] as! Int, assists: document.data()["assists"] as! Int, rebounds: document.data()["rebounds"] as! Int, image: document.data()["image"] as! String)
self.playerArray.append(stats)
dispatchGroup.leave()
}
}
}
dispatchGroup.wait()
dispatchGroup.notify(queue: .main) {
self.tableView.reloadData()
}
}
Make sure your playerArray is initialized and empty in viewDidLoad if you didn't instantiate it as an empty instance variable. Then move the dispatchGroup.notify out of the db.collection call. You can play around with the wait function maybe add on a timeout if you'd like. You don't need the didFinish check.

Enabling cache for AWS AppSync Client iOS Swift

I am using AWS AppSync for creating my iOS application. I want to leverage the offline mutation as well as query caching provided by AppSync. But when I am turning my internet off, I am not getting any response. Rather its showing an error as "The Internet connection appears to be offline.". This seems to be rather an Alamofire exception than an AppSync exception. This is because the query is not getting cached inside my device. Following is my code snippet to initialize the client.
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT, serviceRegion: AWSConstants.AWS_REGION, userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider())
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
UserDefaults.standard.set(nil, forKey: DeviceConstants.ID_TOKEN)
}
I am caching the token in the UserDefaults at the time of fetching the session, and then whenever the AppSyncClient is called, it fetches the latest token by calling the getLatestAuthToken() method of my MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider. This is returning the token stored in the UserDefaults -
// background thread - asynchronous
func getLatestAuthToken() -> String {
print("Inside getLatestAuthToken")
var token: String? = nil
if let tokenString = UserDefaults.standard.string(forKey: DeviceConstants.ID_TOKEN) {
token = tokenString
return token!
}
return token!
}
My query pattern is the following
public func getUserProfile(userId: String, success: #escaping (ProfileModel) -> Void, failure: #escaping (NSError) -> Void) {
let getQuery = GetUserProfileQuery(id: userId)
print("getQuery.id: \(getQuery.id)")
if appSyncClient != nil {
print("AppSyncClient is not nil")
appSyncClient?.fetch(query: getQuery, cachePolicy: CachePolicy.returnCacheDataElseFetch, queue: DispatchQueue.global(qos: .background), resultHandler: { (result, error) in
if error != nil {
failure(error! as NSError)
} else {
var profileModel = ProfileModel()
print("result: \(result)")
if let data = result?.data {
print("data: \(data)")
if let userProfile = data.snapshot["getUserProfile"] as? [String: Any?] {
profileModel = ProfileModel(id: UserDefaults.standard.string(forKey: DeviceConstants.USER_ID), username: userProfile["username"] as? String, mobileNumber: userProfile["mobileNumber"] as? String, name: userProfile["name"] as? String, gender: (userProfile["gender"] as? Gender).map { $0.rawValue }, dob: userProfile["dob"] as? String, profilePicUrl: userProfile["profilePicUrl"] as? String)
} else {
print("data snapshot is nil")
}
}
success(profileModel)
}
})
} else {
APPUtilites.displayErrorSnackbar(message: "Error in the user session. Please login again")
}
}
I have used all the 4 CachePolicy objects provided by AppSync, i.e,
CachePolicy.returnCacheDataElseFetch
CachePolicy.fetchIgnoringCacheData
CachePolicy.returnCacheDataDontFetch
CachePolicy.returnCacheDataAndFetch.
Can someone help me in implementing the cache properly for my iOS app so that I can do queries without the internet also?
Okay so I found the answer myself. The databaseUrl is an optional argument. It does not come in the suggestions when we are initializing the AWSAppSyncClientConfiguration object.
So the new way in which I initialized the client is the following
let databaseURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(AWSConstants.DATABASE_NAME, isDirectory: false)
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT,
serviceRegion: AWSConstants.AWS_REGION,
userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
urlSessionConfiguration: URLSessionConfiguration.default,
databaseURL: databaseURL)
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
}
Hope it helps.

Resources