Firebase Firestore query not executing - swift

func setExpenses(){
FirebaseFunctions().retrieve(from: .expense, username: username as! String, returning: Expenses.self) { (expenses) in
self.expenses = expenses
}
}
I currently have a firebase query as seen above which retrieves a list of expenses from a cloud firestore database. However, when I run the function bellow and try and print the array, I get a result of the array being empty. I don't understand why the query isn't being able to execute correctly. I have the same code in another view controller, and it works fine which makes me think that it is something to do with the timing. But can somebody please help me to solve this issue?
public func getCollectionExpenses(collection: String, completionHandler: #escaping([[Expenses]], [String]) -> Void){
setExpenses()
print(expenses)
print("hello")
for eachExpense in expenses{
if eachExpense.collection == collection{
expensePerCollection.append(eachExpense)
}
}
Here is the code for the retrieve function, just in case
func retrieve<T: Decodable>(from collectionReference:FIRCollectionReference, username:String, returning objectType: T.Type, completion: #escaping (([T]) -> Void)) {
referenceSub(to: collectionReference, username: username).addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else { return }
do {
var objects = [T]()
for document in snapshot.documents {
let object = try document.decode(as: objectType.self)
objects.append(object)
}
completion(objects)
} catch {
print(error)
}
}
}

Where are you setting snapshot.documents? Looks like you need to set in then iterate.

Related

Saving string in flatMap block to database in api call using Combine Swift

I am trying to fetch a value from local database and when not found wants to save it to local database and return it. All of these I am doing in Interactor file and actual saving or fetching is done in seperate file. Following is my code:
public func fetchCode(codeId: String) -> AnyPublisher<String?, Error> {
//Get code from localdb
codeStorageProvider.fetchCode(codeId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).mapError{ $0 as Error }.eraseToAnyPublisher()
}
//If not found in db, Get code from server
let code = self.voucherCodeProvider.fetchVoucherCode(codeId: codeId)
return code.flatMap { code in
//save the code to local db
self.codeStorageProvider.saveVoucherCode(code, codeId)
return code
}.eraseToAnyPublisher()
//return code to presenter
}.eraseToAnyPublisher()
}
I am getting following error in flatMap:
Type of expression is ambiguous without more context
Can someone please help me?
If your saveVoucher doesn't return a Publisher and you are not interested in knowing when the operation is completed, there's no need to use flatMap but you can use handleEvents and call the side effect to save the code from there. Something like this:
func fetchLocal(codeId: String) -> AnyPublisher<String?, Error> {
return Empty().eraseToAnyPublisher()
}
func fetchRemote(codeId: String) -> AnyPublisher<String, Error> {
return Empty().eraseToAnyPublisher()
}
func saveLocal(code: String, codeId: String) {
// Save to BD
}
func fetch(codeId: String) -> AnyPublisher<String?, Error> {
return fetchLocal(codeId: codeId)
.flatMap { code -> AnyPublisher<String, Error> in
if let code = code {
return Just(code)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return fetchRemote(codeId: codeId)
.handleEvents(receiveOutput: {
saveLocal(code: $0, codeId: codeId)
})
.eraseToAnyPublisher()
}
}
.map(Optional.some)
.eraseToAnyPublisher()
}

SWIFTUI Firebase Retrieving Subcollection Data

