Transforming Alamofire response when using Codable and Combine - swift

I want to use Alamofire to query my backend, encode the response using Alamofire's built-in Codable parsing and then publish an extract from the resulting Struct to be consumed by the caller of my API class. Say I have some JSON data from my backend (simplified, but shows the structure):
{
"total": 123,
"results": [
{"foo" : "bar"},
{"foo" : "baz"}
]
}
and the associated Codable Structs
struct MyServerData: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let foo: String
}
I can get, parse, publish, and subscribe all fine with the following:
func myAPI() -> DataResponsePublisher<MyServerData> {
return AF.request("https://backend/path")
.validate()
.publishDecodable(type: MyServerData.self)
}
myAPI()
.sink { response in /* Do stuff, e.g. update #State */ }
What I'd like to do is to publish just the [Result] array. What's the correct approach to this? Should I use .responseDecodable() and create a new publisher (somehow - .map()?) that returns a [Result].publisher?
While I think I understand the reactive/stream based principles my Combine-fu is still weak and I don't have a clear handle on the transformation of one publisher into another (I'm guessing?)
Thanks in advance!

In addition to using Combine API like map, Alamofire offers two publishers on DataResponsePublisher itself.
.result() extracts the Result from the DataResponse and creates an AnyPublisher<Result<Value, AFError>, Never>.
.value() extracts the Value from the DataResponse and creates a failable publisher, AnyPublisher<Value, AFError>.
So depending on what kind of error handling you want, this could be as simple as:
...
.publishDecodable(...)
.value()
.map(\.results)

Related

Swift/Alamofire 5 ResponseSerializer generic stuct decode

I'm trying to serialize some JSON data that is coming from an API endpoint, and because all of the endpoints would have the same structure I thought it would be best for me to implement something that doesn't need to get repeated again in code.
JSON response 1:
{
"code":1100,
"message":"Successfully created application",
"data":{
"key":116541
}
}
JSON response 2:
{
"code":1101,
"message":"Successfully retrived",
"data":{
"id":116541,
"name":"hallow"
}
}
only data changes depending on the API endpoint. They would always come in this structure tho.
struct RawResponse<T: Decodable>: Decodable {
let code: Int
let message: String
let data: T
}
This is where I don't know how to decode RawResponse using DecodableResponseSerializer from Alamofire.
final class CustomDecodeableResponseSerializer<T: Decodable>: ResponseSerializer {
//...decoder setup
//don't work, missing "T" because it's defined in the struct don't know where to add this
private lazy var successSerializer = DecodableResponseSerializer<RawResponse>(decoder: decoder)
//This works as it's just pure struct without generic
private lazy var errorSerializer = DecodableResponseSerializer<APIError>(decoder: decoder)
//...public func serialize stuff
}
This would not work, because it's asking for that "T" defined in the struct, and "T" is passed in through an extension to DataRequest
#discardableResult
func responseCustom<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: #escaping (Result<T, APIError>) -> Void) -> Self {
return response(queue: .main, responseSerializer: CustomDecodeableResponseSerializer<T>()) { response in
switch response.result {
case .success(let result):
completionHandler(result)
case .failure(let error):
completionHandler(.failure(APIError(code: -1, message: error.localizedDescription)))
}
}
}
So it can be called like this:
User is another struct for JSON "data" field to be decoded as
session.request()
.validate()
.responseCustom(of: User.self){(response) in
//do stuff
}
I hope this makes sense...I know that if I just pass T instead of making a raw response struct, I can just repeat code and message in every response struct, and it will work. But I'm seeking to not repeat code and message throughout every response struct. Or is there is a simpler way to achieve this?
You don't have it in your code, but make sure your CustomDecodeableResponseSerializer is returning RawResponse<T> from its serialize method. Then you can make sure your decoder for the success case is DecodableResponseSerializer<RawResponse<T>>(decoder: decoder).
Also, there's no reason to use DispatchQueue.global(). That parameter just controls where the completion handler is called, the actual serialization work is always performed in the background.

How to design Complications for watchOS 6 independent app?

