Delay task until completed - ios

It is a very common question for people to ask "How do I delay a function or a chunk of code?" but that is not what I need here.
I need my code to wait until a certain task is complete, otherwise my function receives an error that I have no access_token (as the code doesn't wait for fetching the data from the Spotify server).
Here is my code so far, with the attempt to add a DispatchGroup:
func getAccessToken() throws -> Spotify.JSONStandard {
var accessToken: Spotify.JSONStandard!
let group = DispatchGroup() // <- Create group
group.enter() // <- Enter group
Alamofire.request("https://accounts.spotify.com/api/token", method: .post, parameters: spotify.parameters, headers: nil).responseJSON(completionHandler: {
response in
// Check if response is valid
if let newValue = response.result.value as? Spotify.JSONStandard {
accessToken = newValue
}
group.leave() // <- Leave group
})
group.wait() // <- Wait until task is completed
// \/ Delay this code until complete \/
if accessToken != nil {
return accessToken
}
else {
throw SpotifyError.failedToGetAccessToken
}
// /\ /\
}
Without the groups, my code throws SpotifyError.failedToGetAccessToken (the access_token is nil).
However, after adding the groups, my code just hangs and waits forever. How can I delay this code from being completed?
I know getting the token has no issues, as if I remove the return and place a print within the request, I get my expected result.
If you have any questions, please ask

Don't try to make an asynchronous task synchronous
This is a solution with a completion handler and a custom enum for convenience
enum Result {
case success(Spotify.JSONStandard), failure(Error)
}
func getAccessToken(completion: #escaping (Result)->()) {
Alamofire.request("https://accounts.spotify.com/api/token", method: .post, parameters: spotify.parameters, headers: nil).responseJSON(completionHandler: {
response in
// Check if response is valid
if let newValue = response.result.value as? Spotify.JSONStandard {
completion(.success(newValue)
} else {
completion(.failure(SpotifyError.failedToGetAccessToken))
}
})
}
and call it
getAccessToken { result in
switch result {
case .success(let token) : // do something with the token
case .failure(let error) : // do something with the error
}
}

Related

iOS rxSwift: retryWhen updating refresh token

I have a static function calling a network service.
When the 400 response code happens I would like to redo the network call.
The current code is working, except that the refreshToken in the header does not update between one try and another.
I think that the problem is because the Observable created but the request function does not update at the retry.
I rode on the web that I should use a deferred method on the Observable, but I don't know how.
I've tried moving the code: headers = [HeaderKeys.refreshToken.rawValue: "test test"] anywhere but still it never makes a call with the "test test" refresh token. it always uses the old one.
How can I fix this?
static func getAccessToken() -> Observable<GetAccessTokenResponse> {
var retryCounter = 0
let maxRetryCounter = 3
let delayRetry = 10.0
guard let refreshToken = NetworkHelper.shared.refreshToken else {
return Observable.error(AuthenticationError.networkError)
}
var headers = [HeaderKeys.refreshToken.rawValue: refreshToken]
return NetworkHelper.shared
.request(url: CoreAPI.accessToken.url, request: nil, headers: headers, responseType: GetAccessTokenResponse.self, method: .get, encoding: nil)
.catchError({ (error) -> Observable<(GetAccessTokenResponse?, Int)> in
return Observable.error(AuthenticationError.networkError)
})
.flatMap({ (response) -> Observable<GetAccessTokenResponse> in
// check http status code
switch response.1 {
case 200:
guard response.0?.accessToken != nil else {
return Observable.error(AuthenticationError.genericError)
}
// success
return Observable.just(response.0!)
case 400:
// invalid parameters, refresh token not existing
return Observable.error(AuthenticationError.invalidParameters)
case 404:
// user not existing
return Observable.error(AuthenticationError.userDoesntExist)
default:
// by default return network error
return Observable.error(AuthenticationError.networkError)
}
})
.retryWhen({ (errors) -> Observable<Void> in
return errors
.do(onNext: { (error) in
headers = [HeaderKeys.refreshToken.rawValue: "test test"]
})
.flatMap({error -> Observable<Int> in
debugLog("Retrying get refresh token")
if retryCounter >= maxRetryCounter {
let authError = error as? AuthenticationError ?? .genericError
if authError == AuthenticationError.invalidParameters {
// publish logged false on subject
VDAAuthenticationManager.shared.logged.onNext(false)
}
return Observable.error(error)
}
// increase the retry counter and retry
retryCounter += 1
return Observable<Int>.timer(delayRetry, scheduler: MainScheduler.instance)
})
.flatMap ({ (_) -> Observable<Void> in
return Observable.just(())
})
})
}
In the article RxSwift and Retrying a Network Request Despite Having an Invalid Token I explain how to keep and update a token and how to handle retries when you get a 401 error. Using deferred is part of the answer.
In your particular case. It looks like you could use my service like this:
func getToken(lastResponse: GetAccessTokenResponse?) -> Observable<(response: HTTPURLResponse, data: Data)> {
guard let refreshToken = lastResponse?.refreshToken else { return Observable.error(AuthenticationError.networkError) }
var request = URLRequest(url: CoreAPI.accessToken.url)
request.addValue(refreshToken, forHTTPHeaderField: HeaderKeys.refreshToken.rawValue)
return URLSession.shared.rx.response(request: request)
}
func extractToken(data: Data) throws -> GetAccessTokenResponse {
return try JSONDecoder().decode(GetAccessTokenResponse.self, from: data)
}
let tokenService = TokenAcquisitionService(initialToken: nil, getToken: getToken, extractToken: extractToken(data:))
In the above, you will have to pass a valid initialToken instead of nil or you will have to modify the getToken so it can get a token even if it doesn't have a refresh token.
An example of how to use deferred is below:
let response = Observable
.deferred { tokenAcquisitionService.token.take(1) }
.flatMap { makeRequest(withToken: $0) }
.map { response in
guard response.response.statusCode != 401 else { throw ResponseError.unauthorized }
return response
}
.retryWhen { $0.renewToken(with: tokenAcquisitionService) }
I explain in the article what each line of code is for and how it works.

Semaphore to sync request Swift4 stopped forever

I need sync request from web service, so I use sempahore:
class func syncProducts() {
print("syncProducts() 1")
let idsLocal = getProductsIds()
let semaphore = DispatchSemaphore(value: 0)
var idsCloud : [Int] = []
print("Getting cloud ids... 1")
OdooService.getProductsIds { (params: [Int]) in
print("SuccessBlock size ids: \(params.count) 1")
idsCloud = params
semaphore.signal()
}
semaphore.wait()
print("Depois do GetproductsIds: 1")
}
but in this example, the app keep locks forever! The request never ends. This is my function to request data from webserver and returns to success block if is case.
static func getProductsIds(successBlock: #escaping (_ params: [Int]) -> Void) {
// check odoo auth
let dispatch = DispatchGroup()
if( OdooAuth.uid == 0 ) {
print("Odoo offline, tentando reconectar...")
dispatch.enter()
OdooAuth.reconnect(successBlock: { params in
print("Reconectado com sucesso...")
dispatch.leave()
}, failureBlock: { params in
print("Falha no ReAuth")
return
})
}
print("Start request from Odoo...")
let fieldsProducts = ["id"]
let options = [ "fields": fieldsProducts] as [String : Any]
var idsList : [Int] = []
let params = [OdooAuth.db, OdooAuth.uid, OdooAuth.password,"product.template","search_read",[],options] as [Any]
AlamofireXMLRPC.request(OdooAuth.host2, methodName: "execute_kw", parameters: params).responseXMLRPC {
(response: DataResponse<XMLRPCNode>) -> Void in
switch response.result {
case .success( _):
print("Success to get Ids")
let str = String(data: response.data!, encoding: String.Encoding.utf8) as String!
let options = AEXMLOptions()
let xmlDoc = try? AEXMLDocument(xml: (str?.data(using: .utf8))!,options: options)
//print(xmlDoc!.xml)
for child in (xmlDoc?.root["params"]["param"]["value"]["array"]["data"].children)! {
for childValue in child["struct"].children {
let id = childValue["value"]["int"].value!
idsList.append(Int(id)!)
//print("Id: \(id)")
}
}
successBlock(idsList)
break
case .failure(let error):
print("Error to get Ids: \(error.localizedDescription)")
break
} // fim switch
} // fim request
} // fim getProductsIds
I don't know if semaphores is the best way to do this, but I need sync the requests! I tried use DispatchGroup() like in reauth, but not works too.
I would expect that the deadlock is a result of getProductsIds callback being called on main thread, which is blocked by the semaphore. As far as I know, by default Alamofire dispatches the callback on the main thread, which I would expect is the case of AlamofireXMLRPC, since it is a wrapper around Alamofire.
I would strongly recommend to not block main thread during an async operation.
If, however, for any really really good reason you cannot do otherwise, you will need to make sure that the callback won't get dispatched on the main dispatch queue (since that one is blocked waiting for the signal). Alamofire itself has a response overload that allows to specify the DispatchQueue object on which to run the callback. It seems that AlamofireXMLRPC has one too, so I would try to utilize that and change
AlamofireXMLRPC.request(OdooAuth.host2, methodName: "execute_kw", parameters: params)
.responseXMLRPC {
// process result
}
to:
AlamofireXMLRPC.request(OdooAuth.host2, methodName: "execute_kw", parameters: params)
.responseXMLRPC(queue: DispatchQueue.global(qos: .background)) {
// process result
}
I based it on the github source code of AlamofireXMLRPC, but have not worked with it before, so maybe there will be some syntactic errors. But it should point you to right direction. Still, I would recommend you NOT to block the thread (I am repeating myself, but this is really very important point).

Synchronized a function call

Consider a scenario, I have a function "REFRESH TOKEN", this function is called by different methods simultaneously, let's say the methods are "A", "B", "C".
If method "A" calls "REFRESH TOKEN" firstly then methods "B" and "C" should wait until it finishes.
Does anybody have a sample swift code?
How can I attain this scenario? Appreciate your help!
let serialQueue = DispatchQueue(label: "serialQueue")
var myFlag = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.refresh(param: 1)
self.refresh(param: 2)
self.refresh(param: 3)
}
func refresh(param: NSInteger) -> Void {
let absolutePath = "MY SAMPLE API"
var headers: [String: String] = Dictionary<String, String>();
headers["Content-Type"] = "application/json"
serialQueue.sync {
print("\nEntered ", param)
Alamofire.request(absolutePath, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: headers).responseString {
response in
switch response.result {
case .success:
print("SUCCESS")
break
case .failure(let error):
print(error)
}
}
}}
Output:
Entered 1
Entered 2
Entered 3
SUCCESS
SUCCESS
SUCCESS
I need an output like this:
Entered 1
SUCCESS
Entered 2
SUCCESS
Entered 3
SUCCESS
You can make use of DispatchGroup.
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
Method A {
//Refresh Token code here
dispatchGroup.leave()
}
dispatchGroup.wait()
dispatchGroup.enter()
Method B {
//Refresh Token code here
dispatchGroup.leave()
}
dispatchGroup.wait()
dispatchGroup.notify(queue: .main) {
print("Both functions are invoked one after other")
}
The easy and nasty way is to set a flag and check the flag on calling the function and changing flag on returning.
A better way, as far as I found out, is using operation queue.
You should create a dispatchQueue(Serial) and put that code in sync of that queue.
//Create a dispatch Queue
var dispatchQueue:DispatchQueue
func refreshToken() {
dispatchQueue.sync {
//whatever code is there in refreshToken method
}
}
Request chaining is one way. If you find that you have to do this a lot then maybe look into futures and promises. Another way is to use Dispatch Group.
Request Chain example
Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"]).response { (_, _, data, error) in
if error == nil {
Alamofire.request(.GET, "http://httpbin.org/get2\(data.something)", parameters: ["foo": "bar"]).response { (_, _, something, error) in
fulfill(something)
}
} else {
reject(error)
}
}
Dispatch Group Example:
DispatchQueue.global(qos: .userInitiated).async {
var storedError: NSError?
let downloadGroup = DispatchGroup()
downloadGroup.enter()
Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo":
"bar"]).response { (_, _, data, error) in
downloadGroup.leave()
}
downloadGroup.wait()
downloadGroup.enter()
Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo":
"bar"]).response { (_, _, data, error) in
downloadGroup.leave()
}
downloadGroup.wait()
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
}
If you want to look into Futures and Promises Alamofire made a lib
https://github.com/PromiseKit/Alamofire-

