handling json response Observable swift - swift

I have an application which uses SwiftyJSON and works. How ever, I now want to expand the project and refactor the codes but I am having a bit of issue as I am now switching to Codable and I need to be able to mapJSON from any path and not a hard coded path. Currently my jsonResponse looks like this
/// handle the network response and map to JSON
/// - returns: Observable<JSON>
func handleResponseMapJSON() -> Observable<Result<JSON, ORMError>> {
return self.map { representor in
guard let response = representor as? Moya.Response else {
return .failure(ORMError.ORMNoRepresentor)
}
guard ((200...299) ~= response.statusCode) else {
return .failure(ORMError.ORMNotSuccessfulHTTP)
}
guard let json = JSON.init(rawValue: response.data),
json != JSON.null,
let code = json["code"].int else {
return .failure(ORMError.ORMParseJSONError)
}
guard code == BizStatus.BizSuccess.rawValue else {
let message: String = {
let json = JSON.init(data: response.data)
guard let msg = json["status"].string else { return "" }
return msg
}()
log(message, .error)
return .failure(ORMError.ORMBizError(resultCode: "\(code)", resultMsg: message))
}
return .success(json["result"])
}
}
how do I eliminate the passage of hardcoded json[""] value. Any help is appreciated

I suggest you try something like this:
protocol ResponseType: Codable {
associatedtype ResultType
var status: String { get }
var code: Int { get }
var result: ResultType { get }
}
func handleResponseMap<T, U>(for type: U.Type) -> (Any) -> Result<T, ORMError> where U: ResponseType, T == U.ResultType {
return { representor in
guard let response = representor as? Moya.Response else {
return .failure(.ORMNoRepresentor)
}
guard ((200...299) ~= response.statusCode) else {
return .failure(.ORMNotSuccessfulHTTP)
}
return Result {
try JSONDecoder().decode(U.self, from: response.data)
}
.mapError { _ in ORMError.ORMParseJSONError }
.flatMap { (response) -> Result<T, ORMError> in
guard response.code == BizStatus.BizSuccess.rawValue else {
log(response.status, .error)
return Result.failure(ORMError.ORMBizError(resultCode: "\(response.code)", resultMsg: response.status))
}
return Result.success(response.result)
}
}
}
Then you can map directly to your Codable type:
let result = self.map(handleResponseMap(for: MyResponse.self))
In the above, result will end up being an Observable<Result<ResultType, ORMError>>

I would like to do extension over PrimitiveSequenceType to treat it as Single
import Foundation
import RxSwift
import Moya
public extension PrimitiveSequenceType where TraitType == SingleTrait, ElementType == Response {
func map<T>(_ type: T.Type, using decoder: JSONDecoder? = nil) -> PrimitiveSequence<TraitType, T> where T: Decodable {
return self.map { data -> T in
let decoder = decoder ?? JSONDecoder()
return try decoder.decode(type, from: data.data)
}
}
}
and you can simply use it like:
return PokeAPIEndPoints.shared.provider.rx
.request(.specieDetails(id: specieId))
.filterSuccessfulStatusCodes()
.map(SpecieDetails.self)

Related

Casting Moya Response error to defined type

