Decoding nested object with Swift and Firestore - swift

I’m developing an app with Swift 5 and Firestore.
I created structs Identifiables and Codables and it works well but I have problem decoding DocumentID for nested documents.
I wasn’t able to find something similar online.
I would like to know if Firestore structure I implemented is right in my use case and how to solve id problem.
I have to show a list of activities with just some info about the User and the Sport (like name and image). That's why I'm trying to nest in the Activity object just the info I need to not query the entire User and the Sport for each activity of the list.
On Firestore:
The activity document contains:
info (String)
organizer:
- name (String)
- image (String)
- ref (Reference)
date (Date)
sport:
- name (String)
- image (String)
- ref (Reference)
Here is my code:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct User: Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let email: String?
let image: String
let birthday: Date?
let description: String?
}
struct Sport: Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let image: String
}
struct Activity: Codable {
#DocumentID var id: String? = UUID().uuidString
let organizer: User
let info: String
let date: Date
let sport: Sport
static func getActivities(completion: #escaping (_ activities: [Activity]?) -> Void) {
Activity.collectionReference.getDocuments { (querySnapshot, error) in
guard error == nil else {
print("Error: \(error?.localizedDescription ?? "Generic error")")
return
}
let activities = querySnapshot?.documents.compactMap({ (queryDocumentSnapshot) -> Activity? in
return try! queryDocumentSnapshot.data(as: Activity.self)
})
completion(activities)
}
}
}
Calling Activity.getActivities I'm able to get everything I need but decoding the user and sport directly from activity object they get the activity ID instead of their own. That's wrong because in this way I have the wrong reference to the object. The idea is to keep the reference to use it in the details page and fetching the complete object.
Can someone told me what's the best way to structure this kind of architecture?
Thank you in advance.

Related

Straightforward way to add timestamp to Firebase in Swift?

so I've been researching a lot and apparently no method is working to add timestamp to my firebase data and then sort the data accordingly. I tried the traditional "timestamp": [".sv":"timestamp"] method and that only adds a value of .sv: Timestamp in firebase. Then I tried self.createdAt = Date(timeIntervalSince1970: timestamp/1000) and added that to where I add the item to firebase but that didn't help either.So doesn't anyone know what's the most straightforward method to add timestamp to firebase via swift? Would appreciate any input! Thanks.
EDIT:
Here's now I define the item being added to firebase:
struct Note: Identifiable, Codable {
var id: String
var content: String
var createdAt: String
}
Here's how I define fetching that item from firebase:
func fetchNotes () {
notes.removeAll()
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
let ref = db.collection("userslist").document(uid).collection("actualnotes")
ref.order(by: "createdAt", descending: true).addSnapshotListener{ (querySnapshot, error) in
let documents = querySnapshot!.documents
if querySnapshot!.isEmpty {
let ref = db.collection("userslist").document(uid).collection("actualnotes")
ref.document().setData(["content": "Welcome", "createdAt": FieldValue.serverTimestamp() ])
}
else {
self.notes = documents.map { (queryDocumentSnapshot) -> Note in
let data = queryDocumentSnapshot.data()
let id = queryDocumentSnapshot.documentID
let content = data["content"] as! String ?? ""
let createdAt = data["createdAt"] as? String ?? "" // this is where
// it breaks with the error (Thread 1: signal SIGABRT)
let note = Note(id:id, content:content, createdAt: createdAt)
return (note)
}
}
}
}
And here's where I want the timestamp to appear in another view:
List{ ForEach(dataManager.notes) { note in
NavigationLink(destination: NoteView(newNote: note.content, idd: note.id ))
{
HStack {
Text(note.createdAt)
}})}}
The [".sv":"timestamp"] applies to the Realtime Database only, while you are using Cloud Firestore. While both databases are part of Firebase, they are completely separate and the API of one does not apply to the other.
For Firestore follow its documentation on writing a server-side timestamp, which says it should be:
db.collection("objects").document("some-id").updateData([
"lastUpdated": FieldValue.serverTimestamp(),
])
If that doesn't work, edit your question to show the exact error you get.

Fetch several ParseObjects with objectId

