How to set up completion handlers in API Call - Swift 5 - ios

I've run into a problem.
I've made a service for API calls that returns a User object, and the API call works fine and it returns the User object.
However, I need to use completion and #escaping, and I'm not sure how to do it properly, since I'm new to Swift and iOS development.
The Code is below.
func login(with credentials: LoginCredentials) {
let params = credentials
AF.request(Service.baseURL.appending("/user/login"), method: .post, parameters: params, encoder: JSONParameterEncoder.default).responseJSON { response in
switch response.result {
case .success:
print("Success")
case.failure:
print("Fail")
}
}
Also, how do I call a function when there is completion?

It’s not clear what kind of argument (if it takes one) the completion for your method shall take, thus I’m gonna give you a couple of hints.
Perhaps you want to also have the completion for your method execute on a queue specified as parameter in the call.
Therefore you might structure your method’s signature in this way:
func login(with credentials: LoginCredentials, queue: DispatchQueue = .main, _ completion: #esacping (Bool) -> Void)
Here I’ve used the type Bool as argument for the completion to be executed, and defaulted the queue argument to be .main, assuming that you’re just interested in deliver as result argument in the completion a value of true in case you’ve gotten a .success response, false otherwise; I’m also assuming that this method will mainly be called with the completion being executed on the main thread for updating a state, hence the default value for the queue argument.
Therefore your method body could become like this:
func login(with credentials: LoginCredentials, queue: DispatchQueue = .main, _ completion: #escaping (Bool) -> Void) {
let params = credentials
AF.request(
Service.baseURL.appending("/user/login"),
method: .post,
parameters: params,
encoder: JSONParameterEncoder.default
).responseJSON { response in
switch response.result {
case .success:
queue.async { completion(true) }
case.failure:
queue.async { completion(false) }
}
}
Now of course if you need to deliver something different (as in the token for the login session and an error in case of failure) then you’d better adopt a Result<T,Error> Swift enum as argument to pass to the completion.
Without you giving more details in these regards, I can only give a generic suggestion for this matter, cause I cannot go into the specifics.
EDIT
As a bonus I'm also adding how you could structure this method with the new Swift concurrency model:
// Assuming you are returning either a value of type User or
// throwing an error in case the login failed (as per your comments):
func login(with credentials: LoginCredentials) async throws -> User {
withCheckedThrowingContinuation { continuation in
AF.request(
Service.baseURL.appending("/user/login"),
method: .post,
parameters: credentials,
encoder: JSONParameterEncoder.default
).responseJSON { response in
switch response.result {
case .success:
let loggedUser = User(/* your logic to create the user to return*/)
continuation.resume(returning: loggedUser)
case.failure:
continuation.resume(throwing: User.Error.loginError)
}
}
}
// Here's possible way to implement the error on your User type
extension User {
enum Error: Swift.Error {
case loginError
// add more cases for other errors related to this type
}
}

Related

Swift optional completion handler

I am trying to create a service object in my Swift application to handle requests a bit easier. I've got most of it wired up, however I might be misunderstanding completion handlers.
I have this function that simply posts to a local API endpoint I have running.
func createList(name: String, completion: #escaping (Response) -> Void) {
let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]
AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
switch response.result {
case .failure(let err):
print(err)
case .success(let res):
completion(res)
}
}
}
All that needs to happen is I need to pass that name to the function which I do here
barback.createList(name: name) -> after a button is tapped
However, I am now getting this error.
Missing argument for parameter 'completion' in call
My goal here is to just return the Response object so I can access attributes on that to do certain things in the UI. I was not able to return res here because, from my understanding, this is an async request and it's actually returning from that competition handler? (could be butchering that). The way I saw others doing this was by adding a competition handler to the params and adding an escape to that.
My end goal here is to do something like...
if barback.createList(name: name).status = 200
(trigger some UI component)
else
(display error toast)
end
Is my function flawed in it's design? I've tried changing my competition handler to be
completion: (#escaping (Response) -> Void) = nil
but run into some other errors there. Any guidance here?
Completion handlers are similar to function return values, but not the same. For example, compare the following functions:
/// 1. With return value
func createList(name: String) -> Response { }
/// 2. With completion handler
func createList(name: String, completion: #escaping (Response) -> Void) { }
In the first function, you'd get the return value instantly.
let response = barback.createList(name: name)
if response.status = 200 {
/// trigger some UI component
}
However, if you try the same for the second function, you'll get the Missing argument for parameter 'completion' in call error. That's because, well, you defined a completion: argument label. However, you didn't supply it, as matt commented.
Think of completion handlers as "passing in" a chunk of code into the function, as a parameter. You need to supply that chunk of code. And from within that chunk of code, you can access your Response.
/// pass in chunk of code here
barback.createList(name: name, completion: { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
})
Note how you just say barback.createList, not let result = barback.createList. That's because in the second function, with the completion handler, doesn't have a return value (-> Response).
Swift also has a nice feature called trailing closure syntax, which lets you omit the argument label completion:.
barback.createList(name: name) { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
}
You can also refer to response, the closure's first argument, by using $0 (which was what I did in my comment). But whether you use $0 or supply a custom name like response is up to you, sometimes $0 is just easier to type out.
barback.createList(name: name) {
/// access $0 (`response`) from within block of code
if $0.status = 200 {
/// trigger some UI component
}
}
Calling createList would look something more like this:
barback.createList(name: name) { response in
if response.status == 200 {
// OK
} else {
// Error
}
}
This fixes the issue because you are now running this completion closure - { response in ... } - where response is the value you pass in. In this case, you pass in res. See this post about using completion handlers.
If you did want an optional completion handler so you don't always need to include it, you could change the definition to the following (adding = { _ in }, meaning it defaults to an empty closure):
func createList(name: String, completion: #escaping (Response) -> Void = { _ in })
Another way is actually making the closure optional:
func createList(name: String, completion: ((Response) -> Void)? = nil)
And then inside the method you need ? when you call completion, since it's optional:
completion?(res)
Use the completion handler as mentioned in the comments and answers.
In addition you should include a completion when it fails, otherwise you
will never get out of that function.
I would restructure your code to cater for any errors that might happens, like this:
func createList(name: String, completion: #escaping (Response?, Error?) -> Void) {
let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]
AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
switch response.result {
case .failure(let err):
print(err)
completion(nil, err)
case .success(let res):
completion(res, nil)
}
}
}
call it like this:
barback.createList(name: name) { (response, error) in
if error != nil {
} else {
}
}
If you do not put a completion(...) in your "case .failure" it will never get out of there.

Completion handlers and Operation queues

I am trying to do the following approach,
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 10
func registerUser(completionHandler: #escaping (Result<Data, Error>) -> Void) -> String {
self.registerClient() { (result) in
switch result {
case .success(let data):
self.downloadUserProfile(data.profiles)
case .failure(let error):
return self.handleError(error)
}
}
}
func downloadUserProfile(urls: [String]) {
for url in urls {
queue.addOperation {
self.client.downloadTask(with: url)
}
}
}
I am checking is there anyway I can get notified when all operations gets completed and then I can call the success handler there.
I tried checking the apple dev documentation which suggests to use
queue.addBarrierBlock {
<#code#>
}
but this is available only from iOS 13.0
Pre iOS 13, we’d use dependencies. Declare a completion operation, and then when you create operations for your network requests, you’d define those operations to be dependencies for your completion operation.
let completionOperation = BlockOperation { ... }
let networkOperation1 = ...
completionOperation.addDependency(networkOperation1)
queue.addOperation(networkOperation1)
let networkOperation2 = ...
completionOperation.addDependency(networkOperation2)
queue.addOperation(networkOperation2)
OperationQueue.main.addOperation(completionOperation)
That having been said, you should be very careful with your operation implementation. Do I correctly infer that downloadTask(with:) returns immediately after the download task has been initiated and doesn’t wait for the request to finish? In that case, neither dependencies nor barriers will work the way you want.
When wrapping network requests in an operation, you’d want to make sure to use an asynchronous Operation subclass (e.g. https://stackoverflow.com/a/32322851/1271826).
The pre-iOS 13 way is to observe the operationCount property of the operation queue
var observation : NSKeyValueObservation?
...
observation = operationQueue.observe(\.operationCount, options: [.new]) { observed, change in
if change.newValue == 0 {
print("operations finished")
}
}
}

How do I get video data outside of function that returns void?

I am trying to create a simple app that fetches a new video from Vimeo everyday. I can access Vimeo and the videos no problem, but the built in "request" function defaults to returning void and I would like to use the video info (URIs, names, .count, etc.) outside of the request function.
The request function returns something called a "RequestToken" which contains a path and a URLSessionDataTask. I thought maybe that was the key but, probably since I'm new to programming, I have been unable to effectively use this info. I've also read a lot of the Vimeo class files associated with VimeoClient and Request and so forth and it seems like it creates a dictionary object when a request has completed but I have no idea how to access that. I feel like this is just some knowledge about functions/closures/returns that I lack and can't quite find the internet search terms to answer.
let videoRequest = Request<[VIMVideo]>(path: "/user/videos")
vimeoClient.request(videoRequest) { result in
switch result {
case .success(let response):
let video: [VIMVideo] = response.model
print("retrieved videos: \(video.count)")
case .failure(let error):
print ("error retrieving video: \(error)")
Here is the full method call under VimeoClient class.
public func request<ModelType>(_ request: Request<ModelType>, completionQueue: DispatchQueue = DispatchQueue.main, completion: #escaping ResultCompletion<Response<ModelType>>.T) -> RequestToken
{
if request.useCache
{
self.responseCache.response(forRequest: request) { result in
switch result
{
case .success(let responseDictionary):
if let responseDictionary = responseDictionary
{
self.handleTaskSuccess(forRequest: request, task: nil, responseObject: responseDictionary, isCachedResponse: true, completionQueue: completionQueue, completion: completion)
}
else
{
let error = NSError(domain: type(of: self).ErrorDomain, code: LocalErrorCode.cachedResponseNotFound.rawValue, userInfo: [NSLocalizedDescriptionKey: "Cached response not found"])
self.handleError(error, request: request)
completionQueue.async {
completion(.failure(error: error))
}
}
case .failure(let error):
self.handleError(error, request: request)
completionQueue.async {
completion(.failure(error: error))
}
}
}
return RequestToken(path: request.path, task: nil)
}
else
{
let success: (URLSessionDataTask, Any?) -> Void = { (task, responseObject) in
DispatchQueue.global(qos: .userInitiated).async {
self.handleTaskSuccess(forRequest: request, task: task, responseObject: responseObject, completionQueue: completionQueue, completion: completion)
}
}
let failure: (URLSessionDataTask?, Error) -> Void = { (task, error) in
DispatchQueue.global(qos: .userInitiated).async {
self.handleTaskFailure(forRequest: request, task: task, error: error as NSError, completionQueue: completionQueue, completion: completion)
}
}
let path = request.path
let parameters = request.parameters
let task: URLSessionDataTask?
switch request.method
{
case .GET:
task = self.sessionManager?.get(path, parameters: parameters, progress: nil, success: success, failure: failure)
case .POST:
task = self.sessionManager?.post(path, parameters: parameters, progress: nil, success: success, failure: failure)
case .PUT:
task = self.sessionManager?.put(path, parameters: parameters, success: success, failure: failure)
case .PATCH:
task = self.sessionManager?.patch(path, parameters: parameters, success: success, failure: failure)
case .DELETE:
task = self.sessionManager?.delete(path, parameters: parameters, success: success, failure: failure)
}
guard let requestTask = task else
{
let description = "Session manager did not return a task"
assertionFailure(description)
let error = NSError(domain: type(of: self).ErrorDomain, code: LocalErrorCode.requestMalformed.rawValue, userInfo: [NSLocalizedDescriptionKey: description])
self.handleTaskFailure(forRequest: request, task: task, error: error, completionQueue: completionQueue, completion: completion)
return RequestToken(path: request.path, task: nil)
}
return RequestToken(path: request.path, task: requestTask)
}
}`
In the Print statement I get the correct count for the videos on the page so I know that it is working properly inside the function. The request doesn't explicitly say that it returns void but if I try to return a [VIMVideo] object the debug tells me that the expected return is Void and then will not build.
All the information I need I can get, but only inside the function. I would like to be able to use it outside the function but the only way I know how is returning the object which it isn't allowing me to do.
Thanks for your help.
After reading the provided documentation on async, it looks like the callback is the way forward here. However, when looking back at the class method in question it already has a completion handler built in. In the examples given to me by the helpful people here the completion handler would use a simple variable that could be referenced in the callback. The, seemingly, built-in VimeoNetworking Pod completion handler has a complex-looking reference to a response class (which may be part of the responseDictionary?) any ideas on how I can reference that completion handler in a callback? Or is that not the purpose and I should attempt to craft my own completion handler on top of the provided one?
Thanks much!

Swift: Selector with #escaping closure return EXC_BAD_ACCESS

What I've been doing is I have a function that requesting data from an API and when the response I've separate to two conditions is .success and .failure as a default response by Alamofire. and I've been using escaping closure to check if the response .success, I'll display something otherwise return an error to the user. and it was working fine until I want to put it into selector that UIRefresh need to use.
Here is my code:
Get Data Function:
#objc func GetData(completion: #escaping (Bool)->Void){
Alamofire.request("\(ConstanClass.http)/api/order?token=\(ConstanClass.token)").responseJSON { response in
switch response.result {
case .success:
if let value = response.result.value{
let json = JSON(value)
//Geting Json
completion(true)
}
case .failure(let error):
self.setErrorForm(self)
self.hud.dismiss(animated: true)
print(error)
completion(false)
}
}
}
Calling From Selector:
refresher.addTarget(self, action: #selector(MyOrderController.GetData(completion:)), for: UIControlEvents.valueChanged)
Here is the Error:
Thread 1: EXC_BAD_ACCESS (code=257, address=0x1a1b50997c9)
and this error pointing to completion(true) in .success.
The problem is that you can't do what you are doing. The selector you set for the UIRefreshControl and the "value changed" event must have a very specific signature. Please review the "Target-Action Mechanism" section of the UIControl documentation.
The selector must take either zero, one, or two parameters and those parameters can only be very specific parameters. The first (if provided) must be a reference to the control (the sender). And the second (if provided) must be a UIEvent.
You can't create a sender that takes a completion block. That is the cause of the crash. The one parameter is being treated as the refresh control but the code treats it as a closure, hence the EXC_BAD_ACCESS error.
Consider this, given your use of GetData, where is the completion handler being passed in? What is handling the result of the completion handler?
Given that there is nothing that can deal with this completion handler, simply change GetData (which should be named getData) to take no parameters and remove the uses of completion.
#objc func getData(){
Alamofire.request("\(ConstanClass.http)/api/order?token=\(ConstanClass.token)").responseJSON { response in
switch response.result {
case .success:
if let value = response.result.value{
let json = JSON(value)
//Geting Json
}
case .failure(let error):
self.setErrorForm(self)
self.hud.dismiss(animated: true)
print(error)
}
}
}
And update your use:
refresher.addTarget(self, action: #selector(getData), for: UIControlEvents.valueChanged)

HTTP requests being blocked by a previous one using Alamofire

I'm having some trouble with an HTTP request performed from our iOS app to our API. The problem with this request is that it usually takes 30-40s to complete. I don't need to handle the response for now, so I just need to fire it and forget about it. I don't know if the problem is in my code or in the server, that's why I'm asking here.
I'm using Alamofire and Swift 2.2 and all the other requests are working just fine. This is an screenshot from Charles proxy while I was trying to debug it: Charles screenshot
As you can see, the request that blocks the others is the refreshchannels. When that request fires (#6 and #25), the others are blocked and don't finish until the refreshchannels finishes.
Here is the code that triggers that request and also the APIManager that i've built on top of Alamofire:
// This is the method that gets called when the user enables the notifications in the AppDelegate class
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
// Recieve the APNSToken and handle it. I've removed it to make it shorter
// This sends a POST to our API to store some data
APIManager().registerForPushNotifications(parametersPush) { (result) in
switch result {
case .Success (let JSON):
// This is the slow call that blocks the other HTTP requests
APIManager().refreshChannels { _ in } // I don't need to handle the response for now
case .Failure: break
}
}
}
And the manager:
//This is my custom manager to handle all the networking inside my app
class APIManager {
typealias CompletionHandlerType = (Result) -> Void
enum Result {
case Success(AnyObject?)
case Failure(NSError)
}
let API_HEADERS = Helper.sharedInstance.getApiHeaders()
let API_DOMAIN = Helper.sharedInstance.getAPIDomain()
//MARK: Default response to a request
func defaultBehaviourForRequestResponse(response: Response<AnyObject, NSError>, completion: CompletionHandlerType) {
print("Time for the request \(response.request!.URL!): \(response.timeline.totalDuration) seconds.")
switch response.result {
case .Success (let JSON):
if let _ = JSON["error"]! {
let error = NSError(domain: "APIError", code: response.response!.statusCode, userInfo: JSON as? [NSObject : AnyObject])
completion(Result.Failure(error))
} else {
completion(Result.Success(JSON))
}
case .Failure (let error):
completion(Result.Failure(error))
}
}
func refreshChannels(completion: CompletionHandlerType) {
Alamofire.request(.PUT, "\(API_DOMAIN)v1/user/refreshchannels", headers: API_HEADERS).responseJSON { response in
self.defaultBehaviourForRequestResponse(response, completion: completion)
}
}
}
Any help will be appreciated. Have a nice day!

Resources