SWIFTUI Firebase Retrieving Subcollection Data - swift

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

Related

Subsequent ordered HTTP calls

I'm building a simple iOS client for HackerNews. I'm using their APIs, according to which I'll be able to get the ordered post IDs (sorted by new, best and top) and a single post item passing the ID to the request. The problem I'm facing is the following: how can I, once I get the IDs array, make an HTTP call for every post in an ordered fashion? With the way I currently implemented it, I'm not having any luck.
E.g. say the IDs array is [3001, 3002, 3003, 3004]. I tried calling the method to get those posts inside a for loop issuing dispatch groups and dispatch semaphores, but I still get them unordered, like the call for item 3003 completes before 3002, and so on.
The methods I'm using:
#Published var posts: [Post] = []
func getPosts(feedType: FeedType){
posts = []
self.getFeedIDs(feedType: feedType).subscribe{ ids in
let firstFifteen = ids[0...15]
let dGroup = DispatchGroup()
let dQueue = DispatchQueue(label: "network-queue")
let dSemaphore = DispatchSemaphore(value: 0)
dQueue.async {
for id in firstFifteen{
dGroup.enter()
self.getPost(id: id).subscribe{ post in
self.posts.append(post)
dSemaphore.signal()
dGroup.leave()
}
dSemaphore.wait()
}
}
}
}
func getFeedIDs(feedType: FeedType) -> Observable<[Int]> {
return self.execute(url: URL(string: "https://hacker-news.firebaseio.com/v0/\(feedType)stories.json")!)
}
func getPost(id: Int) -> Observable<Post>{
return self.execute(url: URL(string: "https://hacker-news.firebaseio.com/v0/item/\(id).json")!)
}
func execute <T: Decodable>(url: URL) -> Observable<T> {
return Observable.create { observer -> Disposable in
let task = URLSession.shared.dataTask(with: url) { res, _, _ in
guard let data = res, let decoded = try? JSONDecoder().decode(T.self, from: data) else {
return
}
observer.onNext(decoded)
observer.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
Any help would be greatly appreciated.
The semaphore makes no sense and is inefficient anyway.
Use the same pattern which Apple suggests in conjunction with TaskGroups: Collect the data in a dictionary and after being notified sort the data by the dictionary keys
func getPosts(feedType: FeedType){
var postData = [Int:Post]()
posts = []
self.getFeedIDs(feedType: feedType).subscribe{ ids in
let firstFifteen = ids[0...15]
let dGroup = DispatchGroup()
for (index, element) in firstFifteen.enumerated() {
dGroup.enter()
self.getPost(id: element).subscribe{ post in
postData[index] = post
dGroup.leave()
}
}
dGroup.notify(queue: .main) {
for key in postData.keys.sorted() {
posts.append(postData[key]!)
}
}
}
}

How to use Decodable on the results of a Firestore Query on each document

I have the following code, and I need to use Query so that I can programmatically build the query up before making the call to Firestore, but the document I get back apparently doesn't support Decodable. If I don't use Query, I cannot build up the where clauses programmatically however the documents I get back do support Decodable. How can I get the first case to allow Decodable to work?
public static func query<T: Codable>(queryFields: [String: Any]) async -> [T] {
let db = Firestore.firestore()
var ref: Query = db.collection("myDocuments")
for (key, value) in queryFields {
ref = ref.whereField(key, isEqualTo: value)
}
let snapshot = try? await ref.getDocuments()
if let snapshot = snapshot {
let results = snapshot.documents.compactMap { document in
try? document.data(as: T.self) // this does not compile
}
return results
} else {
return [T]()
}
}

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.

Firebase Firestore query not executing

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.