I have a networking layer that currently uses completion handlers to deliver a result on the operation is complete.
As I support a number of iOS versions, I instead extend the network layer within the app to provide support for Combine. I'd like to extend this to now also a support Async/Await but I am struggling to understand how I can achieve this in a way that allows me to cancel requests.
The basic implementation looks like;
protocol HTTPClientTask {
func cancel()
}
protocol HTTPClient {
typealias Result = Swift.Result<(data: Data, response: HTTPURLResponse), Error>
#discardableResult
func dispatch(_ request: URLRequest, completion: #escaping (Result) -> Void) -> HTTPClientTask
}
final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func dispatch(_ request: URLRequest, completion: #escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (data, response)
} else {
throw UnexpectedValuesRepresentation()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
private extension URLSessionHTTPClient {
struct UnexpectedValuesRepresentation: Error {}
struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
}
It very simply provides an abstraction that allows me to inject a URLSession instance.
By returning HTTPClientTask I can call cancel from a client and end the request.
I extend this in a client app using Combine as follows;
extension HTTPClient {
typealias Publisher = AnyPublisher<(data: Data, response: HTTPURLResponse), Error>
func dispatchPublisher(for request: URLRequest) -> Publisher {
var task: HTTPClientTask?
return Deferred {
Future { completion in
task = self.dispatch(request, completion: completion)
}
}
.handleEvents(receiveCancel: { task?.cancel() })
.eraseToAnyPublisher()
}
}
As you can see I now have an interface that supports canceling tasks.
Using async/await however, I am unsure what this should look like, how I can provide a mechanism for canceling requests.
My current attempt is;
extension HTTPClient {
func dispatch(_ request: URLRequest) async -> HTTPClient.Result {
let task = Task { () -> (data: Data, response: HTTPURLResponse) in
return try await withCheckedThrowingContinuation { continuation in
self.dispatch(request) { result in
switch result {
case let .success(values): continuation.resume(returning: values)
case let .failure(error): continuation.resume(throwing: error)
}
}
}
}
do {
let output = try await task.value
return .success(output)
} catch {
return .failure(error)
}
}
}
However this simply provides the async implementation, I am unable to cancel this.
How should this be handled?
Swift’s new concurrency model handles cancellation perfectly well. While the WWDC 2021 videos focused on the checkCancellation and isCancelled patterns (e.g., the Explore structured concurrency in Swift video), in this case, one would use withTaskCancellationHandler to create a task that cancels the network request when the task, itself, is canceled. (Obviously, this is only a concern in iOS 13/14, as in iOS 15 one would just use the provided async methods, data(for:delegate) or data(from:delegate:), which also handle cancelation well.)
See SE-0300: Continuations for interfacing async tasks with synchronous code: Additional Examples for example. That download example is a bit outdated, so here is an updated rendition:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `data(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(from:delegate:)` instead")
func data(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `data(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(for:delegate:)` instead")
func data(with request: URLRequest) async throws -> (Data, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.data(for: request) { data, response, error in
guard let data, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
private extension URLSession {
actor SessionTask {
var state: State = .ready
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func cancel() {
if case .executing(let task) = state {
task.cancel()
}
state = .cancelled
}
}
}
// MARK: Data
extension URLSession.SessionTask {
func data(for request: URLRequest, completionHandler: #Sendable #escaping (Data?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.dataTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
extension URLSession.SessionTask {
enum State {
case ready
case executing(URLSessionTask)
case cancelled
}
}
A few minor observations on my code snippet:
I gave these names to avoid collision with the iOS 15 method names, but added deprecated messages to inform the developer to use the iOS 15 renditions once you abandon iOS 13/14 support.
I deviated from SE-0300’s example to follow the pattern of the data(from:delegate:) and data(for:delegate:) methods (returning a tuple with Data and a URLResponse).
The actor, not in the original example, is needed to synchronize the access to the URLSessionTask.
Note that according to SE-0304, that regarding withTaskCancellationHandler:
If the task has already been cancelled at the point withTaskCancellationHandler is called, the cancellation handler is invoked immediately, before the operation block is executed.
Because of this, the actor in the above uses a state variable to determine if the request has already been canceled, and just immediately resumes, throwing a CancellationError if it is already canceled.
But all of that is unrelated to the question at hand. In short, use withTaskCancellationHandler.
E.g. Here are five image requests that I started in a task group, as monitored by Charles:
And here are the same requests, but this time I canceled the whole task group (and the cancelations successfully stopped the associated network requests for me):
(Obviously the x-axis scale is different.)
If you need download renditions (to wrap downloadTask), you could do supplement the above with:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(from:delegate:)` instead")
func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(for:delegate:)` instead")
func download(with request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.download(for: request) { location, response, error in
guard let location, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
// since continuation can happen later, let’s figure out where to store it ...
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(request.url!.pathExtension)
// ... and move it to there
do {
try FileManager.default.moveItem(at: location, to: tempURL)
} catch {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: (tempURL, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
extension URLSession.SessionTask {
func download(for request: URLRequest, completionHandler: #Sendable #escaping (URL?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.downloadTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
You can't hybridize Combine with async/await. If you embrace async/await fully and call one of the async download methods...
https://developer.apple.com/documentation/foundation/urlsession/3767353-data
...then the task where you call that method will be cancellable in good order through the standard structured concurrency mechanism.
So if you want to support Swift 5.5 / iOS 15 async and yet support earlier versions too, you will need two completely independent implementations of this functionality.
async/await might not be the proper paradigm if you want cancellation. The reason is that the new structured concurrency support in Swift allows you to write code that looks single-threaded/synchronous, but it fact it's multi-threaded.
Take for example a naive synchronous code:
let data = tryData(contentsOf: fileURL)
If the file is huge, then it might take a lot of time for the operation to finish, and during this time the operation cannot be cancelled, and the caller thread is blocked.
Now, assuming Data exports an async version of the above initializer, you'd write the async version of the code similar to this:
let data = try await Data(contentsOf: fileURL)
For the developer, it's the same coding style, once the operation finishes, they'll either have a data variable to use, or they'll be receiving an error.
In both cases, there's no cancellation built in, as the operation is synchronous from the developer's perspective. The major difference is that the await-ed call doesn't block the caller thread, but on the other hand once the control flow returns it might well be that the code continues executing on a different thread.
Now, if you need support for cancellation, then you'll have to store somewhere some identifiable data that can be used to cancel the operation.
If you'll want to store those identifiers from the caller scope, then you'll need to split your operation in two: initialization, and execution.
Something along the lines of
extension HTTPClient {
// note that this is not async
func task(for request: URLRequest) -> HTTPClientTask {
// ...
}
}
class HTTPClientTask {
func dispatch() async -> HTTPClient.Result {
// ...
}
}
let task = httpClient.task(for: urlRequest)
self.theTask = task
let result = await task.dispatch()
// somewhere outside the await scope
self.theTask.cancel()
Related
I perform the next code
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
All is clear, after doing network request in background I return completion to .main . But how to handle case, if I want to call completion not in .main but in thread in which session.uploadTask was initiated, because in my application it could be not only .main .
There is not a GCD mechanism to retrieve the current dispatch queue so that you can later dispatch to it. (A long time ago, there used to be a way to fetch the current queue, but it was deprecated back in iOS 7, and even then it was “Recommended for debugging and logging purposes only.”)
If you want to call the completion handlers on specific dispatch queue, I would suggest supplying an explicit DispatchQueue parameter to the method. Below, I have it default to .main, but the caller can override that with whatever it wants:
func perform(_ request: URLRequest, with data: Data, on queue: DispatchQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.async {
completion(.failure(error))
}
}
...
}
...
}
I know that this is not precisely what you are looking for, but it is an easy way to let the caller specify on which queue your closure will be called.
If you are using operation queues, you can get the current to determine the current operation queue. And should one do this, one would use addOperation on that operation queue in order to call the completion handler.
func perform(_ request: URLRequest, with data: Data, completion: #escaping (Result<T, Error>) -> Void) {
guard let queue = OperationQueue.current else {
fatalError("Must be called from operation queue")
}
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
But this pattern only works if the caller was using operation queue, not when only using dispatch queues. For this reason, I would still be inclined to adopt the pattern of supplying the target operation queue as a parameter:
func perform(_ request: URLRequest, with data: Data, on queue: OperationQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
If you would like to call completion on the current thread simply call completion().
current thread
completion(.failure(error))
main thread
DispatchQueue.main.async {
completion(.failure(error))
}
background thread
Dispatch.global(qos: .background) {
completion(.failure(error))
}
I used this solution:
guard let currentDispatch = OperationQueue.current?.underlyingQueue else {
return
}
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
currentDispatch.async {
completion(.failure(error))
}
}
}
Have not tested it too much, but quick debugging shows what I wanted to achieve,
Background
The function below calls two functions, which both access an API, retrieve JSON data, parse through it, etc, and then take that data and populates the values of an object variable in my View Controller class.
func requestWordFromOxfordAPI(word: String, completion: (_ success: Bool) -> Void) {
oxfordAPIManager.fetchDictData(word: word)
oxfordAPIManager.fetchThesData(word: word)
completion(true)
}
Normally, if there was only one function fetching data, and I wanted to call a new function that takes in the data results and does something with them, I would use a delegate method and call it within the closure of the data fetching function.
For Example:
Here, I fetch data from my firebase database and if retrieving the data is succesful, I call self.delegate?.populateWordDataFromFB(result: combinedModel). Since closures occur on separate thread, this ensures that the populateWordDataFromFB function runs only once retrieving the data has finished. Please correct me if I am wrong. I have just recently learned this and am still trying to see the whole picture.
func readData(word: String) {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
self.delegate?.populateWordDataFromFB(result: combinedModel)
} else {
self.delegate?.fbDidFailWithError(error: nil, summary: "\(word) not found, requesting from OxfordAPI")
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
}
case .failure(let error):
self.delegate?.fbDidFailWithError(error: error, summary: "Error decoding CombinedModel")
}
}
}
Also notice from the above code that if the data is not in firebase, I call the delegate method below, which is where I am running into my issue.
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
My Issue
What I am struggling with is the fact that the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions both have closures.
The body of these functions look like this:
if let url = URL(string: urlString) {
var request = URLRequest(url: url)
request.addValue(K.APISettings.acceptField, forHTTPHeaderField: "Accept")
request.addValue(K.APISettings.paidAppID , forHTTPHeaderField: "app_id")
request.addValue(K.APISettings.paidAppKey, forHTTPHeaderField: "app_key")
let session = URLSession.shared
_ = session.dataTask(with:request) { (data, response, error) in
if error != nil {
self.delegate?.apiDidFailWithError(error: error, summary: "Error performing task:")
return
}
if let safeData = data {
if let thesaurusModel = self.parseThesJSON(safeData) {
self.delegate?.populateThesData(thesModel: thesaurusModel, word: word)
}
}
}
.resume()
} else {print("Error creating thesaurus request")}
I assume both of these functions are running on separate threads in the background. My goal is to call another function once both the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions run. These two functions will populate the values of an object variable in my view controller which I will use in the new function. I don't want the new function to be called before the object variable in the view controller is populated with the right data so I tried to implement a completion handler. The completion handler function is being called BEFORE the two functions terminate, so when the new function tries to access the object variable in the View Controller, it's empty.
This is my first time trying to implement a completion handler and I tried to follow some other stack overflow posts but was unsuccessful. Also if this is the wrong approach let me know too, please. Sorry for the long explanation and thank you for any input.
Use DispatchGroup for this,
Example:
Create a DispatchGroup,
let group = DispatchGroup()
Modify the requestWordFromOxfordAPI(word: completion:) method to,
func requestWordFromOxfordAPI(word: String, completion: #escaping (_ success: Bool) -> Void) {
fetchDictData(word: "")
fetchThesData(word: "")
group.notify(queue: .main) {
//code after both methods are executed
print("Both methods executed")
completion(true)
}
}
Call enter() and leave() methods of DispatchGroup at the relevant places in fetchDictData(word:) and fetchThesData(word:) methods.
func fetchDictData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
func fetchThesData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
At last call requestWordFromOxfordAPI(word: completion:)
requestWordFromOxfordAPI(word: "") { (success) in
print(success)
}
I use the following piece of code to generate a cold RxSwift Observable:
func doRequest<T :Mappable>(request:URLRequestConvertible) -> Observable<T> {
let observable = Observable<T>.create { [weak self] observer in
guard let self = self else { return Disposables.create() }
self.session.request(request).validate().responseObject { (response: AFDataResponse<T>) in
switch response.result {
case .success(let obj):
observer.onNext(obj)
observer.onCompleted()
case .failure(let error):
let theError = error as Error
observer.onError(theError)
}
}
return Disposables.create()
}
return observable
}
where Mappable is an ObjectMapper based type, and self.session is an Alamofire's Session object.
I can't find an equivalent to Observable.create {...} in Apple's Combine framework. What I only found is URLSession.shared.dataTaskPublisher(for:) which creates a publisher using Apple's URLSession class.
How can I convert the above observable to an Alamofire Combine's publisher ?
EDIT:
using the solution provided by rob, I ended up with the following:
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
func doRequest<T>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> where T : Mappable {
Deferred { [weak self] () -> Future<T, AFError> in
guard let self = self else {
return Future<T, AFError> { promise in
promise(.failure(.explicitlyCancelled)) }
}
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}
}
.handleEvents(receiveCompletion: { completion in
if case .failure (let error) = completion {
//handle the error
}
})
.receive(on: self.apiQueue)
.eraseToAnyPublisher()
}
EDIT2: I have to remove the private queue since it's not needed, Alamofire does the parsing the decoding on its own, so remove the queue and its usages (.receive(on: self.apiQueue))
You can use Future to connect responseObject's callback to a Combine Publisher. I don't have Alamofire handy for testing, but I think the following should work:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}.eraseToAnyPublisher()
}
Note that this is somewhat simpler than the RxSwift version because promise takes a Result directly, so we don't have to switch over response.result.
A Future is sort of a “lukewarm” publisher. It is like a hot observable because it executes its body immediately and only once, so it starts the Alamofire request immediately. It is also like a cold observable, because every subscriber eventually receives a value or an error (assuming you eventually call promise). The Future only executes its body once, but it caches the Result you pass to promise.
You can create a truly cold publisher by wrapping the Future in a Deferred:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Deferred {
Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result) }
}
}.eraseToAnyPublisher()
}
Deferred calls its body to create a new inner Publisher every time you subscribe to it. So each time you subscribe, you'll create a new Future that will immediately start a new Alamofire request. This is useful if you want to use the retry operator, as in this question.
How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a URLSessionProtocol
protocol URLSessionProtocol {
func loadData(from url: URL) -> URLSession.DataTaskPublisher
}
class Proxy {
private let urlSession: URLSessionProtocol
init(urlSession: URLSessionProtocol) {
self.urlSession = urlSession
}
func get(url: URL) -> AnyPublisher<Data, ProxyError> {
// Using urlSession.loadData(from: url)
}
}
This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.
Is it possible to do the same with the Combine Framework?
In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:
let mockPublisher = Just(MockData()).eraseToAnyPublisher()
However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:
Why does Combine's receive(on:) operator swallow errors?
The best way to test your client is to use URLProtocol.
https://developer.apple.com/documentation/foundation/urlprotocol
You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests.
Tests more reliable, faster, and you got the control!
You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way
But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...
I hope it'll help you!
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).
I want my application to return to the original API call. Here is a sample code:
func getDataFromServer()
{
guard let apiUrl = URL(string: endPoint) else {
return
}
getAccessTokenValue( completionHandler:{ accesstoken in
//if access token is valid continue to get data
//.......
let task = self.apiSession.dataTask(with: apiRequest, completionHandler: {
(data, response, error) in
//Manage response/data/error
})
task.resume()
})
}
func getAccessTokenValue(completionHandlerResult : #escaping (_ accesstoken : String) -> ())
{
var hasAccessTokenExpired = true
// do some calculation
//check if access token has expired
if hasAccessTokenExpired
{
self.renewAccessToken(completionHandler: { accessTokenValue in
completionHandlerResult(accessTokenValue)})
}
else{
completionHandlerResult( accessToken) //return the current access token
}
}
func static func renewAccessToken (completionHandler : #escaping (_ accessTokenValue:String) -> ()) //
{
//another API call to refresh Access Token
//.......
let task = self.apiSession.dataTask(with: apiRequest, completionHandler: {
(data, response, error) in
//Manage response/data/error
//read access token from response
completionHandler(accessToken)
})
task.resume()
}
The problem I am facing is when my access token is no longer valid, the API for refresh token is fired but by the time the data task in renewAccessToken is completed, my application has already executed the end of getAccessTokenValue and getDataFromServer. So the API which should have been executed after getting the refreshed access token (in getDataFromServer's completion handler for getAccessTokenValue), is no longer fired.
I am not able to figure out where I have gone wrong.
Resolved the issue. It was a problem with completion handler not being called when the API failed. I added that and now it works perfectly.