Swift: How to use response from SocketIO emitWithAck method - json

I'm using the SocketIO library to connect my iOS app to my server.
I want to emit some data to the server and get a json dictionary back in the acknowledgment. I currently have something like this:
SocketHandler.mySocket.emitWithAck("my_event", [session, someInput]).timingOut(after: 3) {data in
let myData = try? JSONDecoder().decode(myStruct.self, from: data)
MyStruct is defined as Class inheriting from Decodable and resembles the structure of the json I expect.
I get the following error: Cannot convert value of type 'Any' to expected argument type 'Data'
Any idea how I can tackle that type casting? Or would I need to go a totally other route?
(Swift 4.1 for iOS 11.3)
Cheers!

If anyone else is wondering how to use SocketIO with Decodable, I created a little extension for the client to accept Decodable in the callback, based on Dan Karbayev's answer.
import Foundation
import SocketIO
extension Decodable {
init(from any: Any) throws {
let data = try JSONSerialization.data(withJSONObject: any)
self = try JSONDecoder().decode(Self.self, from: data)
}
}
extension SocketIOClient {
func on<T: Decodable>(_ event: String, callback: #escaping (T)-> Void) {
self.on(event) { (data, _) in
guard !data.isEmpty else {
print("[SocketIO] \(event) data empty")
return
}
guard let decoded = try? T(from: data[0]) else {
print("[SocketIO] \(event) data \(data) cannot be decoded to \(T.self)")
return
}
callback(decoded)
}
}
}
Usage:
socket.on("location") { (data: LocationEventData) in
// ...
}
socket.on("success") { (name: String) in
// ...
}
Where LocationEventData and String are Decodable.

There're two things:
decode(_:from:) accepts a Data as a second parameter. To be able to decode from Any you'll need to add an extension to first serialize the data and then pass it to JSONDecoder, like this:
extension Decodable {
init(from any: Any) throws {
let data = try JSONSerialization.data(withJSONObject: any)
self = try JSONDecoder().decode(Self.self, from: data)
}
}
AckCallback's parameter is of an array type (i.e. [Any]), so you should get the first element of that array.
To make sure that you have indeed a decodable data (a dictionary or a JSON object) you can write something like this:
SocketHandler.mySocket.emitWithAck("my_event", [session, someInput]).timingOut(after: 3) { data in
guard let dict = data.first as? [String: Any] else { return }
let myData = try? myStruct(from: dict)
// ...
}

Related

Decode JSON single object or array of object dynamically [duplicate]

This question already has an answer here:
Swift 4 JSON Codable - value returned is sometimes an object, others an array
(1 answer)
Closed 3 years ago.
Let's say I have an array of JSON response from the GET method like :
[{
"id":"1",
"Name":"John Doe",
},{
"id":"2",
"Name":"Jane Doe",
}]
And from the POST method using id param I only have 1 object JSON response :
{
"id":"1",
"Name":"John Doe",
}
how can I write a method to decode both the JSON dynamically?
At the moment, this is what I'm using :
func convertJSON<T:Decodable>(result: Any?, model: T.Type) -> T? {
if let res = result {
do {
let data = try JSONSerialization.data(withJSONObject: res, options: JSONSerialization.WritingOptions.prettyPrinted)
return try JSONDecoder().decode(model, from: data)
} catch {
print(error)
return nil
}
} else {
return nil
}
}
The method can be used to decode a single object using dynamic model, but I just can't figure it out to handle a single object / an array of objects dynamically.
The most I can get with is just using a duplicate of the method but replacing T with
[T] in the method parameter and return type, if the response is an array.
I'm open to any suggestion, any help is appreciated, Thank You in advance.
Edit : If this question is duplicate of this , I'm not sure how the marked answer could be a solution.
One solution could be to always return [Model]?.
Inside your function first try to decode as Model, on success return an array with that single decoded object inside it. If this fails then try to decode as [Model], on success return the decoded object else return nil.
Using your sample JSONs I created a struct:
struct Person: Codable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id
case name = "Name"
}
}
Then I created a struct with a couple of methods to decode from either a String or an optional Data.
struct Json2Type<T: Decodable> {
// From data to type T
static public func convertJson(_ data: Data?) -> [T]? {
// Check data is not nil
guard let data = data else { return nil }
let decoder = JSONDecoder()
// First try to decode as a single object
if let singleObject = try? decoder.decode(T.self, from: data) {
// On success return the single object inside an array
return [singleObject]
}
// Try to decode as multiple objects
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return nil }
return multipleObjects
}
// Another function to decode from String
static public func convertJson(_ string: String) -> [T]? {
return convertJson(string.data(using: .utf8))
}
}
Finally call the method you prefer:
Json2Type<Person>.convertJson(JsonAsDataOrString)
Update: #odin_123, a way to have either a Model or [Model] as return value can be accomplish using an enum. We can even add the error condition there to avoid returning optionals. Let's define the enum as:
enum SingleMulipleResult<T> {
case single(T)
case multiple([T])
case error
}
Then the struct changes to something like this:
struct Json2Type<T: Decodable> {
static public func convertJson(_ data: Data?) -> SingleMulipleResult<T> {
guard let data = data else { return .error }
let decoder = JSONDecoder()
if let singleObject = try? decoder.decode(T.self, from: data) {
return .single(singleObject)
}
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return .error }
return .multiple(multipleObjects)
}
static public func convertJson(_ string: String) -> SingleMulipleResult<T> {
return convertJson(string.data(using: .utf8))
}
}
You can call it the same way we did before:
let response = Json2Type<Person>.convertJson(JsonAsDataOrString)
And use a switch to check every possible response value:
switch(response) {
case .single(let object):
print("One value: \(object)")
case .multiple(let objects):
print("Multiple values: \(objects)")
case .error:
print("Error!!!!")
}