I have an independent application on watchOS 6 and in my app I am using the Firestore REST API to show the data to the user using URLSession.
Since the Cloud Firestore REST API returns a JSON string, in order to process the data, I have created nested structs. Example: To access the 'title' in a particular response, I do something like this: response.mapValue.fields.title.stringValue.
The app works fine for now. In the long run I plan on creating the URLSessions as background tasks. Right now, I am calling URLSession every time the view is rendered by using .onAppear(functionToUseURLSession())) on my data's List view.
Now the next thing I want to implement is the complication for this particular app.
I am new to Swift and SwiftUI and am having the hardest time just getting started. All the tutorials I've been through online use WKExtensionDelegate to get the data models for the complication.
But in my case, I don't even have a data model! I just have structs calling other structs in a nested fashion in order to process the JSON response I get from the API call.
Any help allowing me to understand this is highly appreciated!
Here is some code to better understand the structure of the App:
struct Firebase: Codable {
var name: String?
var createTime, updateTime: String?
var fields: FirebaseFields
}
Now, FirebaseFields is also another struct:
struct FirebaseFields: Codable {
var emailID, lastName, firstName: String?
var goalsRoutines: GoalsRoutines
enum CodingKeys: String, CodingKey {
case emailID = "email_id"
case lastName = "last_name"
case firstName = "first_name"
}
}
Similarly, GoalsRoutines is also a struct...
As mentioned above, I have made these structs to follow the exact structure of the JSON object is get in response from Firebase API. So I access fields like: Firebase.FirebaseFields.GoalsAndRoutines.whatever.is.my.firebase.schema
My GoalsView.swift is:
var body: some View {
List{
...
...
}.onAppear{
FirebaseServices.getFirebaseData() {
(data) in self.data = data
}
}
}
And finally, in FirebaseServices.swift, func getFirebaseData is:
func getFirebaseData(completion: #escaping ([Value]) -> ()) {
guard let url = URL(string: "FIRESTORE URL HERE") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let data = try! JSONDecoder().decode(Firebase.self, from: data!)
DispatchQueue.main.async {
completion(data.fields.goalsRoutines.arrayValue.values)
}
}.resume()
}
So this is how I currently get the data and show it on the app.
I am really lost on how to do the same on my Complication.
Any help is really appreciated.

How to add Dynamic key name and value in Encodable(Swift)?

I have a very simple request:
{"token": "abcd", "key": "value" }
I'm trying to add this request as an Encodable. Now, here the issue arises that the key name can be anything like "123", "311", the type of the key will be String, but it's name is dynamic. How can I add dynamic names in Encodable?
struct Answers: Encodable {
let token: String
let key: String
}
I tried using generics, but it didn't work. Any one have any idea?
I don't think Codable allows that kind of functionality currently. You can't create a Codable type with dynamic keys as of now.
Alternatively, if this is the model you're using, you can simply create a Dictionary from it and then encode it using JSONEncoder().
Example:
let dict = ["token": "abcd", "1234": "value"]
do {
let response = try JSONEncoder().encode(dict)
print(response)
} catch {
print(error)
}
Rob,
If you could change the JSON response you can use something like that {token: "AAA", data: {"key":"123"}}.
So you can create
struct Response<DataType: Codable>: Codable {
let token: String
let data: DataType
}
With this Struct, you can pass many combinations of dynamic values.
My solution doesn't work with your actual data, but maybe you can talk with the team about API and maybe change the data.

Vapor 3 API: embed Future<Model> in response object

Suppose I have a model called Estimate. I have a Vapor 3 API that I want to return a list of these models, filtered by query parameters. Doing so currently returns a Future<[Estimate]>, which results in the API returning JSON that looks like this:
[{estimate object}, {estimate object}, ...]
Instead, I'd like make it return something this:
{"estimates": [{estimate object}, {estimate object}, ...]}
So, the same thing as before, but wrapped in a JSON object with a single key, "estimates".
According to the documentation, any time I want to return something non-default, I should make a new type for it; this suggests to me I should create a type like:
final class EstimatesResponse: Codable {
var estimates: [Estimate]?
}
However, after filtering I get a Future<[Estimate]> and NOT a pure [Estimate] array, meaning that I couldn't assign it to my EstimatesResponse estimates property. It seems weird to make the type of estimates be Future<[Estimate]>, and I'm not sure how that'd turn out.
How, then, can I return JSON of the correct format?
First, you need to create Codable object, I prefer struct as below. Must implement protocol Content for routing.
struct EstimatesResponse: Codable {
var estimates: [Estimate]
}
extension EstimatesResponse: Content { }
I assumed that you are using a controller and inside the controller, you can use the following pseudo-code. Adjust your code so that val is Future<[Estimate]>, then use flatmap/map to get [Estimate].
func getEstimates(_ req: Request) throws -> Future<EstimatesResponse> {
let val = Estimate.query(on: req).all()
return val.flatMap { model in
let all = EstimatesResponse(estimates: model)
return Future.map(on: req) {return all }
}
}

