Swift closure with Alamofire - swift

I am making API calls to a server. I am leveraging Alamofire to handle this. I'm trying to create a function that uses Alamofire's GET function to return an object based on a custom class that holds the various outputs provided by this GET function.
It's not clear to me the way in which to do this.
Here's my custom class that will hold details about the response:
import Foundation
class ResponsePackage {
var success = false
var response: AnyObject? = nil
var error: NSError? = nil
}
In another class I have the following function:
func get(apiEndPoint: NSString) -> ResponsePackage {
let responsePackage = ResponsePackage()
Alamofire
.request(.GET, apiEndPoint)
.responseJSON {(request, response, JSON, error) in
responsePackage.response = JSON
responsePackage.success = true
responsePackage.error = error
}
return responsePackage
}
This returns nil as the call to the server is not complete before the return gets executed. I know that I should be able to do this with closures, but I am not sure how to construct this.

The code between the {} is the equivalent of block in objective-C : this is a chunk of code that gets executed asynchronously.
The error you made is where you put your return statement : when you launch your request, the code in {} is not executed until the framework received a response, so when the return statement is reached, chances are, there is still no response. You could simply move the line :
return responsePackage
inside the closure, so the func return only when it has received a response. This is a simple way, but it's not really optimal : your code will get stuck at waiting for the answers. The best way you can do this is by using closure, too. This would look something like :
func get(apiEndPoint: NSString, completion: (response: ResponsePackage) -> ()) -> Bool {
let responsePackage = ResponsePackage()
Alamofire
.request(.GET, apiEndPoint)
.responseJSON {(request, response, JSON, error) in
responsePackage.response = JSON
responsePackage.success = true
responsePackage.error = error
completion(response: responsePackage)
}
}

I make an example follow your question about responseJSON with closures:
Follow this little steps:
First of all you can create your custom types in a general class (for example a Constants.swift class):
typealias apiSuccess = (result: NSDictionary?) -> Void
typealias apiProgress = (result: NSDictionary?) -> Void // when you want to download or upload using Alamofire..
typealias apiFailure = (error: NSDictionary?) -> Void
Then in your class:
// Normal http request with JSON response..
func callJSONrequest(url:String, params:[String: AnyObject]?, success successBlock :apiSuccess,
failure failureBlock :apiFailure) {
Alamofire.request(.GET, url, parameters: params, encoding: ParameterEncoding.URL)
.responseJSON { response in
print("\(response.request?.URL)") // original URL request
//print(response.response) // URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
if response.result.isSuccess {
let jsonDic = response.result.value as! NSDictionary
successBlock(result: jsonDic)
} else {
let httpError: NSError = response.result.error!
let statusCode = httpError.code
let error:NSDictionary = ["error" : httpError,"statusCode" : statusCode]
failureBlock(error: error)
}
}
}
}
func myCommonFunction() {
let myApiSuccess: apiSuccess = {(result: NSDictionary?) -> Void in
print ("Api Success : result is:\n \(result)")
// Here you can make whatever you want with result dictionary
}
let myApiFailure: apiFailure = {(error: NSDictionary?) -> Void in
print ("Api Failure : error is:\n \(error)")
// Here you can check the errors with error dictionary looking for http error type or http status code
}
var params :[String: AnyObject]?
let name : String! = "this is my name"
let id : String! = "000a"
params = ["name" : name, "id" : id]
let url : String! = "https://etc..."
callJSONrequest(url, params:params, success: myApiSuccess, failure: myApiFailure)
}

Related

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
}
}
}
}

What does " Value of tuple type '(String, JSON)' has no member 'subscript' " refer to in Xcode?

