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

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]()
}
}

Related

Is there any way to implement a taskGroup to run in parrarel inside another taskGroup?

I am using Firebase to fetch some data from it with a Continuation. Before resuming the continuation I want to run a group of async tasks inside(fetch other data). This would let me achieve maximum concurrency.
I have tried two methods to do this, both does not work.
This is what I have tried first trying to use inside a continuation a taskGroup.(Code Below). The error is in comment.
First Option
try? await withThrowingTaskGroup(of: Void.self) { group in
for referencePosts in referencePostsDict.keys {
group.addTask {
return try! await self.fetchPost(reference: referencePosts)
}
}
func fetchPost(reference: String) async throws -> Void{
var db_ref = Database.database(url:FIREBASEURL.url)
.reference()
.child("posts")
.child(reference)
typealias postContinuation = CheckedContinuation<Void,Error>
return try await withCheckedThrowingContinuation{
(continuation : postContinuation) in
db_ref.observeSingleEvent(of: .value) { data in
if data.exists(){
var three = data.value as! [String : Any]
withThrowingTaskGroup(of: Void.self) { group in //async' call in a function that does not support concurrency
(three["picturePaths"] as! [String : String]).forEach { key, value in
if key != "O"{
group.addTask {
try? await self.fetchPictureDataL(picRef: key)
}
self.threePictures[key] = value
}
}
}
self.threePosts[reference] = three
continuation.resume(returning: ())
}else{
continuation.resume(returning: ())
}
}
}
}
func fetchPictureDataL(picRef : String) async throws ->Void{
var db =Database
.database(url:FIREBASEURL.url)
.reference()
.child("pictures").child(picRef)
typealias postContinuation = CheckedContinuation<Void,Error>
return try await withCheckedThrowingContinuation{
(continuation : postContinuation) in
db.observeSingleEvent(of: .value) { data in
self.threePicturesFetched[picRef] = three as! [String : Any]
continuation.resume(returning: ())
}
}
}
I commented the code where the compiler reports a problem.
Second Option
fetchCheckIn2 method is modified to return a [String]
This is the second method where I have tried to achieve the same result different, to mention the fetchCheckIn2 method is modified to return a [String].
What I want to achieve is that the fetchPictureDataL will run in parallel inside a Task or group, and will not run awaiting each other to finish.
//fetching all references
try! await withThrowingTaskGroup(of: [String].self) { firstGroup in
for referenceCheckIn in reference_checkInInt_dictionary.keys {
firstGroup.addTask {
return try await self.fetchCheckIn2(reference: referenceCheckIn)
}
}
for try await pictureArray in firstGroup {
if pictureArray.count != 0 {
for pic in pictureArray {
try! await self.fetchPictureDataL(picRef: pic)
}
}
}
}
Trying to achieve
I want to achieve parallelism even with fetchPictureDataL method?
If not clear why I am trying to do this, please read the use of case.
Use of this case:
I have social media app with posts. I have a list of paths in order to fetch the post. Each post contains an array of other paths which are pictures.
I want to fetch posts parrarel and also fetch the pictures in parrarel, in a taskGruop, so all this fetch of post+picture is awaited and can be displayed to User.
Thank you

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) }
}

Why converting a Firestore querySnapshot into custom objects with compactMap returns empty although the querySnapshot contains documents?

