Swift Combine: Cannot refactor repetitive code - swift

My API returns this format, where data can contain all kinds of responses.
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
I have made a Codable Response type with a generic for the optional data, of which we do not know the type.
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
I am now trying to write API convenience methods as concisely as possible. So I have a function to return a generic publisher that I can use for all these responses, i.e. one that pre-parses the response and catches any errors upfront.
First, I get a dataTaskPublisher that processes the parameter inputs, if any. Endpoint is just a convenience String enum for my endpoints, Method is similar. MyRequest returns a URLRequest with some necessary headers etc.
Notice the way I define the parameters: params: [String:T]. This is standard JSON so it could be strings, numbers etc.
It seems this T is the problem somehow..
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST && params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
Next, I am parsing the response.
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(\.data)
.mapError { MyError.network(description: "\($0)")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "\($0)")} //(2)
.tryMap {
if $0.status > 204 {
throw MyError.network(description: "\($0.status): \($0.error!)")
}
else {
return $0 // returns a MyResponse
}
}
.mapError { $0 as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "\(err)"))
.eraseToAnyPublisher()
}
}
Now I can write an endpoint method easily. Here are two examples.
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
and
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
All this is working well. Please note that each time, I have to specify my exact return type twice. I think I can live with that.
I should be able to simplify this so that I do not have to repeat the same three operators (map, mapError, receive) in exactly the same way each time.
But when I insert .map(\.data!) at the location //(1) above I get the error Generic parameter T could not be inferred. at the location //(2).
This is really confusing. Why does the generic type in the input parameters play any role here? This must be related to the call to the .decode operator just above, where the generic in question is called R, not T.
Can you explain this? How can I refactor these operators upstream?

