How should I approach doing async HTTP call inside a for-loop?
I’m using Alamofire’s SessionManager because I need to maintain a session during the iteration of the loop. Due to this, I don’t think I can use the shared session manager SessionManager.default.
However, if I were to create a new SessionManager every time, my requests will get cancelled finished with error - code: -999. An assumption that I’ve came up with is because the SessionManager is getting released from memory, therefore it is cancelled.
How should I approach this? I feel that it would be better to do synchronous calls instead. But there is no easy way to do synchronous HTTP calls with Alamofire. I don't mind using a different library.
Some additional info:
NSAllowsArbitraryLoads is set to true in the info.plist.
My app requires nested api calls, where the completion of the HTTP
call triggers another one, while still using the same session.
Controller class
class CheckinController {
private let sessionManager = SessionManager(configuration: URLSessionConfiguration.default)
// status is an enum such as .connectionFailed and .success
func checkin(id: String, password: String, completion: ( (status) -> Void)) {
self.login(id, password, completion: { (status) in
self.sessionManager.request(…).responseString(completionHandler: { (response) in
// Check for response, then calls completion
completion(status)
})
})
}
func login(id: String, password: String, completion: ( (status) -> Void)) {
let params = [“id” : id, “pass”, password]
self.sessionManager.request(LOGINURL, method: .post, parameters: params).responseString(completionHandler: { (response) in
// Check for response, and then calls completion.
…
completion(someStatus)
})
}
}
Usage:
for 0..x {
CheckinController().checkin("someId", "somePassword", completion: { status in
print(status)
)}
}
Related
I am trying to make a retry for my Alamofire Interceptor because I work with JSON Web Token. Adapt works great. But the server updates the Access token every 10 minutes after user registration or authorization. After 10 mins Access token doesn't work anymore, and the server response is 401. So I need to Refresh the token when the status is 401. As I mentioned above, adapt works great. But I need help understanding how to deal with retry. Below is my Interceptor:
class RequestInterceptor: Alamofire.RequestInterceptor {
func adapt( _ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry( _ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
}
}
My View Model:
func refreshTokenFunc() {
AF.request(TabBarModel.Request.refreshTokenUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, interceptor: RequestInterceptor()).response { response in
...
And usage (I work with SwiftUI):
.task {
tabBarViewModel.refreshTokenFunc()
}
I was trying with some examples from the Internet. But it doesn't work for me.
In you retry you need to call the completion handler on both sides of the guard, not just in the else side. completion(.retry) is common but you could also track a delay to make sure you don't overload the backend.
Additionally, you should be validating response and checking the error, not reaching directly into request.task.
AF.request(...).validate()... // Ensure the response code is within range.
// In retry
guard let error = error.asAFError, error.responseCode == 401 else { ... }
The code within the function is executed in a different order than it is expected. I wanted to change the state of the login Boolean variable inside the if statement, but the function returns the initial value before if statement is completed.
Code sample:
class ClassName {
func loginRequest (name: String, pwd: String) -> Bool {
var login:Bool
//Initial value for login
login = false
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if (httpResponse.statusCode) == 200 {
//Change the value of login if login is successful
login = true
if let data = data, let dataString = String(data: data, encoding: .utf8) {
do {
...
} catch {print(error.localizedDescription)}
}
}
}
}
task.resume()
//Problem return false in any case because return is completed before if statement
return login
}
}
Completion Handlers is your friend
The moment your code runs task.resume(), it will run your uploadTask and only when that function is finished running it will run the code where you change your login variable.
With That said: That piece of code is running asynchronously. That means your return login line of code won't wait for your network request to come back before it runs.
Your code is actually running in the order it should. But i myself wrote my first network call like that and had the same problem. Completion Handles is how i fixed it
Here is a very nice tutorial on Completion Handlers or you might know it as Callbacks :
Link To Completion Handlers Tutorial
If i can give you a little hint - You will have to change your function so it looks something like this: func loginRequest (name: String, pwd: String, completionHandler: #escaping (Bool) -> Void)
And replace this login = true with completionHandler(true)
Wherever it is you call your function it will look something like this:
loginRequest(name: String, pwd: String) {didLogIn in
print("Logged In : \(didLogIn)")
}
One last thing... You're actually already using Completion Handlers in your code.
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
... ... But hopefully now you understand a little bit better, and will use a completion handler approach when making network calls.
GOOD LUCK !
I have a function that provides a layer over URLSession. When this function is called I would like to check if the current access token as expired, if it has, I would to pause the current call, make a call to request a new token, replace the existing entry in the Keychain, then continue with the call.
func profile(with endpoint: ProfilesEndpoint, method: HTTPMethod, body: String?, headers: [String: String]?, useAuthToken: Bool = true, completion: #escaping (Either<ProfileResponse>) -> Void) {
var request = endpoint.request
request.httpMethod = method.rawValue
if let body = body {
request.httpBody = body.data(using: .utf8)
}
if useAuthToken {
if !AuthService.shared.isTokenValid {
let group = DispatchGroup()
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
}
let (header, token) = AuthService.shared.createAuthHeaderForNetworkRequest()
request.addValue(token, forHTTPHeaderField: header)
}
if let headers = headers {
for (key, value) in headers {
request.addValue(value, forHTTPHeaderField: key)
}
}
execute(with: request, completion: completion)
}
A mechanism existing for handling the Keychain so please assume this is in place.
The function to request a new token looks like
func requestRefreshToken() -> Void {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else { return }
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
}
}
}
I was expecting the executing to pause here
group.enter()
OAuthService.shared.requestRefreshToken()
group.leave()
However it does not.
How I can await this call to complete before completing the rest of the function?
Add to your requestRefreshToken method completion handler which will get executed when your request for token is completed
func requestRefreshToken(_ completion: #escaping () -> Void) {
if let refreshToken = KeychainWrapper.standard.string(forKey: "RefreshToken") {
var postBody = "grant_type=\(refreshTokenGrantType)&"
postBody += "client_id=\(clientId)&"
postBody += "refresh_token=\(refreshToken)&"
let additionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded;"
]
APIClient.shared.identity(with: .token, method: .post, body: postBody, headers: additionalHeaders, useAuthToken: false) { either in
switch either {
case .success(let results):
guard let accessToken = results.accessToken, let refreshToken = results.refreshToken else {
completion()
return
}
AuthService.shared.addTokensToKeyChain(tokens: ["AccessToken": accessToken, "RefreshToken": refreshToken])
case .error(let error):
print("Error:", error)
}
completion()
}
}
}
then leave dispatchGroup in closure and also add group.wait() (after calling request method) for pausing current thread until group's task has completed
group.enter()
OAuthService.shared.requestRefreshToken {
group.leave()
}
group.wait()
Note: you can add boolean parameter to completion to check if request for token was successful or not
Why not do this underneath instead of on top? This seems like an ideal use for NSURLProtocol. Basically:
The URL protocol snags creation of the request in its init method and saves off all the provided parameters.
The URL protocol allocates a private NSURLSession instance and stores it in a property. That session should not be configured to use the protocol, or else you'll get an infinite loop.
When the protocol gets a startLoading() call, it checks the validity of the token, then:
Fires off a request for a new token if needed, in that private session.
Upon response, or if the token is still valid, fires off the real request — again, in that private session.
With that approach, the entire authentication process becomes basically transparent to the app except for the need to add the protocol into protocolClasses on the session configuration when creating a new session.
(There are a number of websites, including developer.apple.com, that provide examples of custom NSURLProtocol subclasses; if you decide to go with this approach, you should probably use one of those sample code projects as a starting point.)
Alternatively, if you want to stick with the layer-on-top approach, you need to stop thinking about "stopping" the method execution and start thinking about it as "doing the last part of the method later". It's all about asynchronous thinking.
Basically:
Factor out the last part of the method (the code that performs the actual request) into a new method (or a block).
If the token is valid, call that method immediately.
If the token is invalid, asynchronously fetch the new token.
In the token fetch call's completion callback, call the method to perform the actual request.
I'm kind of new to programming in general, so I have this maybe simple question. Actually, writing helps me to identify the problem faster.
Anyway, I have an app with multiple asynchronous calls, they are nested like this:
InstagramUnoficialAPI.shared.getUserId(from: username, success: { (userId) in
InstagramUnoficialAPI.shared.fetchRecentMedia(from: userId, success: { (data) in
InstagramUnoficialAPI.shared.parseMediaJSON(from: data, success: { (media) in
guard let items = media.items else { return }
self.sortMediaToCategories(media: items, success: {
print("success")
// Error Handlers
Looks horrible, but that's not the point. I will investigate the Promise Kit once I get this working.
I need the sortMediaToCategories to wait for completion and then reload my collection view. However, in the sortMediaToCategories I have another nested function, which is async too and has a for in loop.
func sortMediaToCategories(media items: [StoryData.Items],
success: #escaping (() -> Swift.Void),
failure: #escaping (() -> Swift.Void)) {
let group = DispatchGroup()
group.enter()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.notify(queue: .global(), execute: {
self.collectionView.reloadData()
group.leave()
}) },
failure: { print("error") })
//....
I can't afford the collection view to reload every time obviously, so I need to wait for loop to finish and then reload.
I'm trying to use Dispatch Groups, but struggling with it. Could you please help me with this? Any simple examples and any advice will be very appreciated.
The problem you face is a common one: having multiple asynchronous tasks and wait until all are completed.
There are a few solutions. The most simple one is utilising DispatchGroup:
func loadUrls(urls: [URL], completion: #escaping ()->()) {
let grp = DispatchGroup()
urls.forEach { (url) in
grp.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
// handle error
// handle response
grp.leave()
}.resume()
}
grp.notify(queue: DispatchQueue.main) {
completion()
}
}
The function loadUrls is asynchronous and expects an array of URLs as input and a completion handler that will be called when all tasks have been completed. This will be accomplished with the DispatchGroup as demonstrated.
The key is, to ensure that grp.enter() will be called before invoking a task and grp.leave is called when the task has been completed. enter and leave shall be balanced.
grp.notify finally registers a closure which will be called on the specified dispatch queue (here: main) when the DispatchGroup grp balances out (that is, its internal counter reaches zero).
There are a few caveats with this solution, though:
All tasks will be started nearly at the same time and run concurrently
Reporting the final result of all tasks via the completion handler is not shown here. Its implementation will require proper synchronisation.
For all of these caveats there are nice solutions which should be implemented utilising suitable third party libraries. For example, you can submit the tasks to some sort of "executer" which controls how many tasks run concurrently (match like OperationQueue and async Operations).
Many of the "Promise" or "Future" libraries simplify error handling and also help you to solve such problems with just one function call.
You can reloadData when the last item calls the success block in this way.
let lastItemIndex = items.count - 1
for(index, item) in items.enumerated() {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: {
if index == lastItemIndex {
DispatchQueue.global().async {
self.collectionView.reloadData()
}
}
},
failure: { print("error") })
}
You have to move the group.enter() call inside your loop. Calls to enter and leave have to be balanced. If your callbacks of the mediaToStorageDistribution function for success and failure are exclusive you also need to leave the group on failure. When all blocks that called enter leave the group notify will be called. And you probably want to replace the return in your guard statement with a break, to just skip items with missing URLs. Right now you are returning from the whole sortMediaToCatgories function.
func sortMediaToCategories(media items: [StoryData.Items], success: #escaping (() -> Void), failure: #escaping (() -> Void)) {
let group = DispatchGroup()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else { break }
group.enter()
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.leave() },
failure: {
print("error")
group.leave()
})
}
}
group.notify(queue: .main) {
self.collectionView.reloadData()
}
}
The API I use requires multiple requests to get search results. It's designed this way because searches can take a long time (> 5min). The initial response comes back immediately with metadata about the search, and that metadata is used in follow up requests until the search is complete. I do not control the API.
1st request is a POST to https://api.com/sessions/search/
The response to this request contains a cookie and metadata about the search. The important fields in this response are the search_cookie (a String) and search_completed_pct (an Int)
2nd request is a POST to https://api.com/sessions/results/ with the search_cookie appended to the URL. eg https://api.com/sessions/results/c601eeb7872b7+0
The response to the 2nd request will contain either:
The search results if the query has completed (aka search_completed_pct == 100)
Metadata about the progress of search, search_completed_pct is the progress of the search and will be between 0 and 100.
If the search is not complete, I want to make a request every 5 seconds until it's complete (aka search_completed_pct == 100)
I've found numerous posts here that are similar, many use Dispatch Groups and for loops, but that approach did not work for me. I've tried a while loop and had issues with variable scoping. Dispatch groups also didn't work for me. This smelled like the wrong way to go, but I'm not sure.
I'm looking for the proper design to make these recursive calls. Should I use delegates or are closures + loop the way to go? I've hit a wall and need some help.
The code below is the general idea of what I've tried (edited for clarity. No dispatch_groups(), error handling, json parsing, etc.)
Viewcontroller.swift
apiObj.sessionSearch(domain) { result in
Log.info!.message("result: \(result)")
})
ApiObj.swift
func sessionSearch(domain: String, sessionCompletion: (result: SearchResult) -> ()) {
// Make request to /search/ url
let task = session.dataTaskWithRequest(request) { data, response, error in
let searchCookie = parseCookieFromResponse(data!)
********* pseudo code **************
var progress: Int = 0
var results = SearchResults()
while (progress != 100) {
// Make requests to /results/ until search is complete
self.getResults(searchCookie) { searchResults in
progress = searchResults.search_pct_complete
if (searchResults == 100) {
completion(searchResults)
} else {
sleep(5 seconds)
} //if
} //self.getResults()
} //while
********* pseudo code ************
} //session.dataTaskWithRequest(
task.resume()
}
func getResults(cookie: String, completion: (searchResults: NSDictionary) -> ())
let request = buildRequest((domain), url: NSURL(string: ResultsUrl)!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
let theResults = getJSONFromData(data!)
completion(theResults)
}
task.resume()
}
Well first off, it seems weird that there is no API with a GET request which simply returns the result - even if this may take minutes. But, as you mentioned, you cannot change the API.
So, according to your description, we need to issue a request which effectively "polls" the server. We do this until we retrieved a Search object which is completed.
So, a viable approach would purposely define the following functions and classes:
A protocol for the "Search" object returned from the server:
public protocol SearchType {
var searchID: String { get }
var isCompleted: Bool { get }
var progress: Double { get }
var result: AnyObject? { get }
}
A concrete struct or class is used on the client side.
An asynchronous function which issues a request to the server in order to create the search object (your #1 POST request):
func createSearch(completion: (SearchType?, ErrorType?) -> () )
Then another asynchronous function which fetches a "Search" object and potentially the result if it is complete:
func fetchSearch(searchID: String, completion: (SearchType?, ErrorType?) -> () )
Now, an asynchronous function which fetches the result for a certain "searchID" (your "search_cookie") - and internally implements the polling:
func fetchResult(searchID: String, completion: (AnyObject?, ErrorType?) -> () )
The implementation of fetchResult may now look as follows:
func fetchResult(searchID: String,
completion: (AnyObject?, ErrorType?) -> () ) {
func poll() {
fetchSearch(searchID) { (search, error) in
if let search = search {
if search.isCompleted {
completion(search.result!, nil)
} else {
delay(1.0, f: poll)
}
} else {
completion(nil, error)
}
}
}
poll()
}
This approach uses a local function poll for implementing the polling feature. poll calls fetchSearch and when it finishes it checks whether the search is complete. If not it delays for certain amount of duration and then calls poll again. This looks like a recursive call, but actually it isn't since poll already finished when it is called again. A local function seems appropriate for this kind of approach.
The function delay simply waits for the specified amount of seconds and then calls the provided closure. delay can be easily implemented in terms of dispatch_after or a with a cancelable dispatch timer (we need later implement cancellation).
I'm not showing how to implement createSearch and fetchSearch. These may be easily implemented using a third party network library or can be easily implemented based on NSURLSession.
Conclusion:
What might become a bit cumbersome, is to implement error handling and cancellation, and also dealing with all the completion handlers. In order to solve this problem in a concise and elegant manner I would suggest to utilise a helper library which implements "Promises" or "Futures" - or try to solve it with Rx.
For example a viable implementation utilising "Scala-like" futures:
func fetchResult(searchID: String) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, f: poll)
}
}
}
poll()
return promise.future!
}
You would start to obtain a result as shown below:
createSearch().flatMap { search in
fetchResult(search.searchID).map { result in
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
This above contains complete error handling. It does not yet contain cancellation. Your really need to implement a way to cancel the request, otherwise the polling may not be stopped.
A solution implementing cancellation utilising a "CancellationToken" may look as follows:
func fetchResult(searchID: String,
cancellationToken ct: CancellationToken) -> Future<AnyObject> {
let promise = Promise<AnyObject>()
func poll() {
fetchSearch(searchID, cancellationToken: ct).map { search in
if search.isCompleted {
promise.fulfill(search.result!)
} else {
delay(1.0, cancellationToken: ct) { ct in
if ct.isCancelled {
promise.reject(CancellationError.Cancelled)
} else {
poll()
}
}
}
}
}
poll()
return promise.future!
}
And it may be called:
let cr = CancellationRequest()
let ct = cr.token
createSearch(cancellationToken: ct).flatMap { search in
fetchResult(search.searchID, cancellationToken: ct).map { result in
// if we reach here, we got a result
print(result)
}
}.onFailure { error in
print("Error: \(error)")
}
Later you can cancel the request as shown below:
cr.cancel()