So for my file I am trying to use Alamofire to make a request in order to get Photos from Flickr using their API.
I am using CoreData, Alamofire, and SwiftyJSON
The method is as follow:
func getImagesFromFlickr(_ selectedPin: Pin, _ page: Int, _ completionHandler: #escaping (_ result: [Photo]?, _ error: NSError?) -> Void) {
/* Set Parameters */
let methodParameters: [String:String] = [
Constants.FlickrParameterKeys.Method: Constants.FlickrParameterValues.SearchMethod,
Constants.FlickrParameterKeys.APIKey: Constants.FlickrParameterValues.APIKey,
Constants.FlickrParameterKeys.BoundingBox: bboxString(longitude:selectedPin.longitude , latitude: selectedPin.latitude),
Constants.FlickrParameterKeys.Latitude: "\(selectedPin.latitude)",
Constants.FlickrParameterKeys.Longitude: "\(selectedPin.longitude)",
Constants.FlickrParameterKeys.PerPage: "21",
Constants.FlickrParameterKeys.Page: "\(page)",
Constants.FlickrParameterKeys.SafeSearch: Constants.FlickrParameterValues.UseSafeSearch,
Constants.FlickrParameterKeys.Extras: Constants.FlickrParameterValues.MediumURL,
Constants.FlickrParameterKeys.Format: Constants.FlickrParameterValues.ResponseFormat,
Constants.FlickrParameterKeys.NoJSONCallback: Constants.FlickrParameterValues.DisableJSONCallback
]
/* Build the URL */
let url = flickrURLFromParameters(methodParameters)
/* Make the request with Alamofire */
Alamofire.request(url).validate(statusCode: 200..<300).responseJSON { (response) in
if response.result.isSuccess {
print("Success! Got the Images from Flickr!")
let flickrJSON : JSON = JSON(response.result.value!)
print(flickrJSON)
let photosDict = flickrJSON["photos"]
let photoArray = photosDict["photo"]
performUIUpdatesOnMain {
let context = CoreDataStack.getContext()
var imageUrlString = [Photo]()
for url in photoArray {
let urlString = String(url[Constants.FlickrResponseKeys.MediumURL])
let photo : Photo = NSEntityDescription.insertNewObject(forEntityName: "Photo", into: context ) as! Photo
photo.urlString = urlString
photo.pin = selectedPin
imageUrlString.append(photo)
CoreDataStack.saveContext()
}
completionHandler(imageUrlString, nil)
}
} else {
print("Error: \(String(describing: response.result.error))")
}
}
}
The error is within this line of code:
let urlString = String(url[Constants.FlickrResponseKeys.MediumURL])
I get this error in Xcode saying " Value of tuple type '(String, JSON)' has no member 'subscript' "
Any help with why I am getting this error is much appreciated.
Also, any recommendations on how I can implement this method better is much appreciated.
If you would like to take a look at my project it is under the branch called "Networking-with-Alamofire"
The link is here:
https://github.com/RobertoEfrainHernandez/Virtual-Tourist-Udacity/tree/Networking-with-Alamofire
If you look under the master branch I did my networking code using URLSessions and my goal for the Networking-with-Alamofire branch was to convert those networking methods using Alamofire and SwiftyJSON.
for url in photoArray
I suppose here url not always a dictionary, in your case you get a tuple (value1, value2) and obviously you cannot access parameters in tuple via subscript syntax.
You need to check JSON response and make some adjustments in mapping func

Best way to handle errors from async closures in Swift 2?