I do understand, that all request from firebase are async.
I have collection tasksCategory -> document -> subcollection tasks
This is my class for getting all created tasks category, there is no problem. Problem is that I need to retrieve all tasks for each category by passing document ID.
class fsTasks: ObservableObject {
#Published var categories = [fsTaskCategory]()
init() {
fsGetTaskCategories()
}
/// Retrieve Tasks Categories For Logged In User
func fsGetTaskCategories() {
db.collection("tasksCategories").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.categories = documents.compactMap { queryDocumentSnapshot -> fsTaskCategory? in
return try? queryDocumentSnapshot.data(as: fsTaskCategory.self)
}
}
}
}
I have create another function to retrieve all tasks for each passed document ID
func fsGetTasks(documentID: String, completation: #escaping([fsTask]) -> Void) {
var tasks = [fsTask]()
db.collection("tasksCategories").document(documentID).collection("tasks").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
tasks = documents.compactMap { queryDocumentSnapshot -> fsTask? in
return try? queryDocumentSnapshot.data(as: fsTask.self)
}
completation(tasks)
}
}
Problem is that I do not have any idea, how can I call this function directly in the view of SWIFTUI.
Basically I have first ForEach through the ObservedObject of all categories, then I need to do another foreach for all tasks for each category, but first I need to retrieve data. I need function which return an array with all tasks retrieved from firebase but only when completation handler return data.
If I have function like this
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
I was still retrieving only empty array.
This is the issue
func retrieveAllTasks(categoryID: String) -> [fsTasks] {
var fetchedTasks = [fsTasks]()
fsGetTasks(documentID: categoryID, completation: { (tasks) in
fetcheTasks = tasks
})
return fetchedTasks
}
This is an asynchronous function as well (see the closure) and you have to give Firebase time to retrieve the data from the server and handle it within the Firebase closure.
What's happening here is that while you are doing that within the Firebase closure itself, that's not happening within this closure. return fetchedTasks is returning before fetchedTasks = tasks.
I would call the firebase function directly since it doesn't appear you need the middleman retrieveAllTasks function
self.fsGetTasks(documentID: "some_doc", completion: { taskArray in
for task in taskArray {
print(task
}
})
If you do, you need to add an #escaping clause to that as well and not use return fetchedTasks

Firebase Firestore fetching data with an ID reference then fetching the reference

After searching for a few hours for the answer to this question, I have found 1 post that was similar here: However I tried to replicate but I believe the difference in language syntax made it very hard to translate.
Within my application, users are allowed to make posts, the structure for the post in Firsestore looks like this:
The creator is a userId of a user that also lives in the database.
I am aware of how to fetch things from Firestore when my structs conform to Codable and they map 1 to 1 but I have not experienced having to fetch nested data after an initial fetch.
Question
By querying my backend for posts, how can I also create the user object that lives inside?
Here is the post object I was expecting to create:
import FirebaseFirestoreSwift
public struct Post: Codable {
/// The school id
#DocumentID var id: String?
/// The name of the school
public let content: String
/// The user who made the post
public var creator: AppUser?
}
I want to create appUser from the creator field that is returned. Should I build the content and then have some sort of promise.then to fetch the user? Or can i do both at the same time?
Here is what I think I should be doing
public func fetch(for schoolId: String) -> Promise<[Post]> {
return Promise { resolver in
fireStore
.collection("schools").document(schoolId)
.collection("posts").getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {
resolver.reject(Errors.firebaseError)
return
}
let posts = documents.compactMap { queryDocumentSnapshot -> Post? in
return try? queryDocumentSnapshot.data(as: Post.self)
}
let postsWithUser: [Post] = posts.map { post in
//Fetch User and return an updated struct
}
resolver.fulfill(postsWithUser)
}
}
}
I solved it! Basically, we want to let the first fetch complete. Then we iterate through each post.id and call FetchUser() i which is a function i built that returns Promise<User>
func fetchTopLevelPost(for schoolId: String) -> Promise<[Post]> {
return Promise { resolver in
fireStore
.collection("schools").document(schoolId)
.collection("posts").getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {
resolver.reject(Errors.firebaseError)
return
}
let posts = documents.compactMap { queryDocumentSnapshot -> Post? in
return try? queryDocumentSnapshot.data(as: Post.self)
}
resolver.fulfill(posts)
}
}
}
func fetchPostUser(for posts: [Post]) -> Promise<[Post]> {
let allPromise = posts.map{ FetchUser().fetch(for: $0.creatorId) }
return Promise { resolver in
when(fulfilled: allPromise).done { users in
let completePost = zip(users, posts).map(post.init(completeData:))
resolver.fulfill(completePost)
}
.catch { error in
resolver.reject(Errors.firebaseError)
}
}
}
Here is the callsite:
public func fetch(for schoolId: String) -> Promise<[Post]> {
return fetchTopLevelPost(for: schoolId)
.then { self.fetchPostUser(for: $0) }
}

Future Combine sink does not recieve any values