Synchronous Alamofire Request with DispatchGroup

I need to wait until Alamofire request finishes getting data. (Error or value). I'm calling Alamofire function inside a for loop in another function so that Alamofire requests should be finished before second for loop called. For example; first loop -> first request -> second loop -> second request...so on. Now It goes first loop -> second loop -> and after all loops finishes requests response is turning.
Request Function:
func like(sender_id: String, completion: #escaping (String?) -> ()){
let group = DispatchGroup()
if let key = api_key{
let headers: HTTPHeaders = [
"X-Auth-Token": key,
"Accept": "application/json"
]
group.enter()
Alamofire.request(baseUrl + likeURL + sender_id, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).validate()
.responseJSON { (response) -> Void in
guard response.result.isSuccess else {
print("Error while doing : \(response.result.error)")
completion(nil)
group.leave()
return
}
if let error = response.error{
completion(nil)
group.leave()
print("Error Occured with Request: \(error)")
}
if let jsonData = response.data{
let json = JSON(data: jsonData)
print(json)
group.leave()
if let matched = json["match"].bool{
completion(matched.description)
print("Liked!: \(matched)")
if(matched){
}
}else{
group.leave()
"There is an error with JSON"
}
}}
}
}
Where I call:
func like_all(completion: #escaping() -> ()){
for user in SharedUsers.sharedUser.users{
if let id = user.id{
Network.sharedInstance.like(sender_id: id) { result in
print(result)
}
}
else{
continue
}
}
completion()
}
You are using dispatch group, obviously with the intent on waiting for the group at the end of the function. But you're not waiting for it, so you're not getting the synchronous behavior you were looking for.
But that's good, because if you wait for that group (or semaphore, the other pattern to achieve this behavior) on the main thread, not only will you be blocking the main thread (which results in horrible UX and risk having your app killed by watchdog process), you're going to deadlock because responseJSON uses the main queue for it's completion handler. So before you add the wait() call on your dispatch group/semaphore, make sure you dispatch this whole for loop asynchronously to some background thread. That avoids blocking the main thread and eliminates the deadlock risk.
But this whole pattern is fundamentally flawed, as you really shouldn't use dispatch groups or semaphores to make it synchronous at all. That raises a few questions:
The first question is why you want to make this synchronous. Network requests have inherent latency, so you performing a sequence of them will be very slow. Only do this sequence of requests if you absolutely have to (e.g. each request cannot be formed because it needs something from the response of the prior request). But that doesn't appear to be the case here. So why make this process unnecessary so.
But let's assume for a second that you absolutely have to perform these sequentially (not true here, from what I can tell, but let's tease this out). Then there are two patterns for performing a series of requests consecutively rather than concurrently:
You can either lose this for loop entirely, and simply have a routine that sends the n-th request and sends request n+1 in its completion handler. That completely eliminates the need for dispatch groups/semaphores to block a thread.
Or you can wrap this in operation (e.g. https://stackoverflow.com/a/27022598/1271826) and use operation queue.
I solve it by calling like function with index + 1 everytime Alamofire returns value. I also call function in completion part of fetch request.
Here is the code:
#objc func action(_ sender: LGButton) {
sender.titleString = "Started to Like :)"
Network.sharedInstance.get_rec(completion: { (result) in
if(result != nil)
{
Network.sharedInstance.like(sender: 0, completion: { (result) in
//print(result)
})
}
})
}
Like Function:
func like(sender: Int, completion: #escaping (String?) -> ()){
if let key = api_key{
let headers: HTTPHeaders = [
"X-Auth-Token": key,
"Accept": "application/json"
]
print(sender)
print(SharedUsers.sharedUser.users.count)
if(sender < SharedUsers.sharedUser.users.count){
if let user_id = SharedUsers.sharedUser.users[sender].id{
Alamofire.request(baseUrl + likeURL + user_id, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).validate()
.responseJSON { (response) -> Void in
guard response.result.isSuccess else {
print("Error while doing : \(response.result.error)")
completion(nil)
return
}
if let error = response.error{
completion(nil)
print("Error Occured with Request: \(error)")
}
if let jsonData = response.data{
let json = JSON(data: jsonData)
if let matched = json["match"].bool{
completion(matched.description)
print("Liked!: \(matched)")
if(sender <= SharedUsers.sharedUser.users.count){
self.like(sender: sender + 1, completion: {result in
//print(result)
})
}else{
return
}
if(matched){
}
}else{
"There is an error with JSON"
}}}
}else{return}
}}}