I am using RxMoya for my networking calls and extending PremitiveSequence and Response so as to handle the error coming back. I declared a struct of Networking error which I could use to get all the error details and as such Pass the error message via the BaseResponse Model. Here is my NetwokingError struct
public struct NetworkingError: Error {
let httpResponse: HTTPURLResponse?
let networkData: Data?
let baseError: Error
}
For my coding, I have extended the primitive sequence as follows
public extension PrimitiveSequence where TraitType == SingleTrait,
ElementType == Response {
func mapObject<T: Codable>(_ type: T.Type, path: String? = nil) -> Single<T> {
return flatMap { response -> Single<T> in
return Single.just(try response.mapObject(type, path: path))
}
}
func mapArray<T: Codable>(_ type: T.Type, path: String? = nil) -> Single<[T]> {
return flatMap { response -> Single<[T]> in
return Single.just(try response.mapArray(type, path: path))
}
}
func filterSuccess() -> Single<E> {
return flatMap { (response) -> Single<E> in
if 200 ... 299 ~= response.statusCode {
return Single.just(response)
}
print("THIS ERROR JSON jsonObject2 xx mm \(response.data)")
do {
let jsonObject2 = try JSONSerialization.jsonObject(with: response.getJsonData(), options: .allowFragments)
print("THIS ERROR JSON jsonObject2 xx \(jsonObject2)")
let jsonObject = try JSONSerialization.jsonObject(with: response.getJsonData(), options: .allowFragments) as? NetworkingError
print("THIS ERROR JSON xx \(jsonObject)")
return Single.error(jsonObject ?? NetworkingError.self as! Error)
}
}
}
}
if I run this code here, The app crashes return Single.error(jsonObject ?? NetworkingError.self as! Error)
in my code, I am passing data like
func postVerifyApp(challenge: Int, identifier: String) -> Observable<AuthResponse> {
return provider.rx.request(.postVerifyApp(challenge: challenge, identifier: identifier))
.filterSuccess()
.mapObject(AuthResponse.self)
.asObservable()
.flatMap({ authResponse -> Observable<AuthResponse> in
return self.sendTokenToServer(authResponse)
})
}
then I am working with this in my presenter class like this
func postVerifyApp(challenge: Int, identifier: String) {
view?.setProgress(enabled: true)
source.postVerifyApp(challenge: challenge, identifier: identifier)
.retry(.delayed(maxCount: 2, time: 2.5), shouldRetry: networkRetryPredicate)
.asSingle()
.subscribe(onSuccess: { [weak self] response in
guard let presenter = self, let view = presenter.view else {return}
view.setProgress(enabled: false)
log(response, .json)
guard let data = response.data else {
return }
view.showVerifySuccess()
}, onError: { [weak self] error in
guard let presenter = self, let view = presenter.view else {return}
print("MESSAGE X \(error.localizedDescription)")
if let error = error as? NetworkingError {
print("MESSAGE X httpResponse \(error.httpResponse)")
}
view.setProgress(enabled: false)
}).disposed(by: disposeBag)
}
I want to be able to pass this Error and extract the error message and passing it to the console.
This is what my base Model looks like
struct ResponseBase<T: Codable>: Codable {
var error: Bool?
var message: String?
var data: T
var isSucessful: Bool {
return error == false
}
}
The expression used to construct the Single.error can not cast as Error. Firstly, you are trying to cast a jsonObject (a Dictionary) as Error. On the right hand, on the ifNull expression, you are trying to cast a metatype (Networking.Type) as an Error.
To solve your casting problem you can use this modified NetworkingError.
public struct NetworkingError: Error {
let httpResponse: HTTPURLResponse?
let networkData: Data?
let baseError: MoyaError
init(_ response:Response) {
self.baseError = MoyaError.statusCode(response)
self.httpResponse = response.response
self.networkData = response.data
}
func getLocalizedDescription() -> String {
return self.baseError.localizedDescription
}
}
Having this, modify the closure in the filterSuccess to create the NetworkingError object, passing it the Response, just like this:
func filterSuccess() -> Single<E> {
return flatMap {
(response) -> Single<E> in
if 200 ... 299 ~= response.statusCode {
return Single.just(response)
} else {
let netError = NetworkingError(response)
return Single.error(netError)
}
}
}
I encourage you to take a look at the MoyaError definition

Cannot convert return expression of type 'Promise<Void>' to return type 'Promise<JSON>' Swift