I want to add a value to Firestore. When finished I want to return the added value. The value does get added to Firestore successfully. However, the value does not go through sink.
This is the function that does not work:
func createPremium(user id: String, isPremium: Bool) -> AnyPublisher<Bool,Never> {
let dic = ["premium":isPremium]
return Future<Bool,Never> { promise in
self.db.collection(self.dbName).document(id).setData(dic, merge: true) { error in
if let error = error {
print(error.localizedDescription)
} else {
/// does get called
promise(.success(isPremium))
}
}
}.eraseToAnyPublisher()
}
I made a test function that works:
func test() -> AnyPublisher<Bool,Never> {
return Future<Bool,Never> { promise in
promise(.success(true))
}.eraseToAnyPublisher()
}
premiumRepository.createPremium(user: userID ?? "1234", isPremium: true)
.sink { receivedValue in
/// does not get called
print(receivedValue)
}.cancel()
test()
.sink { recievedValue in
/// does get called
print("Test", recievedValue)
}.cancel()
Also I have a similar code snippet that works:
func loadExercises(category: Category) -> AnyPublisher<[Exercise], Error> {
let document = store.collection(category.rawValue)
return Future<[Exercise], Error> { promise in
document.getDocuments { documents, error in
if let error = error {
promise(.failure(error))
} else if let documents = documents {
var exercises = [Exercise]()
for document in documents.documents {
do {
let decoded = try FirestoreDecoder().decode(Exercise.self, from: document.data())
exercises.append(decoded)
} catch let error {
promise(.failure(error))
}
}
promise(.success(exercises))
}
}
}.eraseToAnyPublisher()
}
I tried to add a buffer but it did not lead to success.
Try to change/remove .cancel() method on your subscriptions. Seems you subscribe to the publisher, and then immediately cancel the subscription. The better option is to retain and store all your subscriptions in the cancellable set.

How to use the when function in Promisekit loop

I have an array of appointments and I'm trying to grab all of the photos for these appointments from our windows azure blob storage. First, I want to get the list of blobs with the associated appointmentId so I can download and store them properly afterwards.
I'm using PromiseKit but I'm not at all sure about how to use PromiseKit in a loop:
for appointment in appointments {
// Get blobs
}
Here's my code so far. Any help is greatly appreciated!
func getBlobsPromise(appointmentId: Int32) -> Promise<[BlobDownload]> {
return Promise { seal in
var error: NSError?
var blobDownloads = [BlobDownload]()
container = AZSCloudBlobContainer(url: URL(string: containerURL)!, error: &error)
if ((error) != nil) {
print("Error in creating blob container object. Error code = %ld, error domain = %#, error userinfo = %#", error!.code, error!.domain, error!.userInfo)
seal.reject(error!)
}
let prefix: String = "AppointmentFiles/\(appointmentId)"
container?.listBlobsSegmented(with: nil, prefix: prefix, useFlatBlobListing: true, blobListingDetails: AZSBlobListingDetails(), maxResults: 150) { (error : Error?, results : AZSBlobResultSegment?) -> Void in
if error != nil {
seal.reject(error!)
}
for blob in results!.blobs!
{
let blobInfo = blob as! AZSCloudBlob
if blobInfo.blobName.lowercased().contains("jpg") || blobInfo.blobName.lowercased().contains("jpeg") {
let blobDownload: BlobDownload = BlobDownload(appointmentId: Int(jobId), blob: blobInfo)
blobDownloads.append(blobDownload)
}
}
seal.fulfill(blobDownloads)
}
}
}
That returns the blobs as expected but I want to get all of the blobs for all of the appointments before proceeding. Here's what I tried (among other things):
func getBlobsForAllJobs(appointmentIds: [Int32]) -> Promise<[BlobDownload]> {
return Promise { seal in
let count = appointmentIds.count - 1
let promises = (0..<count).map { index -> Promise<[BlobDownload]> in
return getBlobsPromise(agencyCode: agencyCode, appointmentId: appointmentIds[index])
}
when(fulfilled: promises).then({ blobDownloads in
seal.fulfill(blobDownloads)
})
}
}
EDIT 1
I solved this using a DispatchGroup and completion handler. Here's the code in case someone is interested. If there are alternate (better) ways of doing this I'd love to hear them. I'm a c# guy just getting into Swift.
func getBlobsToDownload(appointmentIds: [Int32], completion: #escaping ([BlobDownload]) -> Void) {
var myBlobsToDownload = [BlobDownload]()
let myGroup = DispatchGroup()
for apptId in appointmentIds {
myGroup.enter()
getBlobs(appointmentId: apptId) { (blobDownloads) in
print("Finished request \(apptId)")
print("Blobs fetched from apptId \(apptId) is \(blobDownloads.count)")
for blobDownload in blobDownloads {
myBlobsToDownload.append(blobDownload)
}
myGroup.leave()
}
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
completion(myBlobsToDownload)
}
}