ich work with swift and firestore and try to implemented a server similar to this:
chatId: String
eventCreatorId: String
matchedUserId: String
eventId: String
messages: [
{userId: String, timestamp:timestamp, messageText: String},
{userId: String, timestamp:timestamp, messageText: String},
],
in other Words with a MVVM design i want to upload a array of Models
but i getting a type error when i try with this
struct ChatModel: Codable {
var chatId: String
var eventCreatorId: String
var matchedUserId: String
var eventId: String
var messages: [MessageModel]
}
struct MessageModel: Codable {
var userId: String
var timeStamp: Timestamp
var messageText: String
}
the error happens if i try to upload
func uploadMessage(messageText: String, chatId: String) -> Promise<Void> {
return Promise { seal in
guard let currentUser = Auth.auth().currentUser else {
return
}
let timeStamp: Timestamp = Timestamp(date: Date())
let messageModel = MessageModel(userId: currentUser.uid,timestamp: timeStamp, messageText: messageText)
print(messageModel)
let _ = db.collection("chats")
.document(chatId)
.updateData(["messages" : FieldValue.arrayUnion([messageModel])]) { error in
if let error = error {
seal.reject(error)
} else {
seal.fulfill(())
}
}
}
}
}
i tried also without the timestamp but ran into the same error
can someone explain me what am i doing wrong?
On ArrayUnion, Firestore does not understand the type you're trying to pass.
Converting the struct to a dictionary with type [String: Any] will ommit this error -
struct MessageModel: Codable {
var userId: String
var timeStamp: Timestamp
var messageText: String
var dictionary: [String: Any] {
return["userId": userId,
"timeStamp": timeStamp,
"messageText": messageText]
}
}
Then when you're uploading to Firestore you use the Dict value:
.updateData(["messages" : FieldValue.arrayUnion([messageModel.dictionary])]) { error in
Related
I am trying to add to an array of maps in Firestore and getting the following error:
Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'Unsupported type: __SwiftValue'
This is my Model Class:
struct StringModel: Identifiable, Codable, Equatable, Hashable {
var id = UUID()
var cross: Int
var mains: Int
var name: String
var date: Date
}
struct UserDataModel: Identifiable, Codable {
#DocumentID public var id: String?
var uid: String
var strings: [StringModel]?
}
Am using the following function to update the value in firestore, however, it doesnt seem to work and throws the error:
#Published var newString = StringModel(cross: 50, date: Timestamp(), mains: 50, name: "")
func addString() {
db.document(uidStr).updateData(["strings" : FieldValue.arrayUnion([newString])]) { err in
if let err = err{
print("\(err)")
}
}
}
Firestore Datatbase
Any ideas how I can go about this? Thanks!
You can't append a Swift object to a Firestore array. You must convert the Swift object into a [String: Any] object. You can add a computed property to the model that outputs this for you.
struct StringModel: Identifiable, Codable, Equatable, Hashable {
var id = UUID()
var cross: Int
var mains: Int
var name: String
var date: Date
var firestoreData: [String: Any] {
return [
"id": id,
"cross": cross,
"mains": mains,
"name": name,
"date": date
]
}
}
func addString() {
db.document(uidStr).updateData(["strings" : FieldValue.arrayUnion([newString.firestoreData])]) { err in
if let err = err{
print("\(err)")
}
}
}
First of all, i am very sorry for the noob question, but i just cant seem to figure this out.
I am very new to coding and just started to get my feet wet with SwiftUI, following a few courses and started to dabble in trying to create some basic apps.
I am currently working on an app that does an API call and displays the data.
My issue is, im trying to put the decoded data into an array, it sounds so simple and I think i am missing something very easy, but for the life of me I cant seem to figure it out.
Below is the codable struct I have
struct Drinks: Codable, Identifiable {
let id = UUID()
let strDrink : String
let strInstructions: String
let strDrinkThumb: String?
let strIngredient1: String?
let strIngredient2: String?
let strIngredient3: String?
let strIngredient4: String?
let strIngredient5: String?
}
I want to put the ingredients into an Array so I can go through them in lists etc
import SwiftUI
struct IngredientView: View {
let drink : Drinks
let ingredientArray : [String] = [] // I want to append the ingredients here
var body: some View {
GroupBox() {
DisclosureGroup("Drink Ingredience") {
ForEach(0..<3) { item in
Divider().padding(.vertical, 2)
HStack {
Group {
// To use the array here
}
.font(Font.system(.body).bold())
Spacer(minLength: 25)
}
}
}
}
}
}
Again, sorry for the noob question that probably has a simple answer, but worth a shot asking :D
Thanks!
you could use this approach to get all your ingredients into an array and use
it in Lists. The idea is to use a function to gather all your ingredients into an array of
Ingredient objects. You could also use a computed property.
It is best to use a Ingredient object and declare it Identifiable
so that when you use them in list and ForEach, each one will be
unique, even if the names are the same.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var drinkList = [Drink]()
var body: some View {
List {
ForEach(drinkList) { drink in
VStack {
Text(drink.strDrink).foregroundColor(.blue)
Text(drink.strInstructions)
ForEach(drink.allIngredients()) { ingr in
HStack {
Text(ingr.name).foregroundColor(.red)
Text(ingr.amount).foregroundColor(.black)
}
}
}
}
}
.task {
let theResponse: ApiResponse? = await getData(from: "https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita")
if let response = theResponse {
drinkList = response.drinks
}
}
}
func getData<T: Decodable>(from urlString: String) async -> T? {
guard let url = URL(string: urlString) else {
print(URLError(.badURL))
return nil // <-- todo, deal with errors
}
do {
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
print(URLError(.badServerResponse))
return nil // <-- todo, deal with errors
}
return try JSONDecoder().decode(T.self, from: data)
}
catch {
print("---> error: \(error)")
return nil // <-- todo, deal with errors
}
}
}
struct ApiResponse: Decodable {
var drinks: [Drink]
}
struct Drink: Decodable, Identifiable {
let id = UUID()
let idDrink: String
let strDrink: String
let strDrinkThumb: String
let strAlcoholic: String
let strGlass: String
let strInstructions: String
let strIngredient1: String?
let strIngredient2: String?
let strIngredient3: String?
let strIngredient4: String?
let strIngredient5: String?
let strIngredient6: String?
let strIngredient7: String?
let strIngredient8: String?
let strIngredient9: String?
let strIngredient10: String?
var strMeasure1: String?
var strMeasure2: String?
var strMeasure3: String?
var strMeasure4: String?
var strMeasure5: String?
var strMeasure6: String?
var strMeasure7: String?
var strMeasure8: String?
var strMeasure9: String?
var strMeasure10: String?
// --- here adjust to your needs, could also use a computed property
func allIngredients() -> [Ingredient] {
return [
Ingredient(name: strIngredient1 ?? "", amount: strMeasure1 ?? ""),
Ingredient(name: strIngredient2 ?? "", amount: strMeasure2 ?? ""),
Ingredient(name: strIngredient3 ?? "", amount: strMeasure3 ?? ""),
Ingredient(name: strIngredient4 ?? "", amount: strMeasure4 ?? ""),
Ingredient(name: strIngredient5 ?? "", amount: strMeasure5 ?? ""),
Ingredient(name: strIngredient6 ?? "", amount: strMeasure6 ?? ""),
Ingredient(name: strIngredient7 ?? "", amount: strMeasure7 ?? ""),
Ingredient(name: strIngredient8 ?? "", amount: strMeasure8 ?? ""),
Ingredient(name: strIngredient9 ?? "", amount: strMeasure9 ?? ""),
Ingredient(name: strIngredient10 ?? "", amount: strMeasure10 ?? "")
].filter{!$0.name.isEmpty}
}
}
struct Ingredient: Identifiable {
let id = UUID()
var name: String
var amount: String
}
change you struct to
struct Drink: Codable, Identifiable {
let id = UUID()
let strDrink : String
let strInstructions: String
let strDrinkThumb: String?
let strIngredients: [String] = []
}
wherever you want to loop through ingredients you can use drink.strIngredients array
First of all your design cannot work because you are ignoring the root object, a struct with a key drinks
struct Root : Decodable {
let drinks : [Drink]
}
A possible solution is to write a custom init method. However this a bit tricky because you have to inject dynamic CodingKeys to be able to decode the ingredients in a loop
First create a custom CodingKey struct
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil } // will never be called
}
In the Drink struct – by the way it's supposed to be named in singular form – specify a second container, create the ingredient keys on the fly, decode the ingredients in a loop and append the results to an array until the value is nil. Somebody should tell the owners of the service that their JSON structure is very amateurish. As you have to specify the CodingKeys anyway I mapped the keys to more meaningful and less redundant struct member names.
struct Drink: Decodable, Identifiable {
private enum CodingKeys : String, CodingKey {
case name = "strDrink", instructions = "strInstructions", thumbnail = "strDrinkThumb"
}
let id = UUID()
let name: String
let instructions: String
let thumbnail: URL
let ingredients: [String]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.instructions = try container.decode(String.self, forKey: .instructions)
self.thumbnail = try container.decode(URL.self, forKey: .thumbnail)
var counter = 1
var temp = [String]()
let anyContainer = try decoder.container(keyedBy: AnyKey.self)
while true {
let ingredientKey = AnyKey(stringValue: "strIngredient\(counter)")!
guard let ingredient = try anyContainer.decodeIfPresent(String.self, forKey: ingredientKey) else { break }
temp.append(ingredient)
counter += 1
}
ingredients = temp
}
}
In the view display the ingredients like this
struct IngredientView: View {
let drink : Drink
var body: some View {
GroupBox() {
DisclosureGroup("Drink Ingredience") {
ForEach(drink.ingredients) { ingredient in
Divider().padding(.vertical, 2)
HStack {
Group {
Text(ingredient)
}
.font(Font.system(.body).bold())
Spacer(minLength: 25)
}
}
}
}
}
}
I'm trying to pass an array of objects inside another object
struct CreationData: Encodable {
enum CodingKeys: String, CodingKey {
case property1
case amount
case participants
case code
}
var property1: String?
var amount: Double?
var tipAmount: Double?
var participants: [Participant]
var currency: Currency
init() {
name = nil
property1 = nil
participants = []
tipAmount = nil
currency = .hryvna
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let name = name {
try container.encode(name, forKey: .name)
}
if let property1 = property1 {
try container.encode(property1, forKey: .property1)
}
if let tipAmount = tipAmount {
try container.encode(tipAmount, forKey: .tipAmount)
}
if !participants.isEmpty {
try container.encode(participants, forKey: .participants)
}
try container.encode(currency, forKey: .currencyCode)
}
}
where object in array is
struct Participant: Hashable, Encodable {
enum CodingKeys: String, CodingKey {
case amount
case id
}
var image: Data?
var displayName: String
var amount: Double? = 100
var id: Int?
var name: String?
var isSelected: Bool = false
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(amount, forKey: .amount)
try container.encode(id, forKey: .id)
}
}
so my JSON looks something like this
{
"property1" : 123,
"code" : "zen",
"name" : "No name",
"participants" : [
{
"amount" : 100,
"id" : 1
}
]
}
On the backend I use this handler
func createHandler(_ req: Request, splitCreate: Create) throws -> Future<SplitResponse> {
let logger = try req.make(Logger.self)
let user = try req.requireAuthenticated(User.self)
guard let id = user.id else {
throw Abort(.badRequest, reason: "Couldn't get an id for current user")
}
...
with this request content object
struct Create: Content {
struct Participant: Content {
var id: Int?
var amount: Double?
}
var amount: Double?
var tipAmount: Double?
var name: String?
var participants: [Participant]
var currencyCode: String
}
I create pivots by the participants array, and the problem is that instead of getting a single participant from an array like so
[App.Create.Participant(id: Optional(1), amount: Optional(100.0))]
on the backend from the specified JSON I get these participants
[App.Create.Participant(id: Optional(1), amount: nil), App.Create.Participant(id: Optional(1), amount: Optional(100.0))]
Seems like my array json is being decoded incorrectly, but I'm unable to find a way to fix it
For some reason adding Content-Type header to URLSessionConfiguration.additionalHeaders didn't change the encoding to JSON.
So I had to set Alamofire's request encoding to JSONEncoding.default explicitly.
I am trying to access a particular an embedded array of dictionaries to create my swift objects. I am unsure about how to access that array in the JSON dictionary.
Here is the definition of my Swift object = StarWarsPeople
class StarWarsPeople: Decodable {
var name: String?
var height: String?
var weight: String?
var hair_color: String?
var skin_color: String?
var eye_color: String?
var birth_year: String?
var gender: String?
}
Here is my APIClient class:
class StarWarsPeopleAPIClient
{
class func getStarWarsPeopleInformation (page: Int, completion:#escaping ([StarWarsPeople])-> ()) throws {
let starWarsPeopleURL = "https://swapi.co/api/people/?page=\(page)"
let convertedStarWarsPeopleURL = URL(string: starWarsPeopleURL)
guard let unwrappedConvertedStarWarsPeopleURL = convertedStarWarsPeopleURL else { print("unwrappedConvertedStarWarsPeopleURL did not unwrap"); return}
let request = URLRequest(url: unwrappedConvertedStarWarsPeopleURL)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let unwrappedData = data else { print("unwrappedData did not unwrap"); return}
do {
let starWarsPeopleDataArray = try JSONDecoder().decode([StarWarsPeople].self, from: unwrappedData)
completion(starWarsPeopleDataArray)
}
catch let error {
print("Error occured here: \(error.localizedDescription)")
}
}
task.resume()
}
}
Here is my Json, it is the results array that I would like to access, it is an array of dictionaries, which I need to iterate over to create my StarWarsPeople Object.
{
"count": 87,
"next": "url",
"previous": null,
"results": [
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "url",
"films": [
"url",
"url",
"url",
"url",
"url"
],
"species": [
"url"
],
"vehicles": [
"url",
"url"
Please read the JSON. You are ignoring the enclosing object
struct Root: Decodable {
let count: Int
let next: URL?
let previous: URL?
let results : [StarWarsPeople]
}
struct StarWarsPeople: Decodable {
private enum CodingKeys: String, CodingKey {
case name, height, mass
case hairColor = "hair_color", skinColor = "skin_color"
case eyeColor = "eye_color", birthYear = "birth_year", gender
}
let name: String
let height: String
let mass: String
let hairColor: String
let skinColor: String
let eyeColor: String
let birthYear: String
let gender: String
}
...
let root = try JSONDecoder().decode(Root.self, from: unwrappedData)
let starWarsPeopleDataArray = root.results
...
Notes:
A struct is sufficient.
Map the snake_cased keys to camelCased properties.
In almost all cases the properties can be declared as constants (let).
Don't declare all properties schematically as optional. Declare only those as optional which corresponding key can be missing or the value can be null.
Simply define a wrapper struct that holds the results property from the JSON response.
struct ApiResponse: Decodable {
results: [StarWarsPeople]
}
and later use
let apiResponse = try JSONDecoder().decode(ApiResponse.self, from: unwrappedData)
let starWarsPeopleDataArray = apiResponse.results
to parse it.
Results you are trying to fetch is actually present in results keys. Also we need to use properties same as parameter name( we can use CodingKeys enum too for overriding this).
So, first parse outer JSON , In new struct say StarWarsPeopleParent
class StarWarsPeopleParent: Decodable {
var count: Int?
var results: [StarWarsPeople]?
}
Update your StarWarsPeople struct's properties as:
class StarWarsPeople: Decodable {
var name: String?
var height: String?
var mass: String?
var hair_color: String?
var skin_color: String?
var eye_color: String?
var birth_year: String?
var gender: String?
}
Then parse it like:
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let unwrappedData = data else { print("unwrappedData did not unwrap"); return}
do {
let starWarsPeopleDataArray = try JSONDecoder().decode(StarWarsPeopleParent.self, from: unwrappedData)
completion(starWarsPeopleDataArray)
}
catch let error {
print("Error occured here: \(error.localizedDescription)")
}
}
Hope this is fine for you to understand.
I have a problem to make a JSON from an array of struct in Swift3. I searched in Stack Overflow, nothing help me (here the screenshot). I have a struct like this:
public struct ProductObject {
var prodID: String
var prodName: String
var prodPrice: String
var imageURL: String
var qty: Int
var stock: String
var weight: String
init(prodID: String, prodName: String, prodPrice: String, imageURL: String, qty: Int, stock: String, weight: String){
self.prodID = prodID
self.prodName = prodName
self.prodPrice = prodPrice
self.imageURL = imageURL
self.qty = qty
self.stock = stock
self.weight = weight
}
}
and the array of that struct:
private var productsArray = [ProductObject]()
When the array is not empty, and then I tried to print it in another class, it shows this in debugger:
[app.cartclass.ProductObject(prodID: "2", prodName: "produk 2", prodPrice: "IDR 1000000", imageURL: "someURL", qty: 1, stock: "11", weight: "200")]
The array is not a valid JSON object. How to make it a valid JSON object? And I wonder whether this part "app.cartclass.ProductObject" is a problem or not to make it a valid JSON object?
edit:
Here's how I serialize into a JSON:
var products = [String:Any]()
for j in 0 ..< cart.numberOfItemsInCart() {
products=["\(j)":cart.getAllProduct(atIndex: j)]
}
if let valid = JSONSerialization.isValidJSONObject(products) {
do {
let jsonproducts = try JSONSerialization.data(withJSONObject: products, options: .prettyPrinted) as! [String:Any]
//print(jsonproducts)
} catch let error as NSError {
print(error)
}
} else {
print("it is not a valid JSON object");
}
If you want to make JSON from custom object then first you need to convert your custom object to Dictionary, so make one function like below in your ProductObject struct.
func convertToDictionary() -> [String : Any] {
let dic: [String: Any] = ["prodID":self.prodID, "prodName":self.prodName, "prodPrice":self.prodPrice, "imageURL":self.imageURL, "qty":qty, "stock":stock, "weight":weight]
return dic
}
Now use this function to generate Array of dictionary from Array of custom object ProductObject.
private var productsArray = [ProductObject]()
let dicArray = productsArray.map { $0.convertToDictionary() }
Here dicArray is made of type [[String:Any]], now you can use JSONSerialization to generate JSON string from this dicArray.
if let data = try? JSONSerialization.data(withJSONObject: dicArray, options: .prettyPrinted) {
let str = String(bytes: data, encoding: .utf8)
print(str)
}