Converting Firebase Datasnapshot to codable JSON data

I'm using Firebase Database and I'm attempting to retrieve and use data with NSObject. I'm receiving an NSUnknownKeyException error when running the app, causing it to crash.
NSObject:
class WatchList: NSObject {
var filmid: Int?
}
Firebase Code:
ref.child("users").child(uid!).child("watchlist").observe(DataEventType.childAdded, with: { (info) in
print(info)
if let dict = info.value as? [String: AnyObject] {
let list = WatchList()
list.setValuesForKeys(dict)
print(list)
}
}, withCancel: nil)
I'm not sure of what could cause this.
Also, to enhance this solution is their a way to take this data and, instead of using NSObject, use Codable and JSONDecoder with the Firebase data?
You can simply use JSONSerialization to convert the snapshot value property from Any to Data:
let data = try? JSONSerialization.data(withJSONObject: snapshot.value)
You can also extend Firebase DataSnapshot type and add a data and json string properties to it:
import Firebase
extension DataSnapshot {
var data: Data? {
guard let value = value, !(value is NSNull) else { return nil }
return try? JSONSerialization.data(withJSONObject: value)
}
var json: String? { data?.string }
}
extension Data {
var string: String? { String(data: self, encoding: .utf8) }
}
usage:
guard let data = snapshot.data else { return }
It's 2021 now.
Firebase finally added support for decoding Firestore documents. Just let your objects conform to Codable and decode like this:
let result = Result {
try document?.data(as: City.self)
}
switch result {
case .success(let city):
if let city = city {
print("City: \(city)")
} else {
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding city: \(error)")
}
Just don't forget to add the 'FirebaseFirestoreSwift' pod and then import it into your file.
Read more:
https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
Original answer
A really nice library to use here is Codable Firebase which I am also using in my project. Just make your class / struct conform to Codable protocol and use FirebaseDecoder to decode your Firebase data into a Swift object.
Example:
Database.database().reference().child("model").observeSingleEvent(of: .value, with: { snapshot in
guard let value = snapshot.value else { return }
do {
let model = try FirebaseDecoder().decode(Model.self, from: value)
print(model)
} catch let error {
print(error)
}
})

parse json, populate collection view Swift 4.2

This is too simple but I am lost. I am still new to swift really.
I need to parse the downloaded json ( localized file in the Xcode project ) and populate the data to a CollectionView.
enum Response{
case success(Data)
case error(Error)
}
// struct follows the json
struct InformationFromJson: Decodable {
let id: Int
let name: String
}
class MYJSON {
public func downloadMYJSON(_ completion: #escaping (Response) -> ()) {
guard let bundle = Bundle(identifier: MYJSON.bundleId), let path = bundle.path(forResource: "data", ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
completion(Response.error(NSError(domain: MYJSON.bundleId, code: MYJSON.bundleErrorCode, userInfo: [NSLocalizedDescriptionKey : MYJSON.bundleError])))
return
}
completion(Response.success(data))
}
}
So, without totally changing the function call, how do I parse the json? It's downloaded so far from the function, but I don't see how to even add a print statement to test, without getting errors because of the guard statement , the way it is.
I need to simple populate a cellForRowAt:
I never saw nested guard like this, so it got me. I am used to seeing the let statements separated so you can put print statements to at least see if things are getting downloaded or parsed.
You can decode your json by passing data, whatever you get from
let data = try? Data(contentsOf: URL(fileURLWithPath: path))
guard let decoded = try? JSONDecoder().decode(InformationFromJson.self, from: data) else {
return
}

How to iterate through JSON

I have a method which performs a GET request to an API:
public func getApiData(completion: #escaping () -> (), fullUrl: String)
{
let session = URLSession.shared
let url = URL(string: fullUrl)!
let task = session.dataTask(with: url) { (data, _, _) -> Void in
if let data = data {
self.serializeToJSON(jsonData: data)
completion()
}
}
task.resume()
}
Using SwiftyJSON I then convert the data into JSON:
private func serializeToJSON(jsonData: Data) {
self.json = JSON(data: jsonData)
print(self.json)
for (index,item) in self.json {
print("hi")
}
}
Printing the full JSON gives:
[{"TenantID":1,"Tenant1":"RAC"},{"TenantID":2,"Tenant1":"VictorMillwell"},{"TenantID":3,"Tenant1":"Comfort"},{"TenantID":4,"Tenant1":"Greenlight"}]
However the JSON can't be iterated through as the print("hi") isn't executed, I'm not sure why, I've looked everywhere on the internet to understand why it doesn't iterate and I cant seem to understand why.
Does anyone know why?
There is a good tutorial here, but in the manual it says you can loop like this:
// If json is .Dictionary
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
// If json is .Array
// The `index` is 0..<json.count's string value
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
if you don't know if it's a dictionary or array, maybe you can do it like this:
switch self.json.type {
case .array:
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
case .dictionary:
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
default:
// Do some error handling
}
It isn't clear why you want to enumerate the JSON. It's trivial to decode this JSON in Swift 4:
struct Tenant:Decodable { let TenantID:Int; let Tenant1:String }
let arr = try! JSONDecoder().decode([Tenant].self, from: data)
Now arr is a Swift array of Tenant, where each Tenant has a TenantID property and a Tenant1 property. And now you can do whatever you like with that array, including cycling through it if you wish.

How to convert a Swift object to a dictionary

I'm relatively new to iOS programming. However, I would have assumed that Swift would have an automated way of converting objects to JSON and vice versa. That being said, I have found several libraries that can do this.
HOWEVER...
It seems that no matter how you post data to a web service (even using something like AlamoFire), the requests must be a dictionary. All these forums show examples of how easy it is to convert the returned JSON string to objects. True. But the request needs to be manually coded. That is, go through all of the object properties and map them as a dictionary.
So my question is this: Am I missing something? Have I got this all wrong and there's a super-easy way to either (a) send JSON (instead of a dictionary) in the REQUEST or (b) convert an object automatically to a dictionary?
Again, I see how easy it is to deal with a JSON response. I'm just looking for an automatic way to convert the request object I want to post to a web service into a format that a library like AlamoFire (or whatever) requires. With other languages this is fairly trivial, so I'm hoping there's an equally easy and automated way with Swift.
I must disagree with #Darko.
In Swift 2,
use protocol oriented programming and the simple reflection offered by Mirror class :
protocol JSONAble {}
extension JSONAble {
func toDict() -> [String:Any] {
var dict = [String:Any]()
let otherSelf = Mirror(reflecting: self)
for child in otherSelf.children {
if let key = child.label {
dict[key] = child.value
}
}
return dict
}
}
then you can use this protocol with your request class and produce the desired dictionary :
class JsonRequest : JSONAble {
var param1 : String?
// ...
}
let request = JsonRequest()
// set params of the request
let dict = request.toDict()
// use your dict
My solution to this will be something like this:
extension Encodable {
var dict : [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else { return nil }
return json
}
}
and usage will be something like this:
movies.compactMap { $0.dict }
Swift currently does not support advanced reflection like Java or C# so the answer is: no, there is not an equally easy and automated way with pure Swift.
[Update] Swift 4 has meanwhile the Codable protocol which allows serializing to/from JSON and PLIST.
typealias Codable = Decodable & Encodable
Without using reflection, and works for nested objects (Swift 4):
protocol Serializable {
var properties:Array<String> { get }
func valueForKey(key: String) -> Any?
func toDictionary() -> [String:Any]
}
extension Serializable {
func toDictionary() -> [String:Any] {
var dict:[String:Any] = [:]
for prop in self.properties {
if let val = self.valueForKey(key: prop) as? String {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Int {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Double {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Array<String> {
dict[prop] = val
} else if let val = self.valueForKey(key: prop) as? Serializable {
dict[prop] = val.toDictionary()
} else if let val = self.valueForKey(key: prop) as? Array<Serializable> {
var arr = Array<[String:Any]>()
for item in (val as Array<Serializable>) {
arr.append(item.toDictionary())
}
dict[prop] = arr
}
}
return dict
}
}
Just implement properties and valueForKey for the custom objects you want to convert. For example:
class Question {
let title:String
let answer:Int
init(title:String, answer:Int) {
self.title = title
self.answer = answer
}
}
extension Question : Serializable {
var properties: Array<String> {
return ["title", "answer"]
}
func valueForKey(key: String) -> Any? {
switch key {
case "title":
return title
case "answer":
return answer
default:
return nil
}
}
}
You can add more value types in the toDictionary function if you need.
The latest solution that I found after lots of digging throughout Stack Overflow is:
//This block of code used to convert object models to json string
let jsonData = try JSONEncoder().encode(requestData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
//This method is used to convert jsonstring to dictionary [String:Any]
func jsonToDictionary(from text: String) -> [String: Any]? {
guard let data = text.data(using: .utf8) else { return nil }
let anyResult = try? JSONSerialization.jsonObject(with: data, options: [])
return anyResult as? [String: Any]
}
//Use above method something like this
let params = jsonToDictionary(from: jsonString) ?? [String : Any]()
//Use params to pass in paramters
Alamofire.request(completeUrl, method: .post, parameters: params, encoding:JSONEncoding.prettyPrinted, headers: myHeaders){
response in
//Do whatever you want with response of it.
}
Note:
I combine this solution from multiple answers.
This solution i used with alamofire because alamofire only accept parameter at this format "[String:Any]".
You can also use the ObjectMapper library. It has a "toJSON" method that converts your object to a dictionary.
in short
let dict = Mirror(reflecting: self).children.map({ $0 }).reduce(into: [:]) { $0[$1.label] = $1.value }
Example how to use Mirror with conversion to specific Dictionary type:
protocol DictionaryConvertible { }
extension DictionaryConvertible {
func toDictionary() -> [String: CustomStringConvertible] {
Dictionary(
uniqueKeysWithValues: Mirror(reflecting: self).children
.compactMap { child in
if let label = child.label,
let value = child.value as? CustomStringConvertible {
return (label, value)
} else {
return nil
}
}
)
}
}