I have a question using Swift 3, I am trying to access the task variable from inside my completion closure but it is not available. If I try to access "task" inside the block I get "error: use of unresolved identifier 'task'" What am I doing wrong?
typealias completionHandler = (data: Data?, httpResponse: HTTPURLResponse?, validServer: Bool, serverRealm: String?, serverVersion: String?) -> Void
typealias failureHandler = (data: Data?, response: URLResponse?, error: NSError?) -> Void
func ping(address: String, completionBlock: completionHandler, failureBlock: failureHandler?) -> URLSessionDataTask? {
guard var addressComponents = URLComponents(string: address) else {
let error = NSError(domain: "PING", code: 99, userInfo: [NSLocalizedDescriptionKey : "Invalid URL: \(address)"])
failureBlock?(data: nil, response: nil, error: error)
return nil
}
addressComponents.path = Paths.ping.rawValue
let request = URLRequest(url: addressComponents.url!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: gsTimeout)
//--------------------------------------------------------------------------------
// Ping the server and process the repsonse
//--------------------------------------------------------------------------------
var task: URLSessionDataTask!
task = session.dataTask(with: request) { (data, response, error) in
print("Task: \(task)")
if error == nil {
if let response = response as? HTTPURLResponse {
let results = self.parseHeader(response)
print("Results: \(results)")
if results.isServer == true {
completionBlock(data: data, httpResponse: response, validServer: results.isServer, serverRealm: results.realm, serverVersion: results.serverVersion)
}
}
} else {
failureBlock?(data: data, response: response, error: error)
}
}
task.taskDescription = Paths.ping.rawValue
task.resume()
return task
}
Updated with working code now. task is available if you use it inside the block. Thanks everyone
There is nothing wrong with your code. It compiles just fine.
You are unable to po the value of task using LLDB while paused in the debugger because you are paused inside the completion block. You never captured task in this code (you didn't refer to it), so it is not in scope here. Only outside scope actually referred to inside a closure is captured by the closure.
Related
I want to test an API without making a server call so I am mocking URLSession and URLSessionDataTask so that I can inject it in my API class.
class MockURLSession: URLSession {
private let mockTask: MockTask
var cachedUrl: URL?
init(data: Data?, urlResponse: URLResponse?, error: Error?) {
mockTask = MockTask(data: data, urlResponse: urlResponse, error:
error)
}
override func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.cachedUrl = url
mockTask.completionHandler = completionHandler
return mockTask
}
}
class MockTask: URLSessionDataTask {
private let data: Data?
private let urlResponse: URLResponse?
var completionHandler: ((Data?, URLResponse?, Error?) -> Void)!
init(data: Data?, urlResponse: URLResponse?, error: Error?) {
self.data = data
self.urlResponse = urlResponse
}
override func resume() {
DispatchQueue.main.async {
self.completionHandler(self.data, self.urlResponse, self.error)
}
}
}
Here when I am running this test it's going in my API class calling the getMovies method. Instance I am getting over there is kind of MockURLSession which is fine. Next moment it gives this ApiTests testGetMoviesSuccessReturnsMovies] : failed: caught "NSInvalidArgumentException", "-[MyAppTests.MockTask error]: unrecognized selector sent to instance 0x600002010500
func testGetMoviesSuccessReturnsMovies() {
let jsonData = "[{\"title\": \"Spider Man Far From Home\",\"detail\": \"The first Spider-Man featuring Tom Holland in the iconic role\"}]".data(using: .utf8)
var mockURLSession = MockURLSession(data: jsonData, urlResponse: nil, error: nil)
let apiRespository = APIRepository(session: mockURLSession)
let moviesExpectation = expectation(description: "movies")
var moviesResponse: Result<[Movie]>?
apiRespository.getMovies { (movies) in
moviesResponse = movies
moviesExpectation.fulfill()
}
waitForExpectations(timeout: 10) { (error) in
XCTAssertNotNil(moviesResponse)
}
}
Here is my protocol extension for api
extension Gettable {
func get<T:Decodable>(with decodingType: T.Type, url: String, session: URLSession, completion:#escaping(Result<T>) -> Void) {
let dataTask = session.dataTask(with: URL(string: url)!) { (data, response, error) in
guard data != nil && error == nil else {
return
}
do {
let decoder = JSONDecoder()
let parsedObj = try decoder.decode(T.self, from: data ?? Data())
completion(Result.success(parsedObj))
}
catch let parsedError {
completion(Result.failure(parsedError))
}
}
dataTask.resume()
}
}
Your help will be highly appreciated.
You are not overriding the method dataTask(with:completionHandler:) in your MockURLSession. So, the original dataTask(with:completionHandler:) of URLSession is called, which internally calls dataTaskForRequest:completion:.
If you are successfully overriding existing methods in the parent class, Swift compiler claims to prefix override keyword.
Move your nested dataTask(with:completionHandler:) out of init(data:urlResponse:error:).
init(data: Data?, urlResponse: URLResponse?, error: Error?) {
mockTask = MockTask(data: data, urlResponse: urlResponse, error:
error)
}
override func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.cachedUrl = url
mockTask.completionHandler = completionHandler
return mockTask
}
For edited part...
Seems inheritance of error property is not working properly. (Which may be a bug of Swift compiler related to bridging NSError and Error.)
Please try this:
class MockTask: URLSessionDataTask {
private let data: Data?
private let urlResponse: URLResponse?
private let _error: Error?
override var error: Error? {
return _error
}
var completionHandler: ((Data?, URLResponse?, Error?) -> Void)!
init(data: Data?, urlResponse: URLResponse?, error: Error?) {
self.data = data
self.urlResponse = urlResponse
self._error = error
}
override func resume() {
DispatchQueue.main.async {
self.completionHandler(self.data, self.urlResponse, self.error)
}
}
}
I cannot return data from my HTTPrequest and I can't get completion handlers to work either. So please assist me in my quest to solve this issue:
public static func createRequest(qMes: message, location: String, method: String) -> String{
let requestURL = URL(string: location)
var request = URLRequest(url: requestURL!)
request.httpMethod = method
request.httpBody = qMes.toString().data(using: .utf8)
let requestTask = URLSession.shared.dataTask(with: request) {
(data: Data?, response: URLResponse?, error: Error?) in
if(error != nil) {
print("Error: \(error)")
}
return String(data: data!, encoding: String.Encoding.utf8) as String!
}
requestTask.resume()
}
It is excpecting non-void return statement in void function. At this point I'm clueless...
You can use this completion block method to send the final response:
For Instance:
I have returned String in completion block, after successful response without error just pass the result in block.
public func createRequest(qMes: String, location: String, method: String , completionBlock: #escaping (String) -> Void) -> Void
{
let requestURL = URL(string: location)
var request = URLRequest(url: requestURL!)
request.httpMethod = method
request.httpBody = qMes.data(using: .utf8)
let requestTask = URLSession.shared.dataTask(with: request) {
(data: Data?, response: URLResponse?, error: Error?) in
if(error != nil) {
print("Error: \(error)")
}else
{
let outputStr = String(data: data!, encoding: String.Encoding.utf8) as String!
//send this block to required place
completionBlock(outputStr!);
}
}
requestTask.resume()
}
You can use this below code to execute the above completion block function:
self.createRequest(qMes: "", location: "", method: "") { (output) in
}
This will solve your following requirement.
{
(data: Data?, response: URLResponse?, error: Error?) in
if(error != nil) {
print("Error: \(error)")
}
return String(data: data!, encoding: String.Encoding.utf8) as String!
}
This part of your code is the completion handler for the dataTask() method. It's a block of code that you pass into the dataTask() method to be executed later on (when the server sends back some data or there's an error). It's not executed straight away.
This means that when your createRequest() method above is executing, it passes straight over that code, then onto the requestTask.resume() line, and then the method ends. At that point, because your method is defined as returning a String, you need to return a String. Returning it from the completion handler is no good because that hasn't been executed yet, that is going to be executed later on.
There's lots of different ways to handle asynchronous programming, but one way of tackling this is to change your createRequest() method so that it isn't defined to return a String, create a method that takes a String as a parameter which does whatever you wanted to do with the return value, and then call that method from your completion handler.
Instead of using return, try using completion handlers as you mentioned in your question.
func createRequest(qMes: message, location: String, method: String, completionHandler: #escaping (_ data:Data?, _ response: URLResponse?, _ error: NSError?) -> Void)
Then instead of return you should use something like completionHandler(data, response, error)
And this is how you make the request:
var request = URLRequest(url: Foundation.URL(string: URL)!)
request.httpMethod = method
//request.addValue(authString, forHTTPHeaderField: "Authorization") // if you need some
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard error == nil && data != nil else
{
print("error=\(error)")
completionHandler(data, response, error as NSError?)
return
}
completionHandler(data, response, error as NSError?)
})
task.resume()
Just in your function call
var webString = try String(contentsOf: URL(string: url)!)
And you have full response in string, that you can return
I have the following function to load data. The function worked just fine till a couple of days ago. The app crashes trying to load any url, see code below and screenshot. Coding ios/swift just a few days a year, it pretty hard to figure out what's wrong...
class func loadDataFromURL(_ url: URL, completion:#escaping (_ data: Data?, _ error: NSError?) -> Void) {
let session = URLSession.shared
// Use NSURLSession to get data from an NSURL
let loadDataTask = session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: NSError?) -> Void in
if let responseError = error {
completion(nil, responseError)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode != 200 {
let statusError = NSError(domain:"com.xyz", code:httpResponse.statusCode, userInfo:[NSLocalizedDescriptionKey : "HTTP status code has unexpected value."])
completion(nil, statusError)
} else {
completion(data, nil)
}
}
} as! (Data?, URLResponse?, Error?) -> Void)
loadDataTask.resume()
}
Use below code with latest Swift 3.0 syntax it works on Xcode 8.2:-
func loadDataFromURL(_ url: URL, completion:#escaping (_ data: Data?, _ error: NSError?) -> Void) {
let session = URLSession.shared
// Use NSURLSession to get data from an NSURL
let loadDataTask = session.dataTask(with: url) { (data, response, error) in
if let responseError = error {
completion(nil, responseError as NSError?)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode != 200 {
let statusError = NSError(domain:"com.xyz", code:httpResponse.statusCode, userInfo:[NSLocalizedDescriptionKey : "HTTP status code has unexpected value."])
completion(nil, statusError)
} else {
completion(data, nil)
}
}
}
loadDataTask.resume()
}
I am studying the swift and I update the xcode to current version (8.1). So the source that I saved was changed. When I ran the code. It cracked and I cannot fixed it. It showed "EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)" on the last line. This is a code that I have studied from web.
Thank you very much.
let urlString = "http://swapi.co/api/people/1/"
let session = URLSession.shared
let url = URL(string: urlString)!
session.dataTask(with: url, completionHandler: { (data: Data?, response:URLResponse?, error: NSError?) -> Void in
if let responseData = data {
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: JSONSerialization.ReadingOptions.allowFragments)
if let dict = json as? Dictionary<String, AnyObject> {
if let name = dict["name"] as? String, let height = dict["height"] as? String, let birth = dict["birth_year"] as? String, let hair = dict["hair_color"] as? String {
let person = SWPerson(name: name, height: height, birthYear: birth, hairColor: hair)
print(person.name)
print(person.height)
print(person.hairColor)
print(person.birthYear)
if let films = dict["films"] as? [String] {
for film in films {
print(film)
}
}
}
}
} catch {
print("Could not serialize")
}
}
} as! (Data?, URLResponse?, Error?) -> Void) .resume()
You can save much time if you simply read the documentation
Press ⇧⌘0 (zero, not O)
Type dataTask, select dataTask(with:completionHandler:) and press return.
You will get
func dataTask(with url: URL,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
Do you see the slightly difference in the error parameter?
So the correct syntax in your code is
session.dataTask(with: url, completionHandler: { (data: Data?, response:URLResponse?, error: Error?) -> Void in
...
}).resume()
There is no type cast at the end.
Further the type annotations in the completion block are not needed because the compiler infers the types. The error doesn't occur if you just write
session.dataTask(with: url, completionHandler: { (data, response, error) in
...
}).resume()
The shortest form is using the trailing closure syntax
session.dataTask(with: url) { (data, response, error) in
...
}.resume()
When I post data to a service, the request is normally fine, but if it takes too long and times out, in the completion handler, I get NSURLResponse as nil even though it should never return nil.
I'm using Swift 1.1, here's an example of how I'm doing this:
func postX(actionKey:String, postData:AnyObject, callBack:((data:NSData?, resp: NSURLResponse) -> Void)?) -> Void
{
var mreq = createRequest(actionKey, method: "POST", https: true, json: true)
if (self.dataTask != nil)
{
self.dataTask?.cancel()
}
var err: NSError?
mreq.HTTPBody = NSJSONSerialization.dataWithJSONObject(postData, options: nil, error: &err)
self.dataTask = self.getSession().dataTaskWithRequest(mreq, completionHandler: { (data:NSData!, resp: NSURLResponse!, error: NSError!) -> Void in
Dlog.log("response: \(resp)") //Prints out: response: nil
if (error != nil)
{
//do something
}
else
{
//do something else
}
})
if (self.dataTask != nil)
{
self.dataTask!.resume()
}
}
The NSURLResponse reference in the completionHandler block of dataTaskWithRequest is an optional (meaning that it can be nil). If the request times out, you'd expect it to be nil (because you presumably have not yet received any response).
I would suggest changing the NSURLResponse parameter of your callback closure to be an optional as well, just like dataTaskWithRequest. (I might return the optional NSError, too, so you can check for particular errors.) And you can detect timeout errors by looking for NSURLErrorTimedOut.
For example:
func postX(actionKey: String, postData: AnyObject, callBack: (NSData?, NSURLResponse?, NSError?) -> ()) {
let request = createRequest(actionKey, method: "POST", https: true, json: true)
dataTask?.cancel() // note, we don't need `if` clause, as the `?` does everything for us
request.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(postData, options: [])
let task = getSession().dataTaskWithRequest(request) { data, response, error in
callBack(data, response, error)
}
task.resume()
dataTask = task
}
Then you could use it like:
postX(actionKey, postData: postData) { data, response, error in
// handle response however you want
// did it time out?
if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorTimedOut {
print("timed out") // note, `response` is likely `nil` if it timed out
}
}
For the sake of completeness, the Swift 3 implementation might look like:
func postX(actionKey: String, postData: AnyObject, callBack: (Data?, URLResponse?, NSError?) -> ()) {
var request = createRequest(actionKey: actionKey, method: "POST", https: true, json: true)
dataTask?.cancel() // note, we don't need `if` clause, as the `?` does everything for us
request.httpBody = try! JSONSerialization.data(withJSONObject: postData, options: [])
let task = getSession().dataTask(with: request) { data, response, error in
callBack(data, response, error)
}
task.resume()
dataTask = task
}
func createRequest(actionKey: String, method: String, https: Bool, json: Bool) -> URLRequest {
var request = URLRequest(url: ...)
...
return request
}
And it would be called like:
postX(actionKey: actionKey, postData: postData) { data, response, error in
// handle response however you want
// did it time out?
if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorTimedOut {
print("timed out") // note, `response` is likely `nil` if it timed out
}
}