I am updating an iOS app using ParseSwift. In general, every ParseUser of the app has several associated Userdata ParseObjects that need to be queried or fetched.
The current implementation uses individual queries to find every individual Userdata object one after the other. This works but is obviously not optimal.
What I already reworked is that every ParseUser now saves an Array of objectId's of the user-associated Userdata ParseObjects.
What I am trying to achieve now is to query for all Userdata objects that correspond to these saved objectId's in the ParseUser's userdataIds field.
This is my code:
func asyncAllUserdataFromServer() {
let userdataIdArray = User.current!.userdataIds
var objectsToBeFetched = [Userdata]()
userdataIdArray?.forEach({ objectId in
let stringObjectId = objectId as String
// create a dummy Userdata ParseObject with stringObjectId
let object = Userdata(objectId: stringObjectId)
// add that object to array of Userdata objects that need to be fetched
objectsToBeFetched.append(object)
})
// fetch all objects in one server request
objectsToBeFetched.fetchAll { result in
switch result {
case .success(let fetchedData):
// --- at this point, fetchedData is a ParseError ---
// --- here I want to loop over all fetched Userdata objects ---
case .failure(let error):
...
}
}
}
I read in the ParseSwift documentation that several objects can be fetched at the same time based on their objectIds, so my idea was to create an Array of dummy Userdata objects with the objectIds to fetch. This, however, does not work.
As indicated in the code block, I was able to narrow the error down, and while the query is executed, fetchedData comes back as a ParseError saying:
Error fetching individual object: ParseError code=101 error=objectId "y55wmfYTtT" was not found in className "Userdata"
This Userdata object with that objectId is the first (and only) objectId saved in the User.current.userdataIds array. This object does exist on my server, I know that for sure.
Any ideas why it cannot be fetched?
General infos:
Xcode 13.2.1
Swift 5
ParseSwift 2.5.0
Back4App backend
Edit:
User implementation:
struct User: ParseUser {
//: These are required by `ParseObject`.
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
//: These are required by `ParseUser`.
var username: String?
var email: String?
var emailVerified: Bool?
var password: String?
var authData: [String: [String: String]?]?
//: Custom keys.
var userdataIds: [String]? // --> array of Userdata objectId's
}
Userdata implementation:
struct Userdata: ParseObject, ParseObjectMutable {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var ACL: ParseACL?
var firstname: String?
var lastSeen: Int64?
var isLoggedIn: Bool?
var profilepicture: ParseFile?
}
You should check your Class Level Permissions (CLP's) and here in Parse Dashboard, if you are using the default configuration, you can’t query the _User class unless you make the CLP’s public or authorized. In addition, you can probably shrink your code by using a query and finding the objects instead of building your user objects and fetching the way you are doing:
let userdataIdArray = User.current!.userdataIds
let query = Userdata.query(containedIn(key: "objectId", array: userdataIdArray))
//Completion block
query.find { result in
switch result {
case .success(let usersFound):
...
case .failure(let error):
...
}
}
// Async/await
do {
let usersFound = try await query.find()
...
} catch {
...
}
You should also look into your server settings to make sure this option is doing what you want it to do: https://github.com/parse-community/parse-server/blob/4c29d4d23b67e4abaf25803fe71cae47ce1b5957/src/Options/docs.js#L31
Are you sure that the Object Ids for UserData are the same as the User ids? while you might have user id in the class, it is probably not the objectId, but some other stored field.
doing
let query = PFQuery(className:"UserData")
query.whereKey("userId", containedIn:userdataIdArray)
query.findObjectsInBackground { (objects: [PFObject]?, error: Error?) in
// do stuff
}
May help.

Error: "Expected to decode Dictionary<String, Any> but found an array instead." — but I haven't defined a dictionary? [duplicate]

This question already has answers here:
Decoding Error -- Expected to decode Dictionary<String, Any> but found an array instead
(2 answers)
Closed 1 year ago.
I'm working on a creative project, and I'm trying to decode content from an API database using Swift's JSONDecoder() function. I've built my structs, a getData() function, and I've set up a do-try-catch for the JSONDecoder() function. I'm having difficulty understanding what I'm doing to get the error I'm getting.
Here are my structs:
struct Response: Codable {
let foundRecipes: [Recipe]
let foundIngredients: [Ingredient]
}
struct Recipe: Codable {
let id: Int
let title: String
let image: String
let imageType: String
let usedIngredientCount: Int
let missedIngredientCount: Int
let missedIngredients: [Ingredient]
let usedIngredients: [Ingredient]
let unusedIngredients: [Ingredient]
let likes: Int
}
struct Ingredient: Codable {
let id: Int
let amount: Int
let unit: String
let unitLong: String
let unitShort: String
let aisle: String
let name: String
let original: String
let originalString: String
let origianalName: String
let metaInformation: [String]
let meta: [String]
let image: String
}
Here's my getData() function:
func getData(from url: String) {
URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong.")
return
}
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
}
catch {
print("")
print(String(describing: error)) // Right here is where the error hits.
}
guard let json = result else {
return
}
print(json.foundRecipes)
}).resume()
}
Here's a link to the API's documentation. The URL I'm calling in getData() links to the same structure of search as shown in their example: https://spoonacular.com/food-api/docs#Search-Recipes-by-Ingredients — and here's a screenshot of the url results for the exact search I'm working on: https://imgur.com/a/K3Rn9SZ
And finally, here's the full error that I'm catching:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
My understanding of this error is that it's saying I told the JSONDecoder() to look for a Dictionary of <String, Any>, but it's at the link and only seeing an array. I'm confused, because I don't know where it thinks I'm providing a dictionary. Where am I screwing up? Not looking for specific code changes, just some guidance on what I'm missing.
Thanks in advance :)
As you can see in your image of the API data and in the API documentation you linked to, the API is returning an array (in the documentation, for example, you can see that it is surrounded by [...]). In fact, it looks like the API returns an array of Recipe.
So, you can change your decoding call to this:
var result: [Recipe]?
do {
result = try JSONDecoder().decode([Recipe].self, from: data)
print(result)
} catch {
print(error)
}
Perhaps your idea for Response came from somewhere else, but the keys foundRecipes or foundIngredients don't show up in this particular API call.
Also, thanks to #workingdog's for a useful comment about changing amount to a Double instead of an Int in your model.