Alamofire POST request replacing characters in output

I am making this request:
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
print("Result: \(response.result.value)"
do {
self.list = try JSONDecoder().decode([list].self, from: result!) for event in self.lists {
print(event.title," : ",event.description)
}
} catch let parseError as NSError {
print("JSON Error \(parseError.localizedDescription)")
}
}
Data that ought to look like this (JSON?) - Postman output, all fields not included herein:
{
"start": "2016-02-01 11:30:00",
"end": "2016-02-01 14:42:24",
"id": 3192,
"ownership": false,
}
prints out looking like this in XCode:
{
start = "2016-02-01 11:30:00";
end = "2016-04-14 20:30:00";
"id" = 3192;
ownership = 0;
}
Result : I am not able to parse this using JSONDecoder, error:
"The data couldn’t be read because it isn’t in the correct format".
Newbie to Swift ... so, thanks in advance for the help!
Edit: Edited for clarity with more information. Thanks again!
Alamofire is not "replacing characters in output", it is giving you a different object than the one you expect. If you print out the type of your response.result you should be surprised by the NSDictionary you are likely to get at that point. Our trusted friend print(...) is nice enough to turn this into a String representation of whatever you pass it, but you are not likely to be able to parse this using JSONDecoder since it is not Data (which is what the decoder is expecting).
As I said before: use responseString in order to get the response and turn it into the appropriate Data for parsing using JSONDecoder. In order to be able to control this process properly you want to include your Codable derivative into the question and you are likely to set the date parsing strategy on the JSONDecoder.
Without your struct and some properly formatted JSON from your response (well, Postman will do if it is reasonably complete) we are unlikely to be able to help you any further.
P.S.: It is not an entirely good idea to change your question completely through an edit. You might be better of posting a new question and leaving a comment with a pointer to it on the old one so people revisiting it may be lead to the right place. If you update your question you should usually leave the old one intact and amend it with additional information in order to keep the existing discussion relevant.
As workaround you can just add CodingKey to decoded struct.
Just add to your struct/class
private enum CodingKeys: String, CodingKey {
case event_id = "id"
}
Please refer to https://benscheirman.com/2017/06/swift-json/
I can suggest the following solution:
Firstly you need a pojo class to refer your json object. Easiest way
that I know is the library called SwiftyJSON
(https://github.com/SwiftyJSON/SwiftyJSON) firstly you can add this
library to your project. Then you can create the following pojo class
for your output (optional: You can also install
SwiftyJSONAccelarator(https://github.com/insanoid/SwiftyJSONAccelerator)
to generate pojo classes using json outputs.):
import Foundation
import SwiftyJSON
public class MyOutput: NSObject {
// MARK: Declaration for string constants to be used to decode and also serialize.
internal let kMyOutputEndKey: String = "end"
internal let kMyOutputInternalIdentifierKey: String = "id"
internal let kMyOutputOwnershipKey: String = "ownership"
internal let kMyOutputStartKey: String = "start"
// MARK: Properties
public var end: String?
public var internalIdentifier: Int?
public var ownership: Bool = false
public var start: String?
// MARK: SwiftyJSON Initalizers
/**
Initates the class based on the object
- parameter object: The object of either Dictionary or Array kind that was passed.
- returns: An initalized instance of the class.
*/
convenience public init(object: AnyObject) {
self.init(json: JSON(object))
}
/**
Initates the class based on the JSON that was passed.
- parameter json: JSON object from SwiftyJSON.
- returns: An initalized instance of the class.
*/
public init(json: JSON) {
end = json[kMyOutputEndKey].string
internalIdentifier = json[kMyOutputInternalIdentifierKey].int
ownership = json[kMyOutputOwnershipKey].boolValue
start = json[kMyOutputStartKey].string
}
}
After that after calling url with Alomofire and getting response, you
can simply map the output to your pojo class. Finally, you can use any
field in your class(myOutput in my example):
Alamofire.request(path,method:.post, parameters:params, encoding: JSONEncoding.default,headers:headers).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
let myOutput = MyOutput.init(json: json)
//use myOutput class for your needs
case .failure( _):
self.createNetworkErrorPopup()
}
}