URLSessionTask instance variable vs local variable - who owns the reference? - ios

Please refer to the code snippet below (certain parts not relevant to question are omitted)
In WebService1, dataTask is an instance variable/property whereas in WebService2, dataTask is a local variable inside the function callWebService.
final class WebService1 {
let urlSession = URLSession(configuration: .default)
// 1. data task is a private property of PNWebService here
private var dataTask: URLSessionDataTask?
func callWebService(completion: () -> ()) {
var urlRequest = URLRequest(url: url)
dataTask = urlSession.dataTask(with: urlRequest) {
// task complete
completion()
}
dataTask?.resume()
}
}
final class WebService2 {
let urlSession = URLSession(configuration: .default)
func callWebService(completion: () -> ()) {
var urlRequest = URLRequest(url: url)
// 2. data task is a local variable here
var dataTask = urlSession.dataTask(with: url) {
// task complete
completion()
}
dataTask.resume()
}
}
Clients an call these two services in the usual way:
let ws1 = WebService1()
ws1.callWebService() {
print("1. complete")
}
let ws2 = WebService2()
ws2.callWebService() {
print("2. complete")
}
Q1) Who owns a strong reference to dataTask in WebService2 so that it is not deallocated before the completion handler is called?
Q2) From a client perspective what is the difference at runtime between WebService1 & WebService2?

Are you asking which pattern is correct? Neither. The URLSession owns the data task and manages its memory as soon as you resume it for the first time, so there is no need for you to keep any reference to it, unless you plan to do something else with that reference such as configuring the task further or cancelling the operation later. Generally it is sufficient and quite usual to say
urlSession.dataTask(with:url) { data, resp, err in
// whatever
}.resume()

Related

Http Request fails with response status code : 502

In a loop I'm hitting like 1000 GET requests. Since there are too many requests and the gateway could not handle it, it fails with the error code 502. Whats the best way to solve this. My code as bellow.
//In the array there are about 10000 ids
for id in Array {
Network.shared.getData(accountId: id ?? "", successBlock: {(result) in
//Save results to coredata
}) {(errorCode:Int, error:String) in
print(errorCode, error)
}
}
My Singleton Network later that ill be calling in the above method as bellow.
class Network: NSObject {
//These blocks catches success & failures
typealias SuccessBlock = (Any) -> Void
typealias FailureBlock = (_ errorCode: Int, _ error: String) -> Void
private static var networkCalls: Network?
private override init() {}
public static var shared: Network {
if networkCalls == nil {
networkCalls = Network()
}
return networkCalls!
}
private lazy var configurationManager: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.allowsExpensiveNetworkAccess = true
configuration.allowsConstrainedNetworkAccess = true
configuration.allowsCellularAccess = true
configuration.timeoutIntervalForRequest = 60
configuration.timeoutIntervalForResource = 60
configuration.httpMaximumConnectionsPerHost = 2
let manager = URLSession(configuration: configuration)
return manager
}()
Inside the above class I use a generic method to call HTTP request (Here Im not showing the full method with all the parameters, but you can get an idea what it looks like)
private func performWebServiceRequest(with url: URL, contentType: CONTENT_TYPE? = nil, requestOptions: [String: String]?,successBlock: #escaping SuccessBlock, failureBlock: #escaping FailureBlock) {
let request = NSMutableURLRequest(url: url)
request.httpMethod = requestType // As in "POST", "GET", "PUT" or "DELETE"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let task = configurationManager.dataTask(with: request as URLRequest) { (data, response, error) in
guard let data: Data = data, let response: URLResponse = response, error == nil else
{return}
do {
let result = try JSONDecoder().decode(type, from: data)
successBlock(result)
} catch {
failureBlock(0,errorMessage)
}
}
task.resume()
}
You could use DispatchSemaphore to limit the number of parallel running tasks, like this:
var countedSemaphore = DispatchSemaphore(value:500) // Allow 500 parallel network calls
var queue = DispatchQueue(label: "runner", qos: .background, attributes: .concurrent)
//In the array there are about 10000 ids
for id in Array {
queue.async { //(1)
countedSemaphore.wait() // (2)
Network.shared.getData(accountId: id ?? "", successBlock: {(result) in
countedSemaphore.signal() // (3a)
//Save results to coredata
}) {(errorCode:Int, error:String) in
countedSemaphore.signal() // (3b)
print(errorCode, error)
}
}
}
The example uses a custom concurrent queue (1) to create tasks that will start the network fetching.
In the task, we first wait (2) for the semaphore. This call will simply decrement the semaphore's counter, as long as the counter is larger than zero. If the counter is zero, the wait call blocks.
After wait returns, we call getData.
In the success (3a) and error (3b) handlers, we call signal on the semaphore, to indicate that we've finished the network call. signal will increment the counter or wake up any waiting call.
This construct ensures that at utmost 500 getData calls are running in parallel