Firebase Firestore: Encoder from custom structs not working

So I created a function in which I try to create a document in my Firestore in which user data is stored. But when upgrading my project to the Xcode 13.0 beta, the Firebase encoder has stopped working. Anyone else experiencing a similar problem?
My model looks like this:
struct User: Identifiable, Codable {
#DocumentID var id: String?
var auth_id: String?
var username: String
var email: String
enum CodingKeys: String, CodingKey {
case auth_id
case username
case email
}
}
And the call to Firebase like this:
let newUser = User(auth_id: idToken, username: name, email: email)
try await databasemodel.database.collection("users").document(idToken).setData(newUser)
The Document is created with this code doesn't exist yet, but that worked earlier.
Now when I compile I get the error: "Cannot convert value of type 'User' to expected argument type '[String : Any]'"
No other errors are displayed, and I'm pretty sure the rest of the code works as expected.
So I ran into a similar issue with Codables... I've made this little extension that's saved me. Maybe it works for you too :)
extension Encodable {
/// Convenience function for object which adheres to Codable to compile the JSON
func compile () -> [String:Any] {
guard let data = try? JSONEncoder().encode(self) else {
print("Couldn't encode the given object")
preconditionFailure("Couldn't encode the given object")
}
return (try? JSONSerialization
.jsonObject(with: data, options: .allowFragments))
.flatMap { $0 as? [String: Any] }!
}
}
You can then do
try await databasemodel.database.collection("users").document(idToken).setData(newUser.compile())
Note. I didn't test this. I'm simply providing the extension which solved my problem when faced with the same thing.
Just use Firestore.Encoder
extension Encodable {
var dictionary: [String: Any]? {
return try? Firestore.Encoder().encode(self)
}
}

Decoding raw data from URLSession the right way in Swift [duplicate]

This question already has an answer here:
iOS Generic type for codable property in Swift
(1 answer)
Closed 2 years ago.
The response for most of the endpoints in my API is something like this -
{
"status":"success",
"data":[
{
"id":"1",
"employee_name":"Tiger Nixon",
"employee_salary":"320800",
"employee_age":"61",
"profile_image":""
},
{
"id":"2",
"employee_name":"Garrett Winters",
"employee_salary":"170750",
"employee_age":"63",
"profile_image":""
}
]
}
And this is what my Employee model looks like
struct Employee: Codable {
let id, employeeName, employeeSalary, employeeAge: String
let profileImage: String?
enum CodingKeys: String, CodingKey {
case id
case employeeName = "employee_name"
case employeeSalary = "employee_salary"
case employeeAge = "employee_age"
case profileImage = "profile_image"
}
}
typealias Employees = [Employee]
I just want to extract the data part of the API response using JSONDecoder and pass it to my completion handler
completionHandler(try? JSONDecoder().decode(Employees.self, from: data), response, nil)
I was able to get around by creating a struct Employees like this -
struct Employees: Codable {
let status: String
let data: [Employee]
}
But this is just a workaround and I would have to do it for every almost every model. So is there a better and less redundant way of extracting data from the response?
What I would do if I were you is just create a template-based wrapper and use it for all of your model objects.
struct ModelWrapper<T: Codable>: Codable {
let status: String
let data: [T]
}
let decoder = JSONDecoder()
let wrapper = try decoder.decode(ModelWrapper<Employee>.self, from: json)