Swift: Decode message sent from GameKit - swift

I'm trying to receive messages in GameKit. The part of receiving the messages works well, but I can not figure out how to decode the data properly.
enum MessageType: Int, Codable {
case BestHost, GameBegin, YourTurn, PlayCard, GameOver
}
struct Message: Codable {
let messageType: MessageType
}
struct MessageBestHost: Codable {
let message: Message
let bestHostId: String
let bestHostName: String
}
I use the above infrastructure for sending and receiving my messages. For sending I encode the message as follows and then send it to all players:
func encode<T: Encodable>(_ item: T) throws -> Data {
let encoder = JSONEncoder()
return try encoder.encode(item)
}
func sendBestHost(player: GKPlayer) {
let message = MessageBestHost(message: Message(messageType: MessageType.BestHost), bestHostId: player.gamePlayerID, bestHostName: player.alias)
do {
try sendDataToAllPlayers(data: encode(message))
}
catch {
print("Error: \(error.localizedDescription)")
}
}
When receiving I use this method to decode the data:
func decode<T: Decodable>(from data:Data) throws -> T {
let decoder = JSONDecoder()
let item = try decoder.decode(T.self, from: data)
return item
}
let message: Message
do {
message = try decode (from: data)
print(message)
} catch {
print (error)
}
if message.messageType == MessageType.BestHost {
do {
let messageBestHost: MessageBestHost = try decode(from: data)
print("\(messageBestHost.bestHostName) hosts the game")
} catch {
print(error)
}
The problem now is. I first need to decode the message as type: Message in order to filter for the correct Subtype and do the magic for the referencing messageType. When trying to cast the data into a variable of type message, the decoding (obviously) fails because the sent data does not contain a messageType in the top hierarchical level.
Error: No value associated with key CodingKeys(stringValue: \"messageType\", intValue: nil)
Without being able to filter for the messageType first I won't be able to distinguish between different messages and execute different methods.
I guess the solution might lay within my data infrastructure but I cant think of any way to make this work. Does anybody have a clue how to solve this problem?

I might try something like this:
import Foundation
enum MessageType: Int, Codable {
case BestHost, GameBegin, YourTurn, PlayCard, GameOver
}
struct Message: Codable {
let messageType: MessageType
let data: Data
}
struct MessageBestHost: Codable {
let bestHostId: String
let bestHostName: String
}
do {
// Serialize:
let messageBestHost = MessageBestHost(bestHostId: "id", bestHostName: "name")
let messageBestHostData = try JSONEncoder().encode(messageBestHost)
let message = Message(messageType: .BestHost, data: messageBestHostData)
let messageData = try JSONEncoder().encode(message)
try sendDataToAllPlayers(data: encode(message))
// Deserialize:
let messageReceived = try JSONDecoder().decode(Message.self, from: messageData)
if messageReceived.messageType == .BestHost {
let messageBestHostReceived = try JSONDecoder().decode(MessageBestHost.self, from: messageReceived.data)
print(messageBestHostReceived.bestHostId)
print(messageBestHostReceived.bestHostName)
}
} catch {
print("Error: \(error.localizedDescription)")
}
Also swift enum cases should start with lowercase letter

Related

Swift Generics - Pass multiple types from caller

I have a function to talk to my REST server as follows
func send<T: Decodable>(_ request: HTTPSClient.Request) async throws -> T {
do {
let (data, status): (Data, HTTPSClient.StatusCode) = try await request.send()
if status.responseType != .success { // success means 2xx
throw try JSONDecoder().decode(CustomError.self, from: data)
}
return try JSONDecoder().decode(T.self, from: data)
} catch {
// some error handling here
}
}
And is called as follows
public struct API1Response: Codable {
// some fields
}
...
let response: API1Response = try await self.send(httpsRequest)
Now I have a special use case where the response needs to be JSON decoded into different structs based on the HTTP response status code (2xx).
For example, if the response code is 200 OK, it needs to be decoded into struct APIOKResponse. If the response code is 202 Accepted, it needs to be decoded into struct APIAcceptedResponse and so on.
I want to write a similar function as above which can support multiple response types
I have written the below function, it does not throw any compilation errors
func send<T: Decodable>(_ request: HTTPSClient.Request, _ types: [HTTPSClient.StatusCode: T.Type]) async throws -> T {
do {
let (data, status): (Data, HTTPSClient.StatusCode) = try await request.send()
if status.responseType != .success { // success means 2xx
throw try JSONDecoder().decode(CustomError.self, from: data)
}
guard let t = types[status] else {
throw ClientError.unknownResponse
}
return try JSONDecoder().decode(t.self, from: data)
} catch {
// some error handling here
}
}
I don't understand how to call this though. Tried below
struct APIAcceptedResponse: Codable {
// some fields
}
struct APIOKResponse: Codable {
// some fields
}
...
let response = try await self.send(httpsRequest, [.accepted: APIAcceptedResponse, .ok: APIOKResponse])
// and
let response = try await self.send(httpsRequest, [.accepted: APIAcceptedResponse.self, .ok: APIOKResponse.self])
But in both cases it shows error
Cannot convert value of type 'APIAcceptedResponse.Type' to expected dictionary value type 'APIOKResponse.Type'
Is there something I am doing wrong in the send function itself?
If not, how to call it?
Is this is something can be achieved even?
This cannot be solved with generics. T has to be of a certain type. So you cannot provide an array of multiple different types the function could choose from. Also this type has to be known at compile time. You canĀ“t choose it depending on your response.
As an alternative you could use protocols.
A simple example:
protocol ApiResponse: Decodable{
// common properties of all responses
}
struct ApiOkResponse: Codable, ApiResponse{
}
struct ApiErrorResponse: Codable, ApiResponse{
}
func getType(_ statusCode: Int) -> ApiResponse.Type{
if statusCode == 200{
return ApiOkResponse.self
} else {
return ApiErrorResponse.self
}
}
func send() throws -> ApiResponse{
let data = Data()
let responseStatusCode = 200
let type = getType(responseStatusCode)
return try JSONDecoder().decode(type, from: data)
}

No value associated with key CodingKeys while trying to get data from GitHub API in Xcode app

So listening to #Larme and #JoakimDanielson (thanks a lot, guys!) I started doing some tasks on URLSessions to actually get the data I am looking for from GitHub API.
The endgame here is to create a mobile GitHub search app for repositories.
I have realised a code from this tutorial:
https://blog.devgenius.io/how-to-make-http-requests-with-urlsession-in-swift-4dced0287d40
Using relevant GitHub API URL. My code looks like this:
import UIKit
class Repository: Codable {
let id: Int
let owner, name, full_name: String
enum CodingKeys: String, CodingKey {
case id = "id"
case owner, name, full_name
}
init(id: Int, owner: String, name: String, fullName: String) {
self.id = id
self.owner = owner
self.name = name
self.full_name = full_name
}
}
(...)
let session = URLSession.shared
let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20")!
let task = session.dataTask(with: url, completionHandler: { data, response, error in
// Check the response
print(response)
// Check if an error occured
if error != nil {
// HERE you can manage the error
print(error)
return
}
// Serialize the data into an object
do {
let json = try JSONDecoder().decode(Repository.self, from: data! )
//try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
} catch {
print("Error during JSON serialization: \(error.localizedDescription)")
print(String(describing:error))
}
})
task.resume()
}
}
The full error text is:
keyNotFound(CodingKeys(stringValue: "id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"id\", intValue: nil) (\"id\").", underlyingError: nil))
I tried removing some values that had this error but the others still threw the same and I used the coding keys I could find in the GitHub docs:
https://docs.github.com/en/rest/reference/search#search-repositories
Please help!
First of all you don't need CodingKeys and the init method.
Second of all use structs, not classes.
If you want to decode the repositories you have to start with the root object, the repositories are in the array for key items
struct Root : Decodable {
let items : [Repository]
}
struct Repository: Decodable {
let id: Int
let name, fullName: String
let owner : Owner
}
struct Owner : Decodable {
let login : String
}
Another issue is that owner is also a Dictionary which becomes another struct.
To get rid of the CodingKeys add the .convertFromSnakeCase strategy which translates full_name into fullName.
let session = URLSession.shared
let url = URL(string: "https://api.github.com/search/repositories?q=CoreData&per_page=20")!
let task = session.dataTask(with: url) { data, response, error in
// Check the response
print(response)
// Check if an error occured
if let error = error {
// HERE you can manage the error
print(error)
return
}
// Serialize the data into an object
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let json = try decoder.decode(Root.self, from: data! )
print(json)
} catch {
print("Error during JSON serialization:", error)
}
}
task.resume()