Alamofire : How to handle errors globally

My question is quite similar to this one, but for Alamofire : AFNetworking: Handle error globally and repeat request
How to be able to catch globally an error (typically a 401) and handle it before other requests are made (and eventually failed if not managed) ?
I was thinking of chaining a custom response handler, but that's silly to do it on each request of the app.
Maybe subclassing, but which class should i subclass to handle that ?
Handling refresh for 401 responses in an oauth flow is quite complicated given the parallel nature of NSURLSessions. I have spent quite some time building an internal solution that has worked extremely well for us. The following is a very high level extraction of the general idea of how it was implemented.
import Foundation
import Alamofire
public class AuthorizationManager: Manager {
public typealias NetworkSuccessHandler = (AnyObject?) -> Void
public typealias NetworkFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError) -> Void
private typealias CachedTask = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void
private var cachedTasks = Array<CachedTask>()
private var isRefreshing = false
public func startRequest(
method method: Alamofire.Method,
URLString: URLStringConvertible,
parameters: [String: AnyObject]?,
encoding: ParameterEncoding,
success: NetworkSuccessHandler?,
failure: NetworkFailureHandler?) -> Request?
{
let cachedTask: CachedTask = { [weak self] URLResponse, data, error in
guard let strongSelf = self else { return }
if let error = error {
failure?(URLResponse, data, error)
} else {
strongSelf.startRequest(
method: method,
URLString: URLString,
parameters: parameters,
encoding: encoding,
success: success,
failure: failure
)
}
}
if self.isRefreshing {
self.cachedTasks.append(cachedTask)
return nil
}
// Append your auth tokens here to your parameters
let request = self.request(method, URLString, parameters: parameters, encoding: encoding)
request.response { [weak self] request, response, data, error in
guard let strongSelf = self else { return }
if let response = response where response.statusCode == 401 {
strongSelf.cachedTasks.append(cachedTask)
strongSelf.refreshTokens()
return
}
if let error = error {
failure?(response, data, error)
} else {
success?(data)
}
}
return request
}
func refreshTokens() {
self.isRefreshing = true
// Make the refresh call and run the following in the success closure to restart the cached tasks
let cachedTaskCopy = self.cachedTasks
self.cachedTasks.removeAll()
cachedTaskCopy.map { $0(nil, nil, nil) }
self.isRefreshing = false
}
}
The most important thing here to remember is that you don't want to run a refresh call for every 401 that comes back. A large number of requests can be racing at the same time. Therefore, you want to act on the first 401, and queue all the additional requests until the 401 has succeeded. The solution I outlined above does exactly that. Any data task that is started through the startRequest method will automatically get refreshed if it hits a 401.
Some other important things to note here that are not accounted for in this very simplified example are:
Thread-safety
Guaranteed success or failure closure calls
Storing and fetching the oauth tokens
Parsing the response
Casting the parsed response to the appropriate type (generics)
Hopefully this helps shed some light.
Update
We have now released 🔥🔥 Alamofire 4.0 🔥🔥 which adds the RequestAdapter and RequestRetrier protocols allowing you to easily build your own authentication system regardless of the authorization implementation details! For more information, please refer to our README which has a complete example of how you could implement on OAuth2 system into your app.
Full Disclosure: The example in the README is only meant to be used as an example. Please please please do NOT just go and copy-paste the code into a production application.
in Alamofire 5 you can use RequestInterceptor
Here is my error handling for 401 error in one of my projects, every requests that I pass the EnvironmentInterceptor to it the func of retry will be called if the request get to error
and also the adapt func can help you to add default value to your requests
struct EnvironmentInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: #escaping (AFResult<URLRequest>) -> Void) {
var adaptedRequest = urlRequest
guard let token = KeychainWrapper.standard.string(forKey: KeychainsKeys.token.rawValue) else {
completion(.success(adaptedRequest))
return
}
adaptedRequest.setValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authentication.rawValue)
completion(.success(adaptedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
//get token
guard let refreshToken = KeychainWrapper.standard.string(forKey: KeychainsKeys.refreshToken.rawValue) else {
completion(.doNotRetryWithError(error))
return
}
APIDriverAcountClient.refreshToken(refreshToken: refreshToken) { res in
switch res {
case .success(let response):
let saveAccessToken: Bool = KeychainWrapper.standard.set(response.accessToken, forKey: KeychainsKeys.token.rawValue)
let saveRefreshToken: Bool = KeychainWrapper.standard.set(response.refreshToken, forKey: KeychainsKeys.refreshToken.rawValue)
let saveUserId: Bool = KeychainWrapper.standard.set(response.userId, forKey: KeychainsKeys.uId.rawValue)
print("is accesstoken saved ?: \(saveAccessToken)")
print("is refreshToken saved ?: \(saveRefreshToken)")
print("is userID saved ?: \(saveUserId)")
completion(.retry)
break
case .failure(let err):
//TODO logout
break
}
}
} else {
completion(.doNotRetry)
}
}
and you can use it like this :
#discardableResult
private static func performRequest<T: Decodable>(route: ApiDriverTrip, decoder: JSONDecoder = JSONDecoder(), completion: #escaping (AFResult<T>)->Void) -> DataRequest {
return AF.request(route, interceptor: EnvironmentInterceptor())
.responseDecodable (decoder: decoder){ (response: DataResponse<T>) in
completion(response.result)
}

Resources