Unpacking NSArrays into String swift - json

I want to unpack an array of users to a single string.
For instance in my app there are group chats and each time a user enters a group chat their uid is added to a list of members. I want to return all members in a single group chat as a string in app.
Each room is represented in code as the following data object
class Room: NSObject {
var owner: String?
var groupChatName: String?
var groupChatDescription: String?
var members: [NSArray]?
}
Below is where I retrieve each room from the database.
func fetchAllRooms(){
Database.database().reference().child("rooms").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let room = Room()
room.setValuesForKeys(dictionary)
self.rooms.append(room)
print(snapshot)
print(room.members?.joined())
print(self.rooms.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
print("end of snap")
}, withCancel: nil)
}
print(room.members?.joined()) Is what currently crashes the app and throws 'NSInvalidArgumentException', reason:
'-[__NSDictionaryI objectAtIndex:]: unrecognized selector sent to instance 0x60800146ab80'
I realize it could possibly be how my data is structured so here's a look at the json
"A632CA68-40D9-4F3A-B8C8-245457057443" : {
"groupChatDescription" : "Test Description",
"groupChatName" : "Test Name",
"members" : {
"WuqCAt4mM3h0P0X1m7hVZ7NQyLC2" : {
"username" : "Steve"
}
},
"owner" : "Steve"
},
Any answers, suggestions, and or references are greatly appreciated.

Seems like you are setting dictionary on your class. So first you need to extract the value from dictionary and set it to your Room model class properties. After that try invoke joined method on your array then it should work.

Related

I expect the result to be a dictionary but it is accessed like a property... why?

I have this JSON coming from a server...
{
"cars": [
{
"name": "Ferrari",
"price": "100"
},
{
"name": "Lamborghini",
"price": "200"
},
{
"name": "Ford Pinto",
"price": "1"
}
]
}
This JSON is a dictionary called cars that contains an array of cars, right?
Then I have this struct...
struct Cars: Codable {
let cars: [Car]
}
struct Car: Codable, Hashable, Identifiable {
let id = UUID()
let name: String
let price: String
}
and I decode the JSON using this:
let (data, _) = try await urlSession.data(from: url)
let result = try JSONDecoder().decode(Cars.self, from: data)
let listOfCars = result.cars
This is something I don't understand.
in result.cars, cars is a property of result that was declared as an array in the struct Cars. Not a dictionary.
I was expecting to access it using result["cars"].
Why is that?
In your code here...
let (data, _) = try await urlSession.data(from: url)
let result = try JSONDecoder().decode(Cars.self, from: data)
let listOfCars = result.cars
result is an instance of the Struct Cars. Your set up code has told Swift how to translate from a JSON dictionary into your own Struct.
So everything inside of result is accessed just like how you would access it in the following...
let result = Cars(cars: [
Car(name: "Ford", price: "£10,000")
])
print(result.cars)
The only difference is how you are creating it. Instead of using the init method like this you are using a JSON decode to decode some JSON into your custom type.
As said in the comments and answers, it takes a result type according to your decode strategy. In your code result type is Cars not a dictionary. So you access the properties with using result.cars
If you want something as dictionary instead, you need to decode it like
let result = try decode.decode([String : [Car]].self, from: data)
Now you can access them like a dictionar
print(result["cars"]?.first?.name) // Optional("Ferrari")

Swift code can not find the symbol of the data

