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
Related
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?
I get the exact same error as #62999928, but not with the same configuration. I'm just trying to access a global Realm file which is on my Realm Cloud (I am NOT on MongoDB and the beta).
I get the βOperation canceledβ Realm Error Domain=io.realm.unknown Code=89 error when I try to open the Realm.
Realm Studio:
Opening code:
let url = URL(string: "realms://\(MY_INSTANCE_ADDRESS)/common")!
let config = user.configuration(realmURL: url, fullSynchronization: true)
Realm.asyncOpen(configuration: config) { ... }
Authentication code (using PromiseKit):
private func connectToRealm(_ firebaseUser: User) -> Promise<SyncUser> {
// if realm user already logged in, check if it's the right user
if let user = SyncUser.current {
guard user.identity == firebaseUser.uid else {
// it's not the right user. log out and try again.
user.logOut()
return connectToRealm(firebaseUser)
}
// it's the right user.
return Promise.value(user)
}
return firstly {
// get JWT token with firebase function
Promise {
Functions.functions().httpsCallable("myAuthFunction").call(completion: $0.resolve)
}
}
.compactMap { result in
// extract JWT token from response
return (result?.data as? [String: Any])?["token"] as? String
}
.then { token in
// connect to Realm
Promise {
SyncUser.logIn(with: SyncCredentials.jwt(token), server:MY_AUTH_URL, onCompletion: $0.resolve)
}
}
}
This method is called after logging with Firebase. The auth URL is just https://\(MY_INSTANCE_ADDRESS).
I suspect it's a permission problem, in which case: how can I easily let this file be readable by everyone, but not writable? I'm not planning on creating a lot of theses files, I just want to do this once.
EDIT: I reduced my code to the bare minimum.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let MY_INSTANCE_ADDRESS = [...]
let AUTH_URL = URL(string: "https://\(MY_INSTANCE_ADDRESS)")!
let COMMON_REALM_URL = URL(string: "realms://\(MY_INSTANCE_ADDRESS)/common")!
SyncUser.logIn(with: SyncCredentials.anonymous(), server: AUTH_URL) { (user, error) in
guard error == nil && user != nil else {
print(error)
return
}
let config = user!.configuration(realmURL: COMMON_REALM_URL, fullSynchronization: true)
Realm.asyncOpen(configuration: config) { (realm, error) in
guard error == nil && realm != nil else {
print(error)
return
}
print("Successfully loaded Realm file with \(realm!.objects(MyObjectType.self).count) objects.")
}
}
}
Nothing else is running, I don't load my app window. Still getting the same error.
EDIT 2: changed code according to #Jay's comment.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let MY_INSTANCE_ADDRESS = [...]
let AUTH_URL = URL(string: "https://\(MY_INSTANCE_ADDRESS)")!
let COMMON_REALM_URL = URL(string: "realms://\(MY_INSTANCE_ADDRESS)/common")!
SyncUser.logIn(with: SyncCredentials.anonymous(), server: AUTH_URL) { (user, error) in
guard error == nil && user != nil else {
print(error)
return
}
print("logged in")
let config = SyncUser.current?.configuration(realmURL: COMMON_REALM_URL, fullSynchronization: true)
let realm = try! Realm(configuration: config!)
// $$$
let objectsResult = realm.objects(MyObjectType.self)
self.notificationToken = objectsResult.observe { changes in
print("changes: \(changes)")
if case .initial(let results) = changes {
print("\(results.count) results: \(results)")
}
}
// $$$
}
}
Result:
logged in
changes: initial(Results<MyObjectType> <0x155d2d4e0> (
))
0 results: Results<MyObjectType> <0x155d2d4e0> (
)
No error, but I don't get the content that are on the cloud.
EDIT 3 Okay that's interesting. By changing the code between the $$$ to:
try! realm.write {
realm.create(MyObjectType.self, value: MyObjectType(id: 2021, name_fr: "test_fr", name_eng: "test_eng"))
}
let objectsResult = realm.objects(MyObjectType.self)
print(objectsResult)
I get the result:
logged in
Results<MyObjectType> <0x143fe0510> (
[0] MyObjectType {
id = 2020;
name_fr = test_fr;
name_eng = test_eng;
}
)
So the object is being written. When I run again the same code (without the login part) and changing the object id, I get:
Results<MyObjectType> <0x111d507c0> (
[0] MyObjectType {
id = 2020;
name_fr = test_fr;
name_eng = test_eng;
},
[1] MyObjectType {
id = 2021;
name_fr = test_fr;
name_eng = test_eng;
}
)
So the objects are stored somewhere. However, I don't see these two objects in Realm Studio. In Realm Studio, I see the objects that I added using a JS script, which should be downloaded to the device but are not.
When I run again the code after logging out, the objects are removed. All the URLs are correct, and I checked that no other Realm file is created by running the code.
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)
}
I'm currently trying to develop an ios application using Firestore, and when querying the database, I'm getting no results, as neither the if/else block execute. I'm wondering what is going wrong here...
db.collection("users").whereField("uid", isEqualTo: uid).getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error.localizedDescription)")
} else {
for document in querySnapshot!.documents {
weight = document.data()["weight"]! as? Double
}
}
}
Database file structure
Update: I make a call to the database in an earlier method, and this properly returns the user's first name (when I add the weight, it also returns the correct value). But any subsequent calls fail to return anything. Hopefully that info helps.
I have the same Firestore structure like you, and this works for me:
func test() {
var accessLevel: Double?
let db = Firestore.firestore()
db.collection("users").whereField("uid", isEqualTo: UserApi.shared.CURRENT_USER_UID!).getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error.localizedDescription)")
} else {
for document in querySnapshot!.documents {
accessLevel = document.data()["accessLevel"]! as? Double
print(accessLevel!)
}
}
}
}
Current uid:
// Actual logged-in User
var CURRENT_USER_UID: String? {
if let currentUserUid = Auth.auth().currentUser?.uid {
return currentUserUid
}
return nil
}
Hope it helps.
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("πππππππIn closure function to update articlesπππππππ")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("πππππππ sending back completion array \(returnArray)πππππππ")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}