I am new to PromiseKit and am having trouble getting the value of the Promise. The end goal is to have the async call resolve to an array of JSON. Currently the compiler is complaining that:
Cannot convert return expression of type 'Promise<Void>' to return type 'Promise<JSON>'
I am pretty confused, and haven't been able to find a solution that looks like what I have going. Can anyone see where I am going wrong?
Here is the get method that makes the call to my backend for Json data.
public func get(_ url: URL) -> Promise<JSON> {
return Promise<JSON> { resolver -> Void in
Alamofire.request(url)
.validate()
.responseJSON { response in
switch response.result {
case .success(let json):
let json = JSON()
if let data = response.data {
guard let json = try? JSON(data: data) else {
resolver.reject("Error" as! Error)
return
}
resolver.fulfill(json)
}
case .failure(let error):
resolver.reject(error)
}
}
}
}
Here is the method that uses the above method:
public func fetchAllocations() -> Promise<JSON> {
let url: URL = createAllocationsUrl()
var allocations = JsonArray()
let responsePromise = httpClient.get(url).done { (fetchedJSON) in
let fetchedAlloc: JsonArray = JSON(fetchedJSON).array ?? []
if fetchedAlloc.count > 0 {
let eid = "9b0e33b869"
let previousAlloc = self.store.get(eid)
if let previousAllocations = previousAlloc {
if previousAllocations.count > 0 {
allocations = Allocations.reconcileAllocations(previousAllocations, allocations)
}
}
self.store.set(eid, val: allocations)
self.allocationStatus = AllocationStatus.RETRIEVED
if self.confirmationSandbagged {
self.eventEmitter.confirm(allocations)
}
}
}
return responsePromise
}
And in the button of my view controller, I am executing that function as so:
// This should be a Promise<JSON> that I can resolve and fulfill
let promise = alloc.fetchAllocations().done { (json) in
self.allocations = [json]
}
let eid = "9b0e33b869"
let cached = store.get(eid)
print("YOUR FETCHED ALLOCATION: \(String(describing: self.allocations))")
print("YOUR CACHED ALLOCATION: \(String(describing: cached))")
When I get to my print statements, I have nil each time.
public func fetchAllocations() -> Promise<[JSON]> {
return Promise { resolve in
let url = self.createAllocationsUrl()
let strPromise = HttpClient.get(url: url).done { (stringJSON) in
var allocations = JSON.init(parseJSON: stringJSON).arrayValue
let previous = self.store.get(self.participant.getUserId())
if let prevAlloc = previous {
if Allocator.allocationsNotEmpty(allocations: prevAlloc) {
allocations = Allocations.reconcileAllocations(previousAllocations: prevAlloc, currentAllocations: allocations)
}
}
self.store.set(self.participant.getUserId(), val: allocations)
self.allocationStatus = AllocationStatus.RETRIEVED
if (self.confirmationSandbagged) {
self.eventEmitter.confirm(allocations: allocations)
}
if self.contaminationSandbagged {
self.eventEmitter.contaminate(allocations: allocations)
}
resolve.fulfill(allocations)
do {
try self.execution.executeWithAllocation(rawAllocations: allocations)
} catch let err {
let message = "There was an error executing with allocations. \(err.localizedDescription)"
Log.logger.log(.error, message: message)
}
}
}
}

'result' is inaccessible due to 'internal' protection level

I want to create custom framework for universal API request using URLSession. So I have used this link. I will be using this project as a custom framework. So to use that I have changed its access specifier by open .And using thislink I have imported it in my project. And I have done the following code to call post request
import iOSCoreFramework
func callBySpeedyNetworking2() {
let trylogin = login(username: "****", password: "***")
SpeedyNetworking.removeToken()
SpeedyNetworking.postUrl(url: URL(string: GlobalConstants.loginFullURL), model: trylogin) { (response) in
if !response.success {
// show a network error
print("network error ",response.error)
return
}
// successful
print("RESPONSE 1 ------------> ")
dump(response.result(model: ModelResponse.self))
dump(response.jsonResults(model: ModelResponse.self))
}
}
But it's giving me an error for 'success', 'error' and on the following lines:
dump(response.result(model: ModelResponse.self))
dump(response.jsonResults(model: ModelResponse.self))
From various links I have made changes in SpeedyResponse class by the following
public class SpeedyResponse {
public var success : Bool!
public var statusCode = 0
public var error: Error?
public var data: Data?
public init (success : Bool, statusCode : Int,error : Error, data : Data){
self.success = success
self.statusCode = statusCode
self.error = error
self.data = data
}
public init(data: Data?, response: URLResponse?, error: Error?) {
self.error = error
self.data = data
if let httpResponse = response as? HTTPURLResponse {
statusCode = httpResponse.statusCode
}
success = statusCode == 200 && error == nil && data != nil ? true : false
}
public func jsonResults<T>(model: T.Type) -> T? {
if !success { return nil }
guard let responseData = data else { return nil }
do {
return try JSONSerialization.jsonObject(with: responseData) as? T
} catch {
return nil
}
}
public func result<T: Decodable>(model: T.Type) -> T? {
if !success { return nil }
guard let responseData = data else { return nil }
do {
return try JSONDecoder().decode(model, from: responseData)
} catch {
return nil
}
}
}
But still it wasn't fixed.
The underlying problem here is inside your struct all variables and methods are declared automatically with its scope as internal. So when you create a type like this:
public class Human {
let foo: String
let bar: String
}
You will not be able to access both foo and bar because they are actually declared as:
public class Human {
internal let foo: String
internal let bar: String
}
To fix this just add the access modifier to public.
In that sense your new model should look like this
public class SpeedyResponse {
public var success: Bool!
public var statusCode = 0
public var error: Error?
public var data: Data?
public init(data: Data?, response: URLResponse?, error: Error?) {
self.error = error
self.data = data
if let httpResponse = response as? HTTPURLResponse {
statusCode = httpResponse.statusCode
}
success = statusCode == 200 && error == nil && data != nil ? true : false
}
public func jsonResults<T>(model: T.Type) -> T? {
if !success { return nil }
guard let responseData = data else { return nil }
do {
return try JSONSerialization.jsonObject(with: responseData) as? T
} catch {
return nil
}
}
public func result<T: Decodable>(model: T.Type) -> T? {
if !success { return nil }
guard let responseData = data else { return nil }
do {
return try JSONDecoder().decode(model, from: responseData)
} catch {
return nil
}
}
}