This code has a number of small problems. You're right that one is [String: T]. That means that for a given set of parameters, all the values must be of the same type. That's not "JSON." This will accept a [String: String] or a [String: Int], but you can't have both Int and String values in the same dictionary if you do it this way. And it will also accept [String: Document], and it doesn't seem like you really want that.
I'd recommend switching this to just Encodable, which would allow you to pass structs if that were convenient, or Dictionaries if that were convenient:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
Then modify your params.count to check for nil instead.
Note that I didn't make params = nil a default parameter. That's because this would recreate a second problem you have. T (and Params) can't be inferred in the default case. For = [:], what is T? Swift has to know, even though it's empty. So instead of a default, you use an overload:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
Now, when you don't pass any parameters, Params automatically becomes String.
So now your code is fine, and you don't need the as at the end
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
Now, that .map(\.data!) makes me sad. If you get back corrupt data from the server, the app will crash. There are lots of good reasons to crash apps; bad server data is never one of them. But fixing that isn't really related to this question (and is a little bit complicated because Failure types other than Error make things hard currently), so I'll leave it for now. My general recommendation is to use Error as your Failure type, and allow unexpected errors to just bubble up rather than wrapping them in an .undefined case. If you need some catch-all "other" anyway, you might as well do that with types ("is") rather than an extra enum case (which just moves the "is" to a switch). At the very least, I would move the Error->MyError mapping as late as possible, which will make handling this much easier.
One more tweak to make later things a little more general, I suspect MyResponse only needs to be Decodable, not Encodable (the rest of this works either way, but it makes it a little more flexible):
struct MyResponse<T: Decodable>: Decodable { ... }
And to your original question of how to make this reusable, you can now pull out a generic function:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(\.data!)
.mapError { MyError.network(description: $0.errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}

Related

How to Implement API for both Success and Failure Response with Combine Swift

EDIT:
I am trying my level best to make my question simpler,
here what I am trying to get a solution for is, I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
also if my data is wrong the API will fail and it will produce an error response which is a different struct.
with the use of combine, I only could decode a single struct type.
SO how do I make my decode accept any type?
Generics is one way I hoped to solve but here the protocol that I need to implement is an issue I believe restricting me from using generics.
thanks for giving it a try.
// MARK: - ResponseStruct Model
struct ResponseStruct: Codable {
}
//MARK: -Protocol
public protocol RequestProtocol{
associatedtype ResponseOutput
func fetchFunction() -> AnyPublisher<ResponseOutput, Error>
}
//Mark: - Implementation
struct RequestStruct: Codable, RequestProtocol {
typealias ResponseOutput = ResponseStruct
func fetchFunction() -> AnyPublisher<ResponseOutput, Error> {
let networkManager = NetworkManager()
do {
return try networkManager.apiCall(url: url, method: .post, body: JSONEncoder().encode(self))
.decode(type: ResponseStruct.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} catch {
}
}
}
Above this is the code, and this is fine if the API call works but if the call fails I will get an error response, so how to decode that struct in a combined way? I don't want to write another call for that and I am hoping to get something to do with Failure in the combine. or CAN I MAKE THE associated type (see protocol) generic?
I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.
Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.
I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
If my data is wrong the API will fail and it will produce an error response which is a different struct.
So we need two structs. One for success and one for failure. I'll make some up:
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
Based on that, request to the network can:
Return success data to decode into SuccessfulResult
Return failure data to decode into FailedResult
Fail because of a low-level error (e.g. The network is unreachable).
Let's create a type for "The network worked just fine and gave me either success data or failure data":
enum NetworkData {
case success(Data)
case failure(Data)
}
I'll use Error for low-level errors.
With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>
But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.
We need a data type to represent the parsed variant of NetworkData:
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>
We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.
One way to write that function is:
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:
import Foundation
import Combine
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
enum NetworkData {
case success(Data)
case failure(Data)
}
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
let successfulBody = """
{ "interestingText" : "This is interesting!" }
""".data(using: .utf8)!
let failedBody = """
{
"errorCode" : -4242,
"failureReason" : "Bogus! Stuff went wrong."
}
""".data(using: .utf8)!
return Future<NetworkData,Error> { fulfill in
let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!
DispatchQueue.global(qos: .background).asyncAfter(
deadline: .now() + .milliseconds(delay)) {
if(shouldSucceed) {
fulfill(.success(NetworkData.success(successfulBody)))
} else {
fulfill(.success(NetworkData.failure(failedBody)))
}
}
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
successfulRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Success Result \($0)") })
.store(in: &subscriptions)
let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
failedRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Failure Result \($0)") })
.store(in: &subscriptions)

AWS get response as data, not JSON (for using with Codable)

Not familiar enough with AWS, but I have some Codable models I need to initialize from AWS. I'm getting JSON result from AWSTask.result (which is AnyObject). I'm trying to avoid creating Data from Dictionaty and back to a struct (to be able to use Codable).
I tied to use AWSNetworkingHTTPResponseInterceptor, but it was never got called and I couldn't find any example of using it.
self.getGatewayClient { (apiGatewayClient: AWSAPIGatewayClient?) in
let queryParameters = ...
let headerParameters = ...
apiGatewayClient?.invokeHTTPRequest(
"GET",
urlString: "/path",
pathParameters: [:],
queryParameters: queryParameters,
headerParameters: headerParameters,
body: nil,
responseClass: nil
).continueWith { (task: AWSTask<AnyObject>) -> Any? in
if let data = task... { // Get response as Data type??
}
if let result = task.result as? [String: Any] {
// Thanks, but I have a Codable, so I'll just take the data thank you.
}
return task
}
}
AWS's AWSAPIGatewayClient has two functions, one is: invokeHTTPRequest (which was what was used). There is another one called invoke, which returns data. It takes a AWSAPIGatewayRequest request:
func someTask(completion: #escaping (String?) -> ()) {
self.getGatewayClient { (apiGatewayClient: AWSAPIGatewayClient?) in
let request = AWSAPIGatewayRequest(httpMethod: "GET",
urlString: "/path",
queryParameters: queryParameters,
headerParameters: headerParameters,
httpBody: nil)
apiGatewayClient?.invoke(request).continueOnSuccessWith { response in
if let data = response.result?.responseData {
// Init Codable using data
}
}
}
}

Swift Combine - Accessing separate lists of publishers

I have two lists of URLs that return some links to images.
The lists are passed into a future like
static func loadRecentEpisodeImagesFuture(request: [URL]) -> AnyPublisher<[RecentEpisodeImages], Never> {
return Future { promise in
print(request)
networkAPI.recentEpisodeImages(url: request)
.sink(receiveCompletion: { _ in },
receiveValue: { recentEpisodeImages in
promise(.success(recentEpisodeImages))
})
.store(in: &recentImagesSubscription)
}
.eraseToAnyPublisher()
}
Which calls:
/// Get a list of image sizes associated with a featured episode .
func featuredEpisodeImages(featuredUrl: [URL]) -> AnyPublisher<[FeaturedEpisodeImages], Error> {
let featuredEpisodesImages = featuredUrl.map { (featuredUrl) -> AnyPublisher<FeaturedEpisodeImages, Error> in
return URLSession.shared
.dataTaskPublisher(for: featuredUrl)
.map(\.data)
.decode(type: FeaturedEpisodeImages.self, decoder: decoder)
.receive(on: networkApiQueue)
.catch { _ in Empty<FeaturedEpisodeImages, Error>() }
.print("###Featured###")
.eraseToAnyPublisher()
}
return Publishers.MergeMany(featuredEpisodesImages).collect().eraseToAnyPublisher()
}
/// Get a list of image sizes associated with a recent episode .
func recentEpisodeImages(recentUrl: [URL]) -> AnyPublisher<[RecentEpisodeImages], Error> {
let recentEpisodesImages = recentUrl.map { (recentUrl) -> AnyPublisher<RecentEpisodeImages, Error> in
return URLSession.shared
.dataTaskPublisher(for: recentUrl)
.map(\.data)
.decode(type: RecentEpisodeImages.self, decoder: decoder)
.receive(on: networkApiQueue)
.catch { _ in Empty<RecentEpisodeImages, Error>() }
.print("###Recent###")
.eraseToAnyPublisher()
}
return Publishers.MergeMany(recentEpisodesImages).collect().eraseToAnyPublisher()
}
and is attached to the app state:
/// Takes an action and returns a future mapped to another action.
static func recentEpisodeImages(action: RequestRecentEpisodeImages) -> AnyPublisher<Action, Never> {
return loadRecentEpisodeImagesFuture(request: action.request)
.receive(on: networkApiQueue)
.map({ images in ResponseRecentEpisodeImages(response: images) })
.replaceError(with: RequestFailed())
.eraseToAnyPublisher()
}
It seems that:
return Publishers.MergeMany(recentEpisodes).collect().eraseToAnyPublisher()
doesn't give me a reliable downstream value as whichever response finishes last overwrites the earlier response.
I am able to log the responses of both series of requests. Both are processing the correct arrays and returning the proper json.
I would like something like:
return recentEpisodeImages
but currently this gives me the error
Cannot convert return expression of type '[AnyPublisher<RecentEpisodeImages, Error>]' to return type 'AnyPublisher<[RecentEpisodeImages], Error>'
How can I collect the values of the inner publisher and return them as
AnyPublisher<[RecentEpisodeImages], Error>
Presuming that the question is how to turn an array of URLs into an array of what you get when you download and process the data from those URLs, the answer is: turn the array into a sequence publisher, process each URL by way of flatMap, and collect the result.
Here, for instance, is how to turn an array of URLs representing images into an array of the actual images (not identically what you're trying to do, but probably pretty close):
func publisherOfArrayOfImages(urls:[URL]) -> AnyPublisher<[UIImage],Error> {
urls.publisher
.flatMap { (url:URL) -> AnyPublisher<UIImage,Error> in
return URLSession.shared.dataTaskPublisher(for: url)
.compactMap { UIImage(data: $0.0) }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.collect().eraseToAnyPublisher()
}
And here's how to test it:
let urls = [
URL(string:"http://www.apeth.com/pep/moe.jpg")!,
URL(string:"http://www.apeth.com/pep/manny.jpg")!,
URL(string:"http://www.apeth.com/pep/jack.jpg")!,
]
let pub = publisherOfArrayOfImages(urls:urls)
pub.sink { print($0) }
receiveValue: { print($0) }
.store(in: &storage)
You'll see that what pops out the bottom of the pipeline is an array of three images, corresponding to the array of three URLs we started with.
(Note, please, that the order of the resulting array is random. We fetched the images asynchronously, so the results arrive back at our machine in whatever order they please. There are ways around that problem, but it is not what you asked about.)

Alamofire retry request - reactive way

I was looking at those two:
http://sapandiwakar.in/refresh-oauth-tokens-using-moya-rxswift/
Using retryWhen to update tokens based on http error code
And trying to create similiar thing, but without Moya, using Alamofire + RxSwift.
First of all is obviously where should I stick this, since my implementation is divided into a couple smaller parts.
First of all I have my custom method for generating reactive requests:
static func rx_request<T>(requestConvertible: URLRequestConvertible, completion: (Request) -> Observable<T> ) -> Observable<T> {
let manager: Manager = Manager.sharedInstance
return manager
.rx_request { manager -> Request in
return Alamofire.request(requestConvertible)
}
.flatMap { request -> Observable<T> in
return completion(request)
}
.shareReplay(1)
}
Which is later used by specific Requests convenience classes. For example my UserRequests has this private extension to extract some common code from it's methods:
private extension Request {
func rx_userRequest() -> Observable<User> {
return self
.validate()
.rx_responseJSON()
.flatMap{ (request, json) -> Observable<User> in
guard
let dict = json as? [ String: AnyObject ],
let parsedUser: User = try? Unbox(dict) else {
return Observable.error(RequestError.ParsingError)
}
return Observable.just(parsedUser)
}
.rx_storeCredentials()
}
}
Because of how things looks like I wonder whare's the right place to put a retry method and also how to implement it? Because depending on the location I can get different input arguments.
The retry code has to go after the first try, which is rx_responseJSON so the way you have things setup now, it must go between that and the flatMap after it.

Is there anyway I can avoid the use of what appears to be some "boilerplate" code?

I am starting with a function that looks like this:
func getUser(command: APICommand, id: Int, handler: (apiResponse: APIResponse<User>) -> Void ) {
let url = apiPath + "users/\(id)"
Alamofire.request(.GET, url, parameters: requestParameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:User?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
if AlamofireAPIRequestRepository.resultIsRetryableError(e, command: command) {
println("retrying request")
command.retry()
} else {
handler(apiResponse: apiResponse)
}
}
}
I am going to have a number of functions that look very similar such as, getUserList() for example.
Looking at this I realized the entire Alamofire.request call is going to be pretty much boiler plate code. The only difference will be the Type of argument c passed into the closure that gets called by the responseObject() method. In this case it is User? , in the getUserList() method it will be UserList?
Is there any way I can make this more generic and avoid what appears to be just "boilerplate" code?
I Here is what I have tried.
func alamofireGetRequest<T>(url: URLStringConvertible, parameters: [String: AnyObject]?,
command: APICommand, handler: (apiResponse: APIResponse<T>) -> Void) -> Void {
Alamofire.request(.GET, url, parameters: parameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:T?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
if AlamofireAPIRequestRepository.resultIsRetryableError(e, command: command) {
println("retrying request")
command.retry()
} else {
handler(apiResponse: apiResponse)
}
}
}
but the compiler complains with:
Cannot invoke 'responseObject' with an argument list of type
'((NSURLRequest, NSHTTPURLResponse?, T?, AnyObject?, NSError?) -> _)'
and if I replace the c:T? above with c:User? it is happy.
A comment below referred to this question, which may explain why the solution I tried does not work, but does really answer my intended question as to how to avoid this duplicated code.
I think I found an acceptable solution.
Below is the relevant parts of the code, but basically I end up passing in an argument type of AnyObject in to the closure rather than a generic. Before you say that I am cheating you have to understand that my original call would have looked something like this.
func testGetUserRequestSuccess() {
let getUserCommand=commandFactory.createGetUserCommandUsing(apiRepo, id: 1) {
if let swiftObject = $0.swiftObject {
XCTAssertEqual(swiftObject.id!, 1, "id is correct")
}
self.expectation.fulfill()
}
commandProcessor.processCommand(getUserCommand)
wait(5)
}
now it looks like this:
func testGetUserRequestSuccess() {
let getUserCommand=commandFactory.createGetUserCommandUsing(apiRepo, id: 1) {
if let swiftObject = $0.swiftObject as? User {
XCTAssertEqual(swiftObject.id!, 1, "id is correct")
}
self.expectation.fulfill()
}
commandProcessor.processCommand(getUserCommand)
wait(5)
}
so I had to add the as? User cast to the if let statement, which seems reasonable.
relevant code for anyone interested.
The "specialty" code (what will be repeated for each operation)
func getUser(retriesUse retryProcessor: APICommandProcessor, command: APICommand, id: Int, handler: (apiResponse: APIResponse) -> Void ) {
let url = apiPath + "users/\(id)"
Alamofire.request(.GET, url, parameters: requestParameters)
.responseObject { (a:NSURLRequest, b:NSHTTPURLResponse?, c:User?, d:AnyObject?, e:NSError? ) in
let apiResponse = APIResponse(command: command,request: a, response: b, swiftObject: c, rawObject: d, error: e)
self.finishResponse(command, retryProcessor: retryProcessor, apiResponse: apiResponse, handler: handler)
}
}
The common part that I wanted to factor out (mainly a placeholder at this point)
func finishResponse(command: APICommand, retryProcessor: APICommandProcessor, apiResponse: APIResponse, handler: (apiResponse: APIResponse) -> Void) -> Void {
if AlamofireAPIRequestRepository.resultIsRetryableError(apiResponse.error, retryProcessor: retryProcessor, command: command) {
println("retrying \(command)")
retryProcessor.retryCommand(command)
} else {
println("request completed")
handler(apiResponse: apiResponse)
}
}
a supporting struct used above:
struct APIResponse {
let command: APICommand
let request: NSURLRequest
let response: NSHTTPURLResponse?
let swiftObject: AnyObject?
let rawObject: AnyObject?
let error: NSError?
}