Screenshot of a Firestore Document
I am using Swift, Xcode and a Firestore database.
I created a TableView and a Custom Object Class (MediumSample) with a dictionary and want to load my Firestore documents and show them in the TableView.
The documents (example in the screenshot) are loaded from Firestore correctly but the conversion into the object did not work. The list of objects returned from compactMap is always empty.
Here is my code snippets. It would be great, if someone has a hint on what is going wrong.
Custom Object Class (simplified):
import Foundation
import FirebaseFirestore
protocol MediumSampleDocumentSerializable {
init?(dictionary: [String:Any])
}
struct MediumSample {
var id: String
var field_t: String
var field_i: Int64
var field_b1: Bool
var field_b2: Bool
var field_g: FirebaseFirestore.GeoPoint
var field_d: Date
var field_a: [String]
var usecase: String
var dictionary: [String:Any] {
return [
"id": id,
"field_t": field_t,
"field_i": field_i,
"field_b1": field_b1,
"field_b2": field_b2,
"field_g": field_g,
"field_d": field_d,
"field_a": field_a,
"usecase": usecase
]
}
}
extension MediumSample: MediumSampleDocumentSerializable {
init?(dictionary: [String:Any]) {
guard let id = dictionary ["id"] as? String,
let field_t = dictionary ["field_t"] as? String,
let field_i = dictionary ["field_i"] as? Int64,
let field_b1 = dictionary ["field_b1"] as? Bool,
let field_b2 = dictionary ["field_b2"] as? Bool,
let field_g = dictionary ["field_g"] as? FirebaseFirestore.GeoPoint,
let field_d = dictionary ["field_d"] as? Date,
let field_a = dictionary ["field_a"] as? [String],
let usecase = dictionary ["usecase"] as? String else {return nil}
self.init (id: id, field_t: field_t, field_i: field_i, field_b1: field_b1, field_b2: field_b2, field_g: field_g, field_d: field_d, field_a: field_a, usecase: usecase)
}
}
Declaration of the database and array and calling the loading function:
import UIKit
import FirebaseFirestore
class MediumTableViewController: UITableViewController {
//MARK: Properties
var db: Firestore!
var mediumsamples = [MediumSample]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
loadMediumSamples()
}
Function for loading the Firestore documents to fill the Array:
private func loadMediumSamples() {
//run the Firestore query
db.collection(Constants.MEDIUMS).whereField("usecase", isEqualTo: Constants.USECASE)
.getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
//initialise an array of medium objects with Firestore data snapshots
self.mediumsamples = querySnapshot!.documents.compactMap({MediumSample(dictionary: $0.data())})
//fill the tableView
DispatchQueue.main.async {
self.tableView.reloadData()
print(self.mediumsamples)
}
print("Mediums List", self.mediumsamples) // This line returns: Mediums List []
print("Mediums List size", (self.mediumsamples.count)) // This line returns: Mediums List size 0
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())") // This line returns the snapshot documents correctly!
}
}
}
}
Here is how the screenshot object object is added:
func addMediumSamples() {
let currentDateTime = Date()
let location = FirebaseFirestore.GeoPoint(latitude: 0, longitude: 0)
let mediumsample = MediumSample(id: "an id", field_t: "field_t", field_i: 10, field_b1: true, field_b2: false, field_g: location, field_d: currentDateTime, field_a: [Constants.SAMPLE_DEV], usecase: Constants.SAMPLE_DEV)
var ref: DocumentReference? = nil
ref = self.db.collection(Constants.MEDIUMS).addDocument(data: mediumsample.dictionary) {
error in
if let error = error {
print("Error writing city to Firestore: \(error)")
} else {
print("Document added with id : \(ref!.documentID)")
}
}
}
The problem is in the MediumSample struct, in the field_d type (Date).
The type of that field in your Cloud Firestore database is Timestamp.
The field "field_d" in the MediumSample struct expects a value of type Date.
You can change the type to the FirebaseFirestore.Timestamp, or you can convert it to Date when mapping and before passing to the MediumSample.
eg. for converting Timestamp to Date in Swift
let date = timestamp.dateValue()

Swift Vapor add additional info to custom response

I've two Models, Trip and Location. I would return a custom response with some field of trip and the number of Location that has the tripID equal to id of Trip. There is my code(not working). The field locationCount is always empty.
func getList(_ request: Request)throws -> Future<Response> {
let deviceIdReq = request.parameters.values[0].value
let queryTrips = Trip.query(on: request).filter(\.deviceId == deviceIdReq).all()
var tripsR = [TripCustomContent]()
var trips = [Trip]()
return queryTrips.flatMap { (result) -> (Future<Response>) in
trips = result
var count = 0
for t in trips {
let tripIdString = String(t.id!)
let v = Location.query(on: request).filter(\.tripID == tripIdString).count().map({ (res) -> Int in
return res
})/*.map{ (result) -> (Int) in
count = result
return result
}*/
let tripCustomContent = TripCustomContent.init(startTimestamp: t.startTimestamp, endTimestamp: t.endTimestamp, deviceId: t.deviceId, locationCount: v)
tripsR.append(tripCustomContent)
}
let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(tripsR)
let response = HTTPResponse.init(status: .ok, version: HTTPVersion.init(major: x, minor: y), headers: HTTPHeaders.init(), body: data)
let finalResponse = Response.init(http: response, using: request)
return try g.encode(for: request)
}
}
and this is my custom content struct:
struct TripCustomContent: Encodable {
var startTimestamp: String?
var endTimestamp: String?
var deviceId: String
var locationCount: Future<Int>
}
any suggestions?
You're trying to use a value which isn't available yet. When you're returning a Future, you aren't returning the value inside it.
So you want your TripCustomContent to be like this (use in vapor Content instead of Codable:
struct TripCustomContent: Content {
var startTimestamp: String?
var endTimestamp: String?
var deviceId: String
var locationCount: Int
}
You queried the Trip correctly, but not the Location. You could maybe try something like this:
return queryTrips.flatMap { trips -> Future<[TripCustomContent]> in
let tripIds = trips.map({ String($0.id!) })
return Location.query(on: request).filter(\.tripID ~~ tripIds).all().map { locations in
return trips.map { trip in
let locationCount = locations.filter({ $0.tripId == String(trip.id!) }).count
return TripCustomContent(... locationCount: locationCount)
}
}
}
What did I do here?
Map the trips to their tripIds to get an array of tripIds
Get all locations with a tripId of one of the tripIds in the above array
Map each of the trips to an instance of TripCustomContent, using the locations of the database filtered by tripId
Finally, you don't need to encode the JSON yourself, just return objects conforming Content:
func getList(_ request: Request) throws -> Future<[TripCustomContent]>
The above could be a solution to your strategy. But maybe you take a look at relations if they can be a more efficient, easier and faster way.