How to cancel a URL session request

I am upload multiple image to server using convert image to base64 and send image in a API as a parameter. But when we call api again and again then how to stop api calling on button click. I am using below code to call API.
Thanks in advance
let urlPath: String = "URL"
let url: URL = URL(string: urlPath)!
var request1 = URLRequest(url: url)
request1.httpMethod = "POST"
let stringPost="imgSrc=\(image)"
let data = stringPost.data(using: String.Encoding.utf8)
// print("data\(data)")
request1.httpBody=data
request1.timeoutInterval = 60
let _:OperationQueue = OperationQueue()
let task = session.dataTask(with: request1){data, response, err in
do
{
if data != nil
{
print("data\(String(describing: data))")
if let jsonResult = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary
{
DispatchQueue.main.async
{
print("json\(jsonResult)")
}
}
}
catch let error as NSError
{
DispatchQueue.main.async
{
print("error is \(error)")
print("error desc \(error.localizedDescription)")
}
}}
task.resume()
Make the object task as a global variable, then you can cancel it anywhere by:
task.cancel()
Alternatively, if the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
If you don't want to allow API call again if there is any previous download is on progress, you can do as follows,
Make your task(URLSessionDataTask type) variable as global variable in the class as follows,
let task = URLSessionDataTask()
Then on your button action do as below by checking the task download status,
func uploadButtonPressed() {
if task.state != .running {
// Make your API call here
} else {
// Dont perform API call
}
}
You can make use following states like running which is provide by URLSessionDataTask class and do action accordingly as per your need,
public enum State : Int {
case running
case suspended
case canceling
case completed
}
You can check result of your task. And if everything is alright you can
task.resume()
but if not
task.cancel()

How do I optimize chaining async requests in Swift

I have code that do requests chaining like A->B->C and I am using URLSession all requests are done in right order and with expected behavior. But i am wondering how I can optimize this chaining because it looks quite complex and not reusable. I am looking for the suggestion how I can do this chaining in more flexible way.
My code:
URLSession.shared.dataTask(with: URLRequest(url: URL(string: "first")!)){ data , res , err in
let second = URLRequest(url: URL(string: "second")!)
URLSession.shared.dataTask(with: second){ data , res , err in
let third = URLRequest(url: URL(string: "second")!)
URLSession.shared.dataTask(with:third){ data , res , err in
}.resume()
}.resume()
}.resume()
Actually you can use dependencies using OperationQueues like below:
func operationQueueWithBlockandCancel(){
let mainQueue = OperationQueue.main
let operationBlock1 = BlockOperation()
let operationBlock2 = BlockOperation()
let operationBlock3 = BlockOperation()
operationBlock1.addExecutionBlock {
//Any task
}
operationBlock2.addExecutionBlock {
//Any task
}
operationBlock3.addExecutionBlock {
//Any task
}
//Add dependency as required
operationBlock3.addDependency(operationBlock2)
operationBlock2.addDependency(operationBlock1)
opQueue.addOperations([operationBlock2,operationBlock1,operationBlock3,], waitUntilFinished: false)
}
As #Paulw11 suggested:
PromiseKit + PMKFoundation
import PromiseKit
import PMKFoundation
let session = URLSession.shared
firstly {
session.dataTask(with: URLRequest(url: URL(string: "first")!))
} .then { data in
session.dataTask(with: URLRequest(url: URL(string: "second")!))
} .then { data in
session.dataTask(with: URLRequest(url: URL(string: "third")!))
} .then { data -> () in
// The data here is the data fro the third URL
} .catch { error in
// Any error in any step can be handled here
}
With 1 (and only 1) retry, you can use recover. recover is like catch except it's expected that the previous then can be retried. However, this is not a loop and executes only once.
func retry(url: URL, on error: Error) -> Promise<Data> {
guard error == MyError.retryError else { throw error }
// Retry the task if a retry-able error occurred.
return session.dataTask(with: URLRequest(url: url))
}
let url1 = URL(string: "first")!
let url2 = URL(string: "second")!
let url3 = URL(string: "third")!
let session = URLSession.shared
firstly {
session.dataTask(with: URLRequest(url: url1))
} .then { data in
session.dataTask(with: URLRequest(url: url2))
} .recover { error in
retry(url: url2, on: error)
} .then { data in
session.dataTask(with: URLRequest(url: url3))
} .recover { error in
retry(url: url3, on: error)
} .then { data -> () in
// The data here is the data fro the third URL
} .catch { error in
// Any error in any step can be handled here
}
NOTE: to make this work without specifying return types and needing a return statement, I need the then and recover to be 1 line exactly. So I create methods to do the processing.

what am I doing wrong Swift 3 http request and response

I'm having big problems in a project I'm currently working with.
I've been reading about URLSession various places but all of them seem to be outdated and refers to NSURLSession I thought that they would be fairly similar and they probably are but for a newbie like me I'm lost. what I do is not working and I do not like solutions I find because they all do their work in a controller..
http://swiftdeveloperblog.com/image-upload-with-progress-bar-example-in-swift/
this one for instance. I'm using the PHP script but wanted to make a networking layer I could invoke and use at will. but I'm lacking a good resource from where I could learn about how to use this api.
every place I find is similar to the link above or older. the few newer seem to also follow the pattern without really explaining how to use this api.
at the same time I'm new to the delegate pattern in fact I only know that it is something that is heavily used in this Api but I have no IDEA how or why.
Basically I need help finding my way to solve this problem here:
I've tried to do something like this:
public class NetworkPostRequestor:NSObject,NetworkPostRequestingProtocol,URLSessionTaskDelegate,URLSessionDataDelegate
{
public var _response:HTTPURLResponse
public override init()
{
_response = HTTPURLResponse()
}
public func post(data: Data, url: URL)
{
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration,delegate: self, delegateQueue: OperationQueue.main)
let task = session.uploadTask(with: request, from: data)
task.resume()
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void)
{
_response = response as! HTTPURLResponse
}
}
however I never even hit the PHPserver. the server when hit will say something like this in the terminal:
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64265 [200]: /
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64266 [404]: /favicon.ico - No such file or directory
Well that is when I hit it with my browser and there is no image with it. but alt least I know that it will write something with the terminal if it hits it. Nothing happens And without a resource to teach me this api I'm afraid I will never learn how to fix this or even if I'm doing something completely wrong.
I'm using Swift 3 and Xcode 8.2.1
Edit:
I've added this method to the class and found that I hit it every single time.
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
{
_error = error.debugDescription
}
the debug description have this string "some"
I never used this exact procedure with tasks but rather use the methods with callback. I am not sure if in the background there should be much of a difference though.
So to generate the session (seems pretty close to your):
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
Then I generate the request which stupidly enough needs an URL in the constructor:
var request = URLRequest(url: URL(string: "www.nil.com")!) // can't initialize without url
request.url = nil
Adding url with query parameters (you can just set the URL in your case, I have a tool to handle a few cases):
fileprivate func injectQueryParameters(request: inout URLRequest) {
if let query = queryParameters.urlEncodedString {
let toReturn = endpoint.url + "?" + query
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
} else {
let toReturn = endpoint.url
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
}
}
Then the form parameters. We mostly use JSON but anything goes here:
fileprivate func injectFormParameters( request: inout URLRequest) {
if let data = rawFormData {
request.httpBody = data
} else if let data = formParameters.urlEncodedString?.data(using: .utf8) {
request.httpBody = data
}
}
And the headers:
fileprivate func injectHeaders(request: inout URLRequest) {
headers._parameters.forEach { (key, value) in
if let stringValue = value as? String {
request.setValue(stringValue, forHTTPHeaderField: key)
}
}
}
So in the end the whole call looks something like:
class func performRequest(request: URLRequest, callback: (([String: Any]?, NSError?) -> Void)?) {
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { data, response, error in
// Response is sent here
if let data = data {
callback?((try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as [String: Any]?, error)
} else {
callback?(nil, error)
}
}
task.resume()
}
I hope this puts you on the right track. In general you do have a few open source libraries you might be interested in. Alamofire is probably still used in most cases.

NSURLSession doesn't return data in first call

In general, it is necessary to implement a class for the network. This is a class that will take a URL, and to give data. All this is done in order not to score an extra logic controllers. I encountered such a problem that when you first create a View, the data do not come. That's Network Class:
private static var dataTask: NSURLSessionDataTask?
private static var dataJSON: NSData?
private static var sessionConfig: NSURLSessionConfiguration = {
var configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.allowsCellularAccess = false
configuration.HTTPMaximumConnectionsPerHost = 2
configuration.HTTPAdditionalHeaders = ["Accept": "application/json"]
configuration.timeoutIntervalForRequest = 30.0
configuration.timeoutIntervalForResource = 60.0
return configuration
}()
static func getListObjectsBy(url: String?) -> NSData? {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) { (data, response, error) in
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
dataJSON = data
} else {
print("Bad request")
}
}
}
dataTask?.resume()
log.debug("DataTask Resume")
return dataJSON
}
Method viewDidLoad in my main controller:
let response = Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5")
print(String(response))
My log say me, that data return nil. Notes, i'm switch between controllers with help SWRevealViewController. When reloading the main view controller, the data is returned. What me do?
enter image description here
You seem to be misunderstanding that this is an asynchronous call.
static func getListObjectsBy(url: String?) -> NSData? {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) { (data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
// this won't happen until the data comes back from the remote call.
dataJSON = data
} else {
print("Bad request")
}
}
}
// This code here does not wait for the response from the remote.
// The call to the remote is sent then this code
// is immediately executed WITHOUT WAITING
dataTask?.resume()
log.debug("DataTask Resume")
// dataJSON will be nil until the remote answers.
return dataJSON
}
When you do this:
let response = Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5")
print(String(response))
The remote has not answered yet so you will get nil.
Your next question might be "what do I do about this?". The answer isn't clear without knowing everything else that you are doing.
Threads
Multiple threads of execution is like two programs running at the same time. Think of 2 people working on two different tasks at the same time. In order to keep the interface very responsive, iOS uses one thread of execution for updating the screen. If a process has to run that take a long time, we would not want the screen to wait until that is done. Let's say you have to fetch data from some remote system and that remote system is slow, your device would sit there frozen until the response came back. To avoid this, activities like calls to remote systems are done in another thread. The request is sent to the operating system itself and the operating system is told to call back when the operation is done.
This is what is happening here.
Sets up the request to send to the operating system.
dataTask = session.dataTaskWithURL(NSURL(string: url!)!)
Tells the operating system to start doing the work.
dataTask?.resume()
This block is the callback AKA the closure. iOS will run this code when the remote call is done.
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) {
// Closure starts here
// Gets called when the remote has sent a response.
(data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
etc
}
This means you must wait until the response has come back before printing your output. You can use a closure in your function to do this.
public typealias CompletionHandler = (data: NSData?, error: NSError?) -> Void
static func getListObjectsBy(url: String?, completion: CompletionHandler) {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) {
(data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
// this won't happen until the data comes back from the remote call.
} else {
print("Bad request")
}
}
// Call your closure
completion(data, error)
}
// This code here does not wait for the response from the remote.
// The call to the remote is sent then this code
// is immediately executed WITHOUT WAITING
dataTask?.resume()
log.debug("DataTask Resume")
}
In your calling code you would do this:
Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5") {
(data, error) in
if let data == data {
print(data)
}
}

Resources