How can I register protocol types as types involved in JSON decoding?

I'm working on an APIClient that gets JSON from the backend, also in cases of failures.
I'm handling parsing of the 'happy path' differently than service-wide errors.
Consider this:
import UIKit
var greeting = "Hello, playground"
protocol ErrorResponse: Codable {
var asErrors: [Error] { get }
}
class FakeClient {
var responseTypes: [ErrorResponse.Type] = []
func registerResponse(type: ErrorResponse.Type) {
responseTypes.append(type)
}
func parse(responseData: Data = Data()) throws {
let decoder = JSONDecoder()
for type in responseTypes {
if let response = try? decoder.decode(type, from: responseData) {
// SEE ERROR MESSAGE BELOW
// Got an error response
throw response.asErrors.first!
}
}
}
}
And that line "SEE ERROR MESSAGE BELOW" produces the compiler error of:
Cannot convert value of type 'ErrorResponse.Type' to expected argument type 'T.Type'
Generic parameter 'T' could not be inferred
I'm a little confused, perhaps because I'm storing an array of protocol types.
I don't know why the decoder can't determine its type, since ErrorResponse conforms to Codable.
I suppose I need to type erase somehow because it needs to understand what concrete type is actually in use?
Does anyone know the right way to accomplish this?
The solution is to create an error response handler then type erase it. The handler itself determines what type it will want to try to decode, and if it can, it will throw that error. If not, it won't, and the code will use the next handler in the array to try to parse it. If none of them throw, there would be no special error parsed, and one can just fallback to a more generic HTTPError. (this code is used when the status code is 422, typically a validation error, but that's somewhat irrelevant here)
I'll let the code speak for itself:
protocol ErrorResponseHandler {
/// if this handler can parse it, it will throw, if not, nothing will happen.
func decodeData(_ responseData: Data, with decoder: JSONDecoder) throws
}
struct AnyErrorHandler: ErrorResponseHandler {
let wrapped: ErrorResponseHandler
init<T: ErrorResponseHandler>(_ handler: T) {
self.wrapped = handler
}
func decodeData(_ responseData: Data, with decoder: JSONDecoder) throws {
try self.wrapped.decodeData(responseData, with: decoder)
}
}
class FakeClient {
private var errorHandlers: [AnyErrorHandler] = []
func registerErrorResponseHandler<T: ErrorResponseHandler>(_ handler: T) {
errorHandlers.append(AnyErrorHandler(handler))
}
func parse(responseData: Data = Data()) throws {
let decoder = JSONDecoder()
for handler in errorHandlers {
try handler.decodeData(responseData, with: decoder)
}
}
}
// Now to set up some boilerplate to test this out.
enum FakeError: Error {
case justTestingThis
}
struct FakeHandler: ErrorResponseHandler {
func decodeData(_ responseData: Data, with decoder: JSONDecoder) throws {
throw FakeError.justTestingThis
}
}
let client = FakeClient()
client.registerErrorResponseHandler(FakeHandler())
do {
try client.parse(responseData: Data())
} catch let e {
print("I think it worked: \(String(describing: e))")
}