Using decoded data from an API into an algorithm

I successfully fetched and decoded data from an API and now have access to all the data I need to be used in the algorithm I want to write in my App.
The issue is that I don't know how to access this data after I decoded it, I can print it immediately after it's decoded but I have no idea how to use it in another function or place in my app.
Here is my Playground:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError : Error {
case FoundNil(String)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
print(marketData.data[0].open)
print(marketData.data[1].open)
print("Average=", (marketData.data[0].open + marketData.data[1].open) / 2)
//completion(marketData, nil)
throw MyError.FoundNil("data")
}
} catch {
print(error)
}
}
task.resume()
}
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
How can I use .data[0], .data[1], ..., somewhere else?
You data will be available in your fecthData() call. Probably what you want is your items variable, where you're printing it. But make sure to call the completion in your fetchData implementation.
WARNING: Untested code.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case FoundNil(String)
case DecodingData(Data)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, MyError.FoundNil("data"))
}
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
completion(marketData, nil)
} else {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
} catch {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
}
task.resume()
}
fetchData() { items, error in
if let error == error {
switch(error) {
case .foundNil(let whatsNil):
print("Something is nil: \(whatsNil)")
case .decodingData(let data):
print("Error decoding: \(data)")
}
} else {
if let items = items {
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
print(items)
} else {
print("No items to show!")
}
}
}
I don't understand what is your real issue, because you have written everything you need here, but as far I understand , to pass data
just uncomment this line completion(marketData, nil)
and in
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
items is an object of your struct Response. You can pass this anywhere in your other class , by just creating an another variable like:
var items : Response!
for example :
class SomeOtherClass : NSObject{
var items : Response!
func printSomeData()
{
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
}
}
and in fetchData method write this:
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let otherObject = SomeOtherClass()
otherObject.items = items
otherObject.printSomeData()
}

Sorting JSON when using Alamofire and SwiftyJSON

Morning all,
I've been following along the examples in the excellent iOS Apps With REST APIs book and as a result using Alamofire and SwiftyJSON. One thing I don't see mentioned in the book is how you would sort the incoming json objects into a specific order, eg. date. The code I've pasted below works fine for pulling in the json objects however as far as I can tell, they're automatically ordered by created_by. I'd like to sort by a different order, lets say my class was called Vegetable and had a name attribute so that I could sort by something like:
.sort { $0.name < $1.name }
I'll start with the Vegetable class in Vegetable.swift
class Vegetable: ResponseJSONObjectSerializable {
var id: Int?
var name : String?
var date: NSDate?
}
Inside my JSONSerializer file I have the following, I'm not sure I'd wish to change the order directly in here as I'd prefer some more flexibility with each call.
public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Array could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let result = JSONResponseSerializer.serializeResponse(request, response, responseData, error)
switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
var objects: [T] = []
for (_, item) in json {
if let object = T(json: item) {
objects.append(object)
}
}
return .Success(objects)
case .Failure(let error):
return .Failure(error)
}
}
return response(responseSerializer: serializer, completionHandler: completionHandler)
}
Then, in my APIManager I have the following function
func getAllVegetables(completionHandler: (Result<[Vegetable], NSError>) -> Void) {
Alamofire.request(VegetableRouter.GetVegetables())
.responseArray { (response:Response<[Vegetable], NSError>) in
completionHandler(response.result)
}
}
Finally, populate my tableview I have:
func loadVegetables() {
self.isLoading = true
VegetablesAPIManager.sharedInstance.getAllVegetables() {
result in
self.isLoading = false
if self.refreshControl.refreshing {
self.refreshControl.endRefreshing()
}
guard result.error == nil else {
print(result.error)
// TODO: Display Error
return
}
if let fetchedVegetables = result.value {
self.vegetables = fetchedVegetables
for vegetable in fetchedVegetables {
// Nothing here at the moment
}
}
self.tableView.reloadData()
}
}
I appreciate any help I can get with this, Thanks!
Since you have a NSDate property, you can sort with the compare method of NSDate.
let sorted = result.value.sort { $0.date.compare($1.date) == .OrderedAscending }