I'm using a lot of async network request (btw any network request in iOS need to by async) and I'm finding way to better handle errors from Apple's dataTaskWithRequest which not supports throws.
I have code like that:
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ()) {
let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)
if someData == nil {
// throw my custom error
}
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
// here I want to handle Apple's error
}
task.resume()
}
I need to parse my possible custom errors and handle possible connection errors from dataTaskWithRequest. Swift 2 introduced throws, but you can't throw from Apple's closure because they have no throw support and running async.
I see only way to add to my completion block NSError returning, but as I know using NSError is old-style Objective-C way. ErrorType can be used only with throws (afaik).
What's the best and most modern method to handle error when using Apple network closures? There is no way no use throws in any async network functions as I understand?
there are many ways you can solve this, but i would recommend using a completion block which expects a Result Enum. this would probably be the most 'Swift' way.
the result enum has exactly two states, success and error, which a big advantage to the usual two optional return values (data and error) which lead to 4 possible states.
enum Result<T> {
case Success(T)
case Error(String, Int)
}
Using the result enum in a completion block finishes the puzzle.
let InvalidURLCode = 999
let NoDataCode = 998
func getFrom(urlString: String, completion:Result<NSData> -> Void) {
// make sure the URL is valid, if not return custom error
guard let url = NSURL(string: urlString) else { return completion(.Error("Invalid URL", InvalidURLCode)) }
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
// if error returned, extract message and code then pass as Result enum
guard error == nil else { return completion(.Error(error!.localizedDescription, error!.code)) }
// if no data is returned, return custom error
guard let data = data else { return completion(.Error("No data returned", NoDataCode)) }
// return success
completion(.Success(data))
}.resume()
}
because the return value is a enum, you should switch off of it.
getFrom("http://www.google.com") { result in
switch result {
case .Success(let data):
// handle successful data response here
let responseString = String(data:data, encoding: NSASCIIStringEncoding)
print("got data: \(responseString)");
case .Error(let msg, let code):
// handle error here
print("Error [\(code)]: \(msg)")
}
}
another solution would be to pass two completion blocks, one for success and one for error. something along the lines of:
func getFrom(urlString: String, successHandler:NSData -> Void, errorHandler:(String, Int) -> Void)
It's very similar to Casey's answer,
but with Swift 5, now we have Result (generic enumeration) implementation in standard library,
//Don't add this code to your project, this has already been implemented
//in standard library.
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
It's very easy to use,
URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in
switch result {
case let .success(success):
handleResponse(success.response, data: success.data)
case let .error(error):
handleError(error)
}
}
https://developer.apple.com/documentation/swift/result
https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md
There's an elegant approach utilising a JavaScript-like Promise library or a Scala-like "Future and Promise" library.
Using Scala-style futures and promises, it may look as follows:
Your original function
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ())
may be implemented as shown below. It also shows, how to create a promise, return early with a failed future and how to fulfill/reject a promise:
func sendRequest(someData: MyCustomClass) -> Future<NSData> {
guard let url = ... else {
return Future.failure(MySessionError.InvalidURL) // bail out early with a completed future
}
let request = ... // setup request
let promise = Promise<NSData>()
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard let error = error else {
promise.reject(error) // Client error
}
// The following assertions should be true, unless error != nil
assert(data != nil)
assert(response != nil)
// We expect HTTP protocol:
guard let response = response! as NSHTTPURLResponse else {
promise.reject(MySessionError.ProtocolError) // signal that we expected HTTP.
}
// Check status code:
guard myValidStatusCodeArray.contains(response.statusCode) else {
let message: String? = ... // convert the response data to a string, if any and if possible
promise.reject(MySessionError.InvalidStatusCode(statusCode: response.statusCode, message: message ?? ""))
}
// Check MIME type if given:
if let mimeType = response.MIMEType {
guard myValidMIMETypesArray.contains(mimeType) else {
promise.reject(MySessionError.MIMETypeNotAccepted(mimeType: mimeType))
}
} else {
// If we require a MIMEType - reject the promise.
}
// transform data to some other object if desired, can be done in a later, too.
promise.fulfill(data!)
}.resume()
return promise.future!
}
You might expect a JSON as response - if the request succeeds.
Now, you could use it as follows:
sendRequest(myObject).map { data in
return try NSJSONSerialization.dataWithJSONObject(data, options: [])
}
.map { object in
// the object returned from the step above, unless it failed.
// Now, "process" the object:
...
// You may throw an error if something goes wrong:
if failed {
throw MyError.Failed
}
}
.onFailure { error in
// We reach here IFF an error occurred in any of the
// previous tasks.
// error is of type ErrorType.
print("Error: \(error)")
}

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?
}

HTTP Request error using Alamofire

I am trying to access my MAMP database webservice using Alamofire. Following is my code:
Following is my router to construct my URL:
enum Router: URLRequestConvertible {
static let baseURLString = "http://pushchat.local:44447/"
case PostJoinRequest(String,String,String,String,String)
var URLRequest: NSURLRequest {
let (path: String, parameters: [String: AnyObject]) = {
switch self {
case .PostJoinRequest (let addPath, let userID, let token, let nickName, let secretCode):
let params = ["cmd": "join", "user_id": "\(userID)", "token": "\(token)", "name": "\(nickName)", "code": "\(secretCode)"]
return (addPath, params)
}
}()
let URL = NSURL(string: Router.baseURLString)
let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
Following is my viewdidload code:
Alamofire.request(.POST,Router.PostJoinRequest("api.php","12345678901234","12345678901234","ABCDEF","TopSecret")).responseJSON()
{(request, response, JSON, error) in
println(JSON)
}
Following is the compile error:
Cannot invoke 'responseJSON' with an argument list of type '((,,,)->_)'
Following is the declaration from Almofire for your reference.
:param: method The HTTP method.
:param: URLString The URL string.
:param: parameters The parameters. `nil` by default.
:param: encoding The parameter encoding. `.URL` by default.
:returns: The created request.
*/
// public func request(method: Method, _ URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
// return request(encoding.encode(URLRequest(method, URLString), parameters: parameters).0)
// }
Please let me know why am I facing this issue while chaining and what is it that I am not doing right?
Thanks for your help.
Dev
The compiler error message is really misleading – there is no problem with responseJSON but with request method itself.
In fact compiler does not like your second parameter. You are passing URLRequestConvertible but URLStringConvertible is expected (see the signature you posted).
Maybe you wanted to call another version of request method:
//:param: URLRequest The URL request
//:returns: The created request.
public func request(URLRequest: URLRequestConvertible) -> Request
In that case you have to adjust your Router class and set HTTP method into NSURLRequest created inside. For example:
let URLRequest = NSMutableURLRequest(URL: URL!.URLByAppendingPathComponent(path))
URLRequest.HTTPMethod = "POST"
Note you will also probably need to use another parameter/data encoding.