With swifter json response

I am create simple rest api with swift using swifter library
How i can response json data?
import Swifter
let server = HttpServer()
server["/hello"] = { request in
var userList:[Any] = []
for user in users {
var b : [String: Any] = [:]
b["name"] = user.name
b["id"] = user.id
userList.append(b)
}
return .ok(.json(userList))
}
But there is below error message
Serialization error: invalidObject
I check the library source code, and found the error message reason
...
//Library Source code
func content() -> (Int, ((HttpResponseBodyWriter) throws -> Void)?) {
do {
switch self {
case .json(let object):
guard JSONSerialization.isValidJSONObject(object) else {
throw SerializationError.invalidObject
}
let data = try JSONSerialization.data(withJSONObject: object)
return (data.count, {
try $0.write(data)
})
...
So, I need pass guard JSONSerialization.isValidJSONObject(object) else {
also, there is no enough document for the library, How I can fix this problem ?
Use codable and a jsonEncoder to convert the users array to data and then convert them back to a jsonObject and pass it in:
do {
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(users)
let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
return .ok(.json(jsonObject))
} catch {
print(error)
return .internalServerError(.htmlBody("\(error.localizedDescription)"))
}
Note that you should consider returning an error to the caller of the service. I've used .internalServerError but you may consider returning a better error.

Having trouble loading custom data from firebase using swift

I followed the directions on the firebase website and came up with the code below labeled NEW CODE. The error I am getting is:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
I have tried researching about the Result object and this error but have not found anything that would help me directly. I was able to read the data using the old way (which I will post below), but am trying to follow Googles documentation
(https://firebase.google.com/docs/firestore/query-data/get-data)
Any help would be appreciated, thank you!
OLD CODE
func readData(word: String) -> CombinedModel? {
print("reading data")
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
var wordResults: CombinedModel? = nil
docRef.getDocument { (document, error) in
if let e = error {
print("Error loading data: \(e)")
return
} else {
do {
if let resultData = try document?.data(as: CombinedModel.self){
print("Definitions: \(resultData.definitionsArray)")
print("\n Synonyms: \(resultData.synonyms)")
wordResults = resultData
}
} catch {
print("Error decoding: \(error)")
}
}
}
return wordResults
}
NEW CODE
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
print("CombinedModel: \(combinedModel)")
} else {
print("Document does not exist")
}
case .failure(let error):
print("Error decoding city: \(error)")
}
}
}
Here is the codable custom class I created for reading the data.
struct CombinedModel: Codable {
var definitionsArray: [WordModel]
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case definitionsArray
case synonyms
}
}
struct WordModel: Codable {
let id: String
let partOfSpeech: String
let definitions: [String]
let example: [String]
let ipa: String
let audio: String
private enum CodingKeys: String, CodingKey {
case id
case partOfSpeech
case definitions
case example
case ipa
case audio
}
}
struct ThesaurusModel: Codable {
var synonyms: [String]
private enum CodingKeys: String, CodingKey {
case synonyms
}
}
I realized the issue with the Result object was that Xcode was reading it as a structure that I created in a different file, but once I changed the name of that structure(it was named Result previously), it recognized the Result object as the correct object which is of type: enum Result where Failure : Error
Looks like you're getting close - minus the type error. From what I can tell, your error:
Cannot invoke initializer for type 'Result' with an argument list of type '(#escaping () throws -> CombinedModel?)'
Refers to this portion of code:
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
At least, this is the portion of code that looks off to me. Your (document, error) variable as a whole represents your result. You shouldn't need a Result middleman.
The real focus should be around determining whether or not there is an error. If there isn't you should read the document.
A working approach should look something like this:
func newReadData(word: String) -> CombinedModel? {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
guard let fbDoc = document else {
// document is nil, parse the error object and handle it.
}
if let resultData = try fbDoc?.data(as: CombinedModel.self) {
return resultData
} else {
return nil
}
}
}
Let me know how that goes for you! Best of luck.