Access dataTask from inside completionHandler - swift

I am using URLSession.shared.dataTask(with:completionHandler:) to access an API which, on certain circumstances, won't respond immediately. I also need to cancel pending requests on certain conditions.
My current implementation uses the completionHandler approach and multiple requests can happen in parallel. This makes it very inconvenient to change the pattern to a delegate.
What I am trying is to store every pending task in a set, and then remove the task from the set once they are done. The problem is accessing the return of the dataTask from inside the completion handler seems very cumbersome, to a point I am not sure this is even correct.
class API {
private var tasks: Set<URLSessionTask> = []
func sendRequest(path: String, body: [String: Any], completionHandler: #escaping #Sendable (APIResponse) -> Void) {
var request = // ... create request
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
// ... handle response
tasks.remove(task)
}
tasks.insert(task)
task.resume()
}
func cancelAllTasks() {
for task in tasks {
task.cancel()
tasks.remove(task)
}
}
}
I either get a compiler error Closure captures 'task' before it is declared or a warning 'task' mutated after capture by sendable closure.
Is there a better way of doing this?

The problem is that your completion handler tries to reference the task which isn't yet defined when you create the completion handler.
Assuming your requests are unique, you could use a dictionary of your data tasks keyed by their URLRequest. Then add a method taskComplete(forRequest:) to your API class. Have your completion handler send that message to the API class.
The API class could remove the task using the request as a key (I checked. URLRequest is Hashable.
If your requests aren't certain to be unique, create a request struct that contains the URLTask and the Date when the task is created. The Date should make the hash of the request struct unique.

Related

Is there a way to enforce serial scheduling of async/await calls similar to a GCD serial queue?

Using Swift's new async/await functionality, I want to emulate the scheduling behavior of a serial queue (similar to how one might use a DispatchQueue or OperationQueue in the past).
Simplifying my use case a bit, I have a series of async tasks I want to fire off from a call-site and get a callback when they complete but by design I want to execute only one task at a time (each task depends on the previous task completing).
Today this is implemented via placing Operations onto an OperationQueue with a maxConcurrentOperationCount = 1, as well as using the dependency functionality of Operation when appropriate. I've build an async/await wrapper around the existing closure-based entry points using await withCheckedContinuation but I'm trying to figure out how to migrate this entire approach to the new system.
Is that possible? Does it even make sense or am I fundamentally going against the intent of the new async/await concurrency system?
I've dug some into using Actors but as far as I can tell there's no way to truly force/expect serial execution with that approach.
--
More context - This is contained within a networking library where each Operation today is for a new request. The Operation does some request pre-processing (think authentication / token refreshing if applicable), then fires off the request and moves on to the next Operation, thus avoiding duplicate authentication pre-processing when it is not required. Each Operation doesn't technically know that it depends on prior operations but the OperationQueue's scheduling enforces the serial execution.
Adding sample code below:
// Old entry point
func execute(request: CustomRequestType, completion: ((Result<CustomResponseType, Error>) -> Void)? = nil) {
let operation = BlockOperation() {
// do preprocessing and ultimately generate a URLRequest
// We have a URLSession instance reference in this context called session
let dataTask = session.dataTask(with: urlRequest) { data, urlResponse, error in
completion?(/* Call to a function which processes the response and creates the Result type */)
dataTask.resume()
}
// queue is an OperationQueue with maxConcurrentOperationCount = 1 defined elsewhere
queue.addOperation(operation)
}
// New entry point which currently just wraps the old entry point
func execute(request: CustomRequestType) async -> Result<CustomResponseType, Error> {
await withCheckedContinuation { continuation in
execute(request: request) { (result: Result<CustomResponseType, Error>) in
continuation.resume(returning: result)
}
}
}
A few observations:
For the sake of clarity, your operation queue implementation does not “[enforce] the serial execution” of the network requests. Your operations are only wrapping the preparation of those requests, but not the performance of those requests (i.e. the operation completes immediately, and does not waiting for the request to finish). So, for example, if your authentication is one network request and the second request requires that to finish before proceeding, this BlockOperation sort of implementation is not the right solution.
Generally, if using operation queues to manage network requests, you would wrap the whole network request and response in a custom, asynchronous Operation subclass (and not a BlockOperation), at which point you can use operation queue dependencies and/or maxConcurrentOperationCount. See https://stackoverflow.com/a/57247869/1271826 if you want to see what a Operation subclass wrapping a network request looks like. But it is moot, as you should probably just use async-await nowadays.
You said:
I could essentially skip the queue entirely and replace each Operation with an async method on an actor to accomplish the same thing?
No. Actors can ensure sequential execution of synchronous methods (those without await calls, and in those cases, you would not want an async qualifier on the method itself).
But if your method is truly asynchronous, then, no, the actor will not ensure sequential execution. Actors are designed for reentrancy. See SE-0306 - Actors » Actor reentrancy.
If you want subsequent network requests to await the completion of the authentication request, you could save the Task of the authentication request. Then subsequent requests could await that task:
actor NetworkManager {
let session: URLSession = ...
var loginTask: Task<Bool, Error>?
func login() async throws -> Bool {
loginTask = Task { () -> Bool in
let _ = try await loginNetworkRequest()
return true
}
return try await loginTask!.value
}
func someOtherRequest(with value: String) async throws -> Foo {
let isLoggedIn = try await loginTask?.value ?? false
guard isLoggedIn else {
throw URLError(.userAuthenticationRequired)
}
return try await foo(for: createRequest(with: value))
}
}
Perhaps this is unrelated, but if you are introducing async-await, I would advise against withCheckedContinuation. Obviously, if iOS 15 (or macOS 12) and later, I would use the new async URLSession methods. If you need to go back to iOS 13, for example, I would use withTaskCancellationHandler and withThrowingCheckedContinuation. See https://stackoverflow.com/a/70416311/1271826.

Is it correct to call a Swift closure multiple times

I'm designing a Swift API where I define a protocol EventTransmitter with a method to transmit a batch of Events. Processing a batch of Events can end with a list of both success and failed events.
So any implementation of this protocol should call a completion with the events that failed and/or the events that succeeded.
public enum EventResult {
/// When the event was transmitted correctly.
case success([Event])
/// When the event failed and should be discarded
case failure([Event])
}
public protocol EventTransmitter {
func transmit(events: [Event], completion: #escaping (EventResult) -> Void)
}
With the above protocol, whoever implements the protocol has to call the completion twice:
public class HTTPTransmitter: EventTransmitter {
public func transmit(events: [Event], completion: #escaping (EventResult) -> Void) {
let failedEvents = ...
let successEvents = ...
completion(.failure(failedEvents))
completion(.success(successEvents))
}
}
And the usage would be
transmitter.transmit(events: events, completion: { result in
switch result {
case .success(let events):
// handle success events
case .failure(let events):
// handle failed events
}
})
Is it correct to require to call one completion closure several times?
Or would it be more appropriate to have different closures for each case:
public protocol EventTransmitter {
typealias EventCallback = (([Event]) -> Void)
func transmit(events: [Event], onSuccess: #escaping EventCallback, onFailure: #escaping EventCallback)
}
If you wanted to do something like this where a list of failed events and succeeded events can happen then I would probably combine them both into the same callback...
A callback something like...
typealias EventCallBack = (succeeded: [Event], failed: [Event]) -> ()
Then you only need to call the callback once.
In the case where you have an enum that captures the state of the callback then I'd suggest only making it run once.
This is purely from an API point of view. What does it mean to deal with a success twice? Or three times? Or a success and then a failure compared to a failure then a success. etc...
Of course, you CAN call it multiple times and there are places where you might want to. For instance, storing a closure and calling it in response to a user interaction or something.
But for something like this where there is one request I would suggest calling it once with all the necessary information.
Another possible alternative
Maybe what you could do is incorporate the success and failure closure handling into the Event itself.
That way if an event fails you can call event.failed() and if it succeeded... event.succeeded() or something.
I don't know in this specific case if that is a good idea but it is certainly another option :D
Suggested by vacawama
You could also define your enum like...
enum EventResult {
case success(Event)
case failed(Event)
}
And then make your callback like...
([EventResult]) -> ()
That way you just call it once with a single array of all of the individual results.
This would be useful if it is important for the order of results to be the same as the order of events.

Swift - Protocol can only be used as a generic constraint because it has Self or associated type requirements

I'm working on an app which needs to query multiple APIs. I've come up with classes for each API provider (and in more extreme cases, a class for each specific API Endpoint). This is because each API query is expected to return a very strict type of response, so if an API can, for instance, return both user profiles and profile pictures, I only want a response to be specific to either of those.
I've implemented it roughly in the following manner:
protocol MicroserviceProvider {
associatedtype Response
}
protocol ProfilePictureMicroserviceProvider: MicroserviceProvider {
func getPicture(by email: String, _ completion: (Response) -> Void)
}
class SomeProfilePictureAPI: ProfilePictureMicroserviceProvider {
struct Response {
let error: Error?
let picture: UIImage?
}
func getPicture(by email: String, _ completion: (Response) -> Void) {
// some HTTP magic
// will eventually call completion(_:) with a Response object
// which either holds an error or a UIImage.
}
}
Because I want to be able to Unit Test classes that will rely on this API, I need to be able to inject that profile picture dependency dynamically. By default it will use SomeProfilePictureAPI but when running tests I will be able to replace that with a MockProfilePictureAPI which will still adhere to ProfilePictureMicroserviceProvider.
And because I'm using associated types, I need to make classes that depend on ProfilePictureMicroserviceProvider generic.
At first, I naively did try to write my view controller like such
class SomeClass {
var profilePicProvider: ProfilePictureMicroserviceProvider
}
But that just led the frustratingly famous 'Protocol ProfilePictureMicroserviceProvider can only be used as a generic constraint because it has Self or associated type requirements' compile-time error.
Now I've been reading up on the issue over the last couple days, trying to wrap my head around Protocols with Associated Types (PATS), and figured I'd take the route of generic classes like such:
class SomeClass<T: ProfilePictureMicroserviceProvider> {
var profilePicProfider: T = SomeProfilePictureAPI()
}
But even then I get the following error:
Cannot convert value of type 'SomeProfilePictureAPI' to specified type 'T'
Even though having T being constrained to the ProfilePictureMicroserviceProvider protocol, and having SomeProfilePictureAPI adhere to it...
Basically the main idea was to reach 2 objectives: enforce Microservice structure with mandatory Response type, and make each Microservice mock-able for unit tests of dependent classes.
I'm now stuck with choosing either one of the two as I can't seem to make it work. Any help telling me what I'm doing wrong would be most welcome.
I've also had a look at type-erasure. But this to me seems very whacky and quite an effort for something that looks wrong on many aspects.
So basically my question is two-fold: how can I enforce my Microservices to define their own Response type ? And how can I easily replace them by mock microservices in classes that depend on them ?
You have to turn these requirements around;
Instead of injecting a MicroServiceProvider into each request, you should write a generic MicroService 'Connector' Protocol that should define what it expects from each request, and what each request expects it to return.
You can then write a TestConnector which conforms to this protocol, so that you have complete control over how your requests are handled. The best part is, your requests won't even need to be modified.
Consider the following example:
protocol Request {
// What type data you expect to decode and return
associatedtype Response
// Turn all the data defined by your concrete type
// into a URLRequest that we can natively send out.
func makeURLRequest() -> URLRequest
// Once the URLRequest returns, decode its content
// if it succeeds, you have your actual response object
func decode(incomingData: Data?) -> Response?
}
protocol Connector {
// Take in any type conforming to Request,
// do whatever is needed to get back some potential data,
// and eventually call the handler with the expected response
func perform<T: Request>(request: T, handler: #escaping (T.Response?) -> Void)
}
These are essentially the bare minimum requirements to setup such a framework. In real life, you'll want more requirements from your Request protocol (such as ways to define the URL, request headers, request body, etc).
The best part is, you can write default implementations for your protocols. That removes a lot of boilerplate code! So for an actual Connector, you could do this:
extension Connector {
func perform<T: Request>(request: T, handler: #escaping (T.Response?) -> Void) {
// Use a native URLSession
let session = URLSession()
// Get our URLRequest
let urlRequest = request.makeURLRequest()
// define how our URLRequest is handled
let task = session.dataTask(with: urlRequest) { data, response, error in
// Try to decode our expected response object from the request's data
let responseObject = request.decode(incomingData: data)
// send back our potential object to the caller's completion block
handler(responseObject)
}
task.resume()
}
}
Now, with that, all you need to do is implement your ProfilePictureRequest like this (with extra example class variables):
struct ProfilePictureRequest: Request {
private let userID: String
private let useAuthentication: Bool
/// MARK: Conform to Request
typealias Response = UIImage
func makeURLRequest() -> URLRequest {
// get the url from somewhere
let url = YourEndpointProvider.profilePictureURL(byUserID: userID)
// use that URL to instantiate a native URLRequest
var urlRequest = URLRequest(url: url)
// example use: Set the http method
urlRequest.httpMethod = "GET"
// example use: Modify headers
if useAuthentication {
urlRequest.setValue(someAuthenticationToken.rawValue, forHTTPHeaderField: "Authorization")
}
// Once the configuration is done, return the urlRequest
return urlRequest
}
func decode(incomingData: Data?) -> Response? {
// make sure we actually have some data
guard let data = incomingData else { return nil }
// use UIImage's native data initializer.
return UIImage(data: data)
}
}
If you then want to send a profile picture request out, all you then need to do is (you'll need a concrete type that conforms to Connector, but since the Connector protocol has default implementations, that concrete type is mostly empty in this example: struct GenericConnector: Connector {}):
// Create an instance of your request with the arguments you desire
let request = ProfilePictureRequest(userID: "JohnDoe", useAuthentication: false)
// perform your request with the desired Connector
GenericConnector().perform(request) { image in
guard let image = image else { return }
// You have your image, you can now use that instance whichever way you'd like
ProfilePictureViewController.current.update(with: image)
}
And finally, to set up your TestConnector, all you need to do is:
struct TestConnector: Connector {
// define a convenience action for your tests
enum Behavior {
// The network call always fails
case alwaysFail
// The network call always succeeds with the given response
case alwaysSucceed(Any)
}
// configure this before each request you want to test
static var behavior: Behavior
func perform<T: Request>(request: T, handler: #escaping (T.Response?) -> Void) {
// since this is a test, you don't need to actually perform any network calls.
// just check what should be done
switch Self.behavior {
case alwaysFail:
handler(nil)
case alwaysSucceed(let response):
handler(response as! T)
}
}
}
With this, you can easily define Requests, how they should configure their URL actions and how they decode their own Response type, and you can easily write mocks for you connectors.
Of course, keep in mind that the examples given in this answer are quite limited in how they can be used. I would highly suggest you to take a look at this library I wrote. It extends this example in a much more structured way.

proper use of Alamofire queue

Here is the scenario, everything works but I get hanged up on the main queue. I have:
singleton class to manage API connection. Everything works (execution time aside....)
a number of view controllers calling GET API via the above singleton class to get the data
I normally call the above from either viewDidLoad or viewWillAppear
they all work BUT ....
if I call a couple of API methods implemented with Alamofire.request() with a closure (well, I need to know when it is
time to reload!), one of the two gets hung waiting for the default
(main) queue to give it a thread and it can take up to 20 seconds
if I call only one, do my thing and then call a POST API, this
latter one ends up in the same situation as (5), it takes a long
time to grab a slot in the default queue.
I am not specifying a queue in Alamofiore.request() and it sounds to me like I should so I tried it. I added a custom concurrent queue to my singleton API class and I tried adding that to my Alamofiore.request() .....and that did absolutely nothing. Help please, I must be missing something obvious?!
Here is my singleton API manager (excerpt) class:
class APIManager {
// bunch of stuff here
static let sharedInstance = APIController()
// more stuff here
let queue = DispatchQueue(label: "com.teammate.response-queue", qos: .utility, attributes: [.concurrent])
// more stuff
func loadSports(completion: #escaping (Error?) -> Void) {
let parameters: [String: Any?] = [:]
let headers = getAuthenticationHeaders()
let url = api_server+api_sport_list
Alamofire.request(url, method: .get, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString (queue: queue) { response in
if let json = response.result.value {
if let r = JSONDeserializer<Response<[Sport]>>.deserializeFrom(json: json) {
if r.status == 200 {
switch r.content{
case let content as [Sport]:
self.sports = content
NSLog("sports loaded")
completion(nil)
default:
NSLog("could not read the sport list payload!")
completion(GenericError.reportError("could not read sports payload"))
}
}
else {
NSLog("sports not obtained, error %d %#",r.status, r.error)
completion(GenericError.reportError(r.error))
}
}
}
}
}
// more stuff
}
And this is how I call the methods from APIManager once I get the sigleton:
api.loadSports(){ error in
if error != nil {
// something bad happened, more code here to handle the error
}
else {
self.someViewThingy.reloadData()
}
}
Again, it all works it is just that if I make multiple Alamofire calls from the same UIViewController, the first is fast, every other call sits for ever to get a spot in the queue an run.
UI updates must happen on the main queue, so by moving this stuff to a concurrent queue is only going to introduce problems. In fact, if you change the completion handler queue to your concurrent queue and neglect to dispatch UI updates back to the main queue, it's going to just make it look much slower than it really is.
I actually suspect you misunderstand the purpose of the queue parameter of responseString. It isn't how the requests are processed (they already happen concurrently with respect to the main queue), but merely on which queue the completion handler will be run.
So, a couple of thoughts:
If you're going to use your own queue, make sure to dispatch UI updates to the main queue.
If you're going to use your own queue and you're going to update your model, make sure to synchronize those updates with any interaction you might be doing on the main queue. Either create a synchronization queue for that or, easier, dispatch all model updates back to the main queue.
I see nothing here that justifies the overhead and hassle of running the completion handler on anything other than the main queue. If you don't supply a queue to responseString, it will use the main queue for the completion handlers (but won't block anything, either), and it solves the prior two issues.

Swift POST Request in same Thread

Hope you can help me. I want a swift function that make a post request and return the json data
so here is my class
import Foundation
class APICall {
//The main Url for the api
var mainApiUrl = "http://url.de/api/"
func login(username: String, password: String) -> String {
let post = "user=\(username)&password=\(password)";
let action = "login.php";
let ret = getJSONForPOSTRequest(action: action, post: post)
return ret;
}
//Function to call a api and return the json output
func getJSONForPOSTRequest(action: String, post: String) -> String {
var ret: String?
let apiUrl = mainApiUrl + action;
let myUrl = URL(string: apiUrl);
var request = URLRequest(url:myUrl!);
request.httpMethod = "POST";
let postString = post;
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil
{
print("error=\(error)")
return
}
print("response=\(response)")
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let login = parseJSON["Login"] as? String
print("login: \(login)")
ret = login
}
} catch {
print(error)
}
}
task.resume()
return ret!;
}
}
But ret is nil. In the debugger is see the inner of the task is called later by another thread?
How can if fix that?
Thank you guys
The data task completion closure is called on another thread and after the execution of the method is completed so you need to re-jig your code a bit. Instead of having a String return value for your getJSONForPOSTRequest, don't return anything and instead have an additional argument that is a closure and call that from within your dataTask closure instead.
func getJSONForPOSTRequest(action: String, post: String, completion: (string: String) -> Void) {
// ...
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
// ... (Convert data to string etc.)
completion(string: myString)
}
task.resume()
}
Remember, doing this means that the completion handler will be called once the network request completes and not right away.
EDIT:
Lets take this from the beginning. When you download something from the network in iOS you typically use NSURLSession. NSURLSession has a number of methods available to it for different means of interacting with the network, but all of these methods use a different thread, typically a background thread, which will do work independently of the rest of your code.
With this in mind, when you call the dataTask method you will notice that you have to add a completion closure as one of the parameters (notice in your example you are using something called a 'trailing closure' which is a closure that is the last argument in the method call that doesn't fall within the parenthesis of the method with the rest of the arguments). Think of a closure as a piece of code that is executed at a different time, it's not executed in line with the rest of the code around it (See the Swift documentation on closures here). In this case the closure will be called once the network request has been completed. Network requests aren't instant so we typically use a background thread to execute them while the user is shown an activity indicator etc and can still use the app. If we waited until the network request completed on the same thread as the rest of our code then it results in the app appearing laggy and even frozen which is terrible for users.
So going back to your example at hand; when you call your getJSONForPOSTRequest method the code within that method will complete and return before the network request has completed which is why we don't need to use a return value. Once the network request has completed your closure code will get called. Because the closure is called later it's also being called from an entirely different place within the code, in this case it's called from within iOS's network code. Because if this if you return a value from within the closure you will be trying to return the value to the network code which isn't what you want, you want to return the value to your own code.
To return the value of the network response to your code you need to define a closure (or a delegate, but I'm not going to go into that here) yourself. If you look at the example code above I've removed the return value from your getJSONForPOSTRequest method and added a new argument called 'completion', and if you look at the type of that argument you can see it's (string: String) -> Void, this defines a closure that passes in a string (the string that you will have downloaded from the network). Now that we have a closure thats within your method we can use this to call back to the caller of the getJSONForPOSTRequest with the data we have downloaded form the network.
Lets take your login method and see how we use getJSONForPOSTRequest within it:
func login(username: String, password: String, completion: (success: Bool) -> Void) {
let post = "user=\(username)&password=\(password)";
let action = "login.php";
let ret = getJSONForPOSTRequest(action: action, post: post) { string in
// This will be called once the network has responded and 'getJSONForPOSTRequest' has processed the data
print(string)
completion(success: true)
}
}
See that again we aren't returning anything directly from the login method as it has to rely on the a-synchronousness of calling off to the network.
It might feel by now that you are starting to get into something called 'callback hell', but this is the standard way to deal with networking. In your UI code you will call login and that will be the end of the chain. For example here is some hypothetical UI code:
func performLogin() {
self.activityIndicator.startAnimating()
self.apiCaller.login(username: "Joe", password: "abc123") { [weak self] success in
print(success)
// This will get called once the login request has completed. The login might have succeeded of failed, but here you can make the decision to show the user some indication of that
self?.activityIndicator.stopAnimating()
self?.loginCompleted()
}
}
Hopefully that clarifies a few things, if you have any other questions just ask.