I am new to swift . I am trying to convert json into model by using swift . I am using generic functions to complete the functions . Here is the structure of the json .
Here is the model I created based on jason .
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let photos: [Photo]
}
// MARK: - Photo
struct Photo: Codable {
let id, sol: Int
let camera: Camera
let imgSrc: String
let earthDate: String
let rover: Rover
enum CodingKeys: String, CodingKey {
case id, sol, camera
case imgSrc = "img_src"
case earthDate = "earth_date"
case rover
}
}
// MARK: - Camera
struct Camera: Codable {
let id: Int
let name: String
let roverID: Int
let fullName: String
enum CodingKeys: String, CodingKey {
case id, name
case roverID = "rover_id"
case fullName = "full_name"
}
}
// MARK: - Rover
struct Rover: Codable {
let id: Int
let name, landingDate, launchDate, status: String
enum CodingKeys: String, CodingKey {
case id, name
case landingDate = "landing_date"
case launchDate = "launch_date"
case status
}
}
Here is the code in generic function.
func getModel<Model: Codable>(_ type: Model.Type, from url: String, completion: #escaping (Result<Model, NetworkError>) -> ()) {
guard let url = URL(string: url) else {
completion(.failure(.badURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(.other(error)))
return
}
if let data = data {
do {
let response = try JSONDecoder().decode(type, from: data)
completion(.success(response))
} catch let error {
completion(.failure(.other(error)))
}
}
}
.resume()
}
I am trying to call this function form controller but it is showing the error Value of type 'Post' has no member 'data'
Here is the code to call the function.
class ViewModel {
private let networkManager = NetworkManager()
private var rovers = [Post]()
func getStories (){
networkManager
.getModel(Post.self, from: NetworkURLs.baseURL) {[weak self]result in
switch result{
case .success(let response):
self?.rovers = response.data.camera.map{$0.data} **// error on this line**
case .failure( let error):
print( error.localizedDescription)
}
}
}
Your response is of type Post which has no property data. You'll need to extract your photos array from the response, and then map across that array and retrieve the rovers property from it.
I think what you meant to write was
self?.rovers = response.photos.camera.map{$0.rover}
However even that won't work as your data structures don't match your JSON. From what can be seen, rover is a property on photo not on camera.
You will need to validate the JSON -> Model mapping
EDIT after JSON linked in comment below:
Using the JSON from the API, it confirms that camera and rover sit at the same level in the JSON:
{
"photos": [
{
"id": 102693,
"sol": 1000,
"camera": {
"id": 20,
"name": "FHAZ",
"rover_id": 5,
"full_name": "Front Hazard Avoidance Camera"
},
"img_src": "http://mars.jpl.nasa.gov/msl-raw-images/proj/msl/redops/ods/surface/sol/01000/opgs/edr/fcam/FLB_486265257EDR_F0481570FHAZ00323M_.JPG",
"earth_date": "2015-05-30",
"rover": {
"id": 5,
"name": "Curiosity",
"landing_date": "2012-08-06",
"launch_date": "2011-11-26",
"status": "active"
}
},
....
So you will need to change your data model:
struct Photo : Codable{
let id : Int
let sol : Int
let camera : Camera
let imgSrc: String
let earthDate: String
let rover: Rover
}
and then to decode it
self?.rovers = response.photos.map{$0.rover}
nb. in Swift all struct types should be capitalised by convention.
Your struct of type Post does not have a member called "data", indeed.
You seem to be assuming, that your response object is of type Photo - but the error message is telling you, that it is of type Post, which only holds an array of Photo objects.
Try something like:
response.photos[0] to get the first Photo object out of the array - if there is one.
Then, assuming you got one response.photos[0].data gives you a Camera object already - you seem to be calling via the type, instead of the member name.
So in case you want to go one step further and access a Rover object, you need to do: response.photos[0].data.data
I see, that you want to extract several Rovers, supposedly one from each Post, but this will clash with your initial rovers variable being assigned a type of an array of Posts - this means you have to change it to [Rover]. I'm not sure if the map-function is actually suitable for what you want to do here.
Using a loop, iterating through the Posts and appending Rover objects to the Rover array would be the "manual" way to do it.
Hope this helps.
Edit: because you have edited your model mid-question, I can't see where "Post" has gone now. My reply might only fit the way the original question was posted.

how to Converting JSON into Codable in swift 4.2?

I am using Xcode 10.1 and Swift 4.2. When i try to convert JSON response into Codable class it gives an error that Expected to decode Array<Any> but found a string/data instead.
My Actual JSON response is Like this from API .
{
"d": "[{\"Data\":{\"mcustomer\":[{\"slno\":1000000040.0,\"fstname\":null}]},\"Status\":true}]"
}
My Model is Like this
class MainData: Codable{
var d: [SubData]
}
class SubData : Codable {
var Data : Customer
var Status : Bool?
}
class Customer : Codable {
var mcustomer : [Detail]
}
class Detail : Codable {
var slno : Double?
var fstname : String?
}
And I am Decode this Model using JSONDecoder()
let decoder = JSONDecoder()
let deco = try decoder.decode(MainData.self, from: data)
but, I am unable to Decode this Json into My Model.
Your API is wrong. You array in json shouldn't have quotation marks around it. Otherwise you're declaring that value for key "d" is string
"[...]"
[...]
Suggestions:
Variables and constants should start with small capital letter. Otherwise for example your Data property would cause confusion with Data type. For renaming it while decoding you can use CodingKeys
If you don't need to encode your model, you can just implement Decodable protocol
You can use struct instead of class for your model
The top-level JSON object is a dictionary with the key "d" and a string value, representing another JSON object (sometimes called "nested JSON"). If the server API cannot be changed then the decoding must be done in two steps:
Decode the top-level dictionary.
Decode the JSON object from the string obtained in step one.
Together with Robert's advice about naming, CodingKeys and using structs it would look like this:
struct MainData: Codable {
let d: String
}
struct SubData : Codable {
let data : Customer
let status : Bool
enum CodingKeys: String, CodingKey {
case data = "Data"
case status = "Status"
}
}
struct Customer : Codable {
let mcustomer : [Detail]
}
struct Detail : Codable {
let slno : Double
let fstname : String?
}
do {
let mainData = try JSONDecoder().decode(MainData.self, from: data)
let subData = try JSONDecoder().decode([SubData].self, from: Data(mainData.d.utf8))
print(subData)
} catch {
print(error)
}
For your solution to work, the JSON reponse has to be following format
let json = """
{
"d": [
{
"Data": {
"mcustomer": [
{
"slno": 1000000040,
"fstname": null
}
]
},
"Status": true
}
]
}
"""
But, as you can see, the JSON response you are getting is quite different than you are expecting. Either you need to ask to change the response or you need to change your model.

Decoding JSON array of different types in Swift

I'm trying to decode the following JSON Object
{
"result":[
{
"rank":12,
"user":{
"name":"bob","age":12
}
},
{
"1":[
{
"name":"bob","age":12
},
{
"name":"tim","age":13
},
{
"name":"tony","age":12
},
{
"name":"greg","age":13
}
]
}
]
}
struct userObject {
var name: String
var age: Int
}
Basically a JSON Array with two different object types
{ "rank":12, "user": {userObject} }
and a
"1" : array of [userObjects]
struct data: Decodable {
rank: Int
user: user
1: [user] <-- this is one area Im stuck
}
Thanks in advance
Just for fun:
First you need structs for the users and the representation of the first and second dictionary in the result array. The key "1" is mapped to one
struct User : Decodable {
let name : String
let age : Int
}
struct FirstDictionary : Decodable {
let rank : Int
let user : User
}
struct SecondDictionary : Decodable {
let one : [User]
private enum CodingKeys: String, CodingKey { case one = "1" }
}
Now comes the tricky part:
First get the root container.
Get the container for result as nestedUnkeyedContainer because the object is an array.
Decode the first dictionary and copy the values.
Decode the second dictionary and copy the values.
struct UserData: Decodable {
let rank : Int
let user : User
let oneUsers : [User]
private enum CodingKeys: String, CodingKey { case result }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var arrayContainer = try container.nestedUnkeyedContainer(forKey: .result)
let firstDictionary = try arrayContainer.decode(FirstDictionary.self)
rank = firstDictionary.rank
user = firstDictionary.user
let secondDictionary = try arrayContainer.decode(SecondDictionary.self)
oneUsers = secondDictionary.one
}
}
If this code is preferable over traditional manual JSONSerialization is another question.
If your JSON format is given then you are pretty much out of luck, since you will most likely have to parse your array as [Any] which is, to put it mildly, not very useful. If on the other hand you are able to modify the format of the JSON you should start from the other direction. Define your desired Swift object and encode it using JSONEncoder.encode(...) in order to quickly determine how your JSON should look like in order to make it parse in as typed a way as possible.
This approach will easily half your JSON handling code as your web service protocol will end up being structured much better. This will likely improve the structure of the overall system since it will yield a much more stable communication protocol.
Sadly enough this approach is not always possible which is when things get messy. Given your example you will be able to parse your code as
let st = """
{
"result":[
{
"rank":12,
"user":{
"name":"bob",
"age":12
}
},
{
"1":[
{
"name":"bob","age":12
},
{
"name":"tim","age":13
},
{
"name":"tony","age":12
},
{
"name":"greg","age":13
}
]
}
]
}
"""
let jsonData1 = st.data(using: .utf8)!
let arbitrary = try JSONSerialization.jsonObject(with: jsonData1, options: .mutableContainers)
This will let you access your data with a bunch of casts as in
let dict = arbitrary as! NSDictionary
print(dict["result"])
you get the idea. not very useful as you would very much like to use the Codable protocol as in
struct ArrayRes : Codable {
let result : [[String:Any]]
}
let decoder1 = JSONDecoder()
do {
let addrRes = try decoder.decode(ArrayRes.self, from: jsonData1)
print(addrRes)
} catch {
print("error on decode: \(error.localizedDescription)")
}
Unfortunately this does not work since Any is not Codable for slightly obvious reasons.
I hope you are able to change your JSON protocol since the current one will be the root cause of lot of messy code.

Swift Siesta edit fetched entity

I'm building an API client using Siesta and Swift 3 on Xcode 8. I want to be able to fetch an entity using a Siesta resource, then update some of the data and do a patch to the API.
The issue is that having an entity, if I save the JSON arrays in my entity fields I can't send them back to the server, I get the following error:
▿ Siesta.RequestError
- userMessage: "Cannot send request"
- httpStatusCode: nil
- entity: nil
▿ cause: Optional(Siesta.RequestError.Cause.InvalidJSONObject())
- some: Siesta.RequestError.Cause.InvalidJSONObject
- timestamp: 502652734.40489101
My entity is:
import SwiftyJSON
import Foundation
struct Order {
let id: String?
let sessionId: String?
let userId: Int?
let status: String?
let comment: String?
let price: Float?
let products: Array<JSON>?
init(json: JSON) throws {
id = json["id"].string
sessionId = json["sessionId"].string
userId = json["userId"].int
status = json["status"].string
comment = json["comment"].string
price = json["price"].float
products = json["products"].arrayValue
}
/**
* Helper method to return data as a Dictionary to be able to modify it and do a patch
**/
public func toDictionary() -> Dictionary<String, Any> {
var dictionary: [String:Any] = [
"id": id ?? "",
"sessionId": sessionId ?? "",
"userId": userId ?? 0,
"status": status ?? "",
"comment": comment ?? ""
]
dictionary["products"] = products ?? []
return dictionary
}
}
What I'm doing is:
MyAPI.sessionOrders(sessionId: sessionId).request(.post, json: ["products": [["product": productId, "amount": 2]], "comment": "get Swifty"]).onSuccess() { response in
let createdObject : Order? = response.typedContent()
expect(createdObject?.sessionId).to(equal(sessionId))
expect(createdObject?.comment).to(equal("get Swifty"))
expect(createdObject?.products).to(haveCount(1))
expect(createdObject?.price).to(equal(product.price! * 2))
if let createdId = createdObject?.id {
var data = createdObject?.toDictionary()
data?["comment"] = "edited Swifty" // can set paid because the user is the business owner
MyAPI.order(id: createdId).request(.patch, json: data!).onSuccess() { response in
result = true
}.onFailure() { response in
dump(response) //error is here
}
}
}
Resources:
func sessionOrders( sessionId: String ) -> Resource {
return self
.resource("/sessions")
.child(sessionId)
.child("orders")
}
func order( id: String ) -> Resource {
return self
.resource("/orders")
.child(id)
}
Transformers:
self.configureTransformer("/sessions/*/orders", requestMethods: [.post, .put]) {
try Order(json: ($0.content as JSON)["data"])
}
self.configureTransformer("/orders/*") {
try Order(json: ($0.content as JSON)["data"])
}
I've managed to circle this by creating dictionary structures like:
let products: Array<Dictionary<String, Any>>?
products = json["products"].arrayValue.map({
["product": $0.dictionaryValue["product"]!.stringValue, "amount": $0.dictionaryValue["amount"]!.intValue]
})
But I live in a hell of downcasts if I need to modify anything:
var data = createdObject?.toDictionary()
data?["comment"] = "edited Swifty"
//if I want to modify the products...
var products = data?["products"] as! Array<Dictionary<String, Any>>
products[0]["amount"] = 4
data?["products"] = products
How can I send those original JSON arrays with Siesta? They're really easy to modify and read! I've browsed the siesta docs and github issues with no success...
Your problem is a mismatch between SwiftyJSON and Foundation’s JSONSerialization; Siesta just happens to be in the middle of it.
InvalidJSONObject is Siesta telling you that Foundation doesn’t understand the thing you gave it — which would be the value returned by your toDictionary() method. Most of the things in that dictionary look fine: strings, ints, a float. (Careful about using float for money, BTW.)
The culprit is that products array: it’s [JSON], where JSON is a SwiftyJSON type that Foundation doesn’t know what to do with. You should be in the clear if you turn the JSON values back into simple dictionaries:
dictionary["products"] = (products ?? []).map { $0.dictionaryObject }
If that doesn’t do it, or if you need to diagnose a similar error in the future, remove all the values from the offending dictionary and then add them back in one at a time to see which one is tripping up JSONSerialization.