Best way to dynamically store and access data in Swift - json

I have an app where every time it starts up (within the splash screen) it gets the currently logged in users' data from my database via an API endpoint.
The type of JSON expected to be sent back to the app when it asks for data is as shown:
{
"username": "test",
"email": "test#gmail.com",
"uid": "5f661ffe1a80160027a6cb0c",
"isVerified": true,
"hub": {
"hubID": "npnDsZegiSL5",
"isSetup": true,
"hubSWVersion": 0.11,
"_id": "5f661ffe1a80160027a6cb0d",
"cameraService": {
"isSetup": true,
"bridgeIP": "192.168.0.12",
"username": "JesCGSr6HrWoKbKnnNOHSayVKdb1"
},
"servicesSetup": {
"1": "cameraService",
"2": "TPLinkPlug"
}
},
"code": "100",
"message": "Success. User details have been provided"
}
As the user adds services/updates values, this database structure will constantly change. Which is my problem. There are tonnes of questions like this out there with all the same answer, to use the JSONDecoder and push the values to an observable object which can be accessed throughout the app via creating an instance of it or just to simply assign the values to the object when it has been fetched. Which I have done and am currently using:
The class userData object:
class userData: NSObject, ObservableObject {
var userdata = userData.self
#Published var uid: String = ""
#Published var isVerified: Bool = false
#Published var username: String = ""
#Published var email: String = ""
}
The code where I assign the values to the object when I receive the above json in my app:
...
#EnvironmentObject var user: userData
print("Successfully got user data. Code: \(code ?? "0"). UID: \(response?["uid"] as! String). Username: \(response?["username"] as! String)")
DispatchQueue.main.async {
user.email = response?["email"] as! String
user.uid = response?["uid"] as! String
user.username = response?["username"] as! String
...
Then I can access it anywhere in my via:
#EnvironmentObject var user: userData
username = self.user.username
But this would require hard-coded code to assign each value from my database, is there a better, more dynamic way to assign the whole json response from my API (no matter what values/embedded jsons there is) to an object and access these data values throughout my app via that object?
Thanks

Because you'll have new key value pairs in your response, I think you should store the response as a Dictionary and get desired values from that dictionary instead of parsing the response and storing separate variables.
here is an example:
let jsonStr = """
{\"username\": \"test\",
\"email\": \"test#gmail.com\",
\"uid\": \"5f661ffe1a80160027a6cb0c\",
\"isVerified\": true,
\"hub\": {
\"hubID\": \"npnDsZegiSL5\",
\"isSetup\": true,
\"hubSWVersion\": 0.11,
\"_id\": \"5f661ffe1a80160027a6cb0d\",
\"cameraService\": {
\"isSetup\": true,
\"bridgeIP\": \"192.168.0.12\",
\"username\": \"JesCGSr6HrWoKbKnnNOHSayVKdb1\"
},
\"servicesSetup\": {
\"1\": \"cameraService\",
\"2\": \"TPLinkPlug\"
}
},
\"code\": \"100\",
\"message\": \"Success. User details have been provided\"
}
"""
let data = jsonStr.data(using: .utf8)
do {
// store this variable
let dict = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
for (k,v) in dict! {
// example access to values
print("key: \(k), value: \(v)")
}
} catch {
print(error.localizedDescription)
}

Related

How to convert dinamically Swift classes to JSON and vice-versa?

In my new project that act like a RPC, in some moment i receive a JSON with function name and a list of parameters. Example:
{
"f": "sample.login",
"p": [
{"n": "param1", "v": "value1"},
{"n": "param2", "v": true},
{"n": "param3", "v": {"id": 1, "title": "title xyz"}}
[...] any amount of params [...]
]
}
In other moments, i need create the same structure and encode as JSON. Example:
public class Param: Codable {
public var n: String
public var v: Any?
init(n: String, v: Any?) {
self.n = n
self.v = v
}
}
struct Todo: Codable {
var id: Int64
var title: String
var data: [String: String]
var done: Bool
}
public class JsonSerializer: Serializer {
private var decoder = JSONDecoder()
private var encoder = JSONEncoder()
public func encodeRequest(functionName: String, params: [Param]) -> String {
do {
let request = JsonRequestData(f: functionName, p: params)
let data = try encoder.encode(request)
if let result = String(data: data, encoding: .utf8) {
return result
} else {
print("[JsonSerializer : encodeRequest] Error when try to encode data")
}
} catch let e {
print("[JsonSerializer : encodeRequest] Error when try to encode data: \(e.localizedDescription)")
}
return ""
}
struct JsonRequestData: Codable {
let f: String
var p: [Param]
init(f: String, p: [Param]) {
self.f = f
self.p = p
}
}
}
let todo = Todo(id: 1, title: "Title 1", data: [:], done: true)
let name = "sample.todo.single"
var params: [Param] = []
params.append(Param(n: "suffix", v: "%"))
params.append(Param(n: "p2", v: todo))
let s = JsonSerializer()
let json = s.encodeRequest(functionName: name, params: params)
print(json)
I made it work in C++ (nlohmann json) and Kotlin (with gson). Only left make it work in Swift.
I know of course Swift doesn't support encoding ANY type. And I'm aware of some limitations on this in Swift.
But I would like to find a plausible solution to my problem.
Even if the user has to implement a protocol on his side for his types, or enter his type in a list of known types or something.
The project is at this URL, if you want to see the codes in more depth:
https://github.com/xplpc/xplpc
Removing this lock, the code is practically ready.
I tried on Apple forums, search on Google and on iOS group inside Slack.
Thanks for answers.
But after try a lot, i decide to use AnyCodable project (https://github.com/Flight-School/AnyCodable) with my modifications (https://github.com/xplpc/xplpc/tree/main/swift/lib/Sources).
AnyCodable let me use all swift types and if i use these types on my class/struct it works without problems.
To use any custom type, only need add more lines on AnyEncodable and AnyDecodable class.
Thanks.

Append Nested Objects in Parameters of multi-form Swift using Alamofire

I am uploading multiple images as well as JSON data using "Content-type": "multipart/form-data"
my issue is, I have nested objects to pass to the parameters
I was looking for a solution, and what I find is nested data with an array of String or Int, not another custom object (I was struggling with it for so long)
struct Car: Codable{
var id :Int,
var name:String,
var address:String,
var new:Bool
var users:[User]
}
struct User: Codable{
var id :Int,
var name:String,
var address:String,
var age:Int
}
I wanted to convert the data to do a dictionary to use it as parameters
func addNewCae(newCar:Car, images:[UIImage]){
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(newCar)
let test = convertStringToDictionary(text: jsonData)
print(test)
let headers: HTTPHeaders
headers = ["Content-type": "multipart/form-data",
"Content-Disposition" : "form-data"]
AF.upload(multipartFormData: { multipartFormData in
for imageData in images {
guard let imgData = imageData.pngData() else { return }
multipartFormData.append(imgData , withName: "images[]", fileName: "image.jpeg", mimeType: "image/jpeg")
}
for (key, value) in self.convertStringToDictionary(text: jsonData)! {
multipartFormData.append("\(value)".data(using: .utf8)!, withName: key)
}
},to: "\(url)", usingThreshold: UInt64.init(),
method: .post,
headers: headers).responseString{ response in
switch response.result {
case .success(let value):
print(value)
break
case .failure(let error):
print(error)
}
}
}
func convertStringToDictionary(data: Data) -> [String:AnyObject]? {
do {
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:AnyObject]
return json
} catch {
print("Something went wrong")
}
return nil
}
As a request of my code: all the car fields are registered in the database except the users' field
(in reality, I have more fields than that)
Why the users' field is not added to the parameters?
Any idea?
The users filed normally be of type [[String: AnyObject]] in the parameters
here is the data after I convert it to a dictionary
Optional(["name":"whatever" , "address": "here", "users": <__NSArrayM 0x6000008e4f90>(
{
"name" = "hello";
"address" = "there";
"age" = 20;
},
{
"name" = "hi";
"address" = "location";
"age" = 25;
}
)
, "new": 0])
hope am clear enough, I can add any further code or information if needed to be clearer
Thanks
Update
I did the encoding manually so now I have a correct dictionary format of type [String: AnyObject] but one of them is nested :
["name":"whatever" , "address": "here", "users": [
[
"name" = "hello";
"address" = "there";
"age" = 20;
],
[
"name" = "hi";
"address" = "location";
"age" = 25;
]
]
, "new": 0]
BUT, still, the "users" field is not able to be read, I think because the parameters don't support that kind of type?
Anyone have any other idea how to deal with these nested objects?

Match row selected to pass data

I've taken a look at all these Swift, and asp.net, and javascript questions.
1
2
3
4
5
Goal:
When I select a messages from the list of chat messages in the MessageListController I want the opened session in the next ChatDetailController to be the conversation that was selected.
I'm doing the same thing in this iOS image for my WatchKit app. The message with Sophia is selected and the chat with Sophia opens.
[![enter image description here][6]][6]
I want to pass the json "message_id" i.e. the chatMessageId property. I'm already passing the chatMessageId from the MessageModelto the ChatDetailController as you can see in code.
Is it the chatMessageId of the ChatModelI need to pass? Or am I already passing the data that I need?
Passed context: Optional(HTWatch_Extension.MessageModel(partner: "9859", nickname: "Marco", message: "Have you seen is dog?", city: "Madrid", countryBadgeImageURL: https://i.imgur.com/PJcyle7.jpg, messageListImageURL: https://i.imgur.com/PJcyle7.jpg, chatMessageId: "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ", status: "offline"))
Attempt:
Do I need to take the do-catch block where I parse the ChatModel out of the ChatDetailController's awakeWithContext method and put it in the didSelectRowAt method of the MessageListController?
MessageListController
// ...code...
var messageObject = [MessageModel]()
var chatObject = [ChatModel]()
// ...code...
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
var messageContext = messageObject[rowIndex]
var chatContext = chatObject[rowIndex]
do {
guard let fileUrl = Bundle.main.url(forResource: "Chats", withExtension: "json") else {
print("File could not be located")
return
}
let data = try Data(contentsOf: fileUrl)
let decoder = JSONDecoder()
let msg = try decoder.decode([ChatModel].self, from: data)
self.chatObject = msg
} catch let error {
print(error)
}
messageContext.chatMessageId = (chatObject as AnyObject).filter { (dictionaryTemp:[String:String]) -> Bool in
return dictionaryTemp["message_id"] == chatContext.chatMessageId
}
// WatchKit's model presentation method.
presentController(withName: "ChatDetailController", context: messageContext)
}
If I have understood correctly, your Chat.json, will have chat's for all message id's. Select one of the message id row and load the respective chat history.
In that case you can parse based on message by using filter. Let's say you have it in a dictionary like this.
Example:
let responseString = "{\"name\":\"Tom\"}"
if let responseData = responseString.data(using: .utf8){
do {
let object = try JSONSerialization.jsonObject(with:responseData , options: .allowFragments)
print("Response Object=\(object)")
} catch{
print("parsing Error=\(error)")
}
}
You can use a similar code to create your Object. The final object should be something like chatDictionary
let chatDictionary = [
[
"fromId": "zz1234skjksmsjdfwe2zz",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "Have you seen is dog?",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ",
"read": "true"
],
[
"fromId": "zz1234skjksmsjdfwe2zz",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "Yes I have. It's cute.",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ",
"read": "true"
],
[
"fromId": "zz1234skjksmsjdfwe2zz",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "I want to get a pet too.",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ1",
"read": "true"
]
]
Your did Select Row At index
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
let message = messageObjects[rowIndex]
// Create a chat object Dictionary, parse it before you pass it to the detail View Controller , if you have the chat.json.
// I have used 'Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ', but here you can your message id property to make it dynamic
message.chatObjects= chatDictionary.filter { (dictionaryTemp:[String : String]) -> Bool in
return dictionaryTemp["message_id"] == "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ"
}
presentController(withName: "ChatDetailController", context: message)
}

Facing Problem in encoding array of dictionary to JSON

In POST request I have to send following JSON data
{
"users": [{"userid": 16, "meetAt":"Some place (College/trip etc)", "showFields": "11111111000"}, {"userid": 17, "meetAt":"Some place (College/trip etc)", "showFields": "11111111001"}]
}
I am trying
static func linkRequestBody(userDetails: ScannedContact) -> Any
{
let jsonToRegistrer = [["userid":userDetails.id, "meetAt":"Defalut Test Location at", "showFields":userDetails.showFields]];
return jsonToRegistrer;
}
I can see in debugger that userDetails.id and userDetails.showFields have valid value but still it fails.
ERROR:
{"":["The input was not valid."]}
That's your target format:
{
"users": [{
"userid": 16,
"meetAt": "Some place (College/trip etc)",
"showFields": "11111111000"
}, {
"userid": 17,
"meetAt": "Some place (College/trip etc)",
"showFields": "11111111001"
}]
}
After calling JSONSerialization on it (or if your code accept a Dictionary/Array and do the translation itself):
let jsonToRegistrer = [["userid":userDetails.id, "meetAt":"Defalut Test Location at", "showFields":userDetails.showFields]];
Should represent that:
[{
"userid": 16,
"meetAt": "Defalut Test Location at",
"showFields": "11111111000"
}]
You see the issue? Your version is an Array and the target one is a Dictionary and you are so missing the users key.
To fix it:
let jsonToRegistrer = ["user": [["userid":userDetails.id, "meetAt":"Defalut Test Location at", "showFields":userDetails.showFields]]];
You can also not write it in one line to be clearer:
let jsonToRegistrer = ["user": [
["userid": userDetails.id,
"meetAt": "Defalut Test Location at",
"showFields": userDetails.showFields]
]
];
So the issue is that your version didn't have the same format at the one needed.
To see what's your version rendered in JSON you can use:
let jsonData = try? JSONSerialization.data(withJSONObject: jsonToRegistrer, options: .prettyPrinted)
let jsonString = String(data: jsonData!, encoding: .utf8) //Note it's a force unwrap but just to debug, avoid using force unwrap
print("jsonString: \(jsonString)")
I would personally avoid the problem by using struct thant implements Codable. As you don't have any value of type Any nor recursive field, this should work very easily
struct Users: Codable {
var users: [User]
}
struct User: Codable {
let userid: Int
let meetAt: String
let showFields: String
}
Then you just have to bind User in Users.users can encode it easily doing
do {
let json = try? JSONEncoder().encode(Users)
} catch { // whatever you want }
This article might help you to understand better Codable protocol.
Hope it helps !

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.