Swift: Escaping closure captures non-escaping parameter 'onCompletion' - ios

I have a problem with my swift. I am trying to send an API request and then retrieve data but I get the following error message:
"Swift: Escaping closure captures non-escaping parameter 'onCompletion'".
Does anyone know how I can solve this? thanks in advance
Code:
class RestApiManager: NSObject {
static let sharedInstance = RestApiManager()
let baseURL = "http://api.randomuser.me/"
func getRandomUser(onCompletion : (JSON) -> Void) {
makeHTTPGetRequest(path: baseURL, onCompletion: { json, err -> Void in
onCompletion(json)
})
}
func makeHTTPGetRequest(path: String, onCompletion: ServiceResponse) {
let request = NSMutableURLRequest(url : URL(string: path)! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
let json:JSON = JSON(data as Any)
onCompletion(json, error as NSError?)
})
task.resume()
}
}

You have to mark both completion handlers with #escaping. Usually the compiler offers a fix
class RestApiManager: NSObject {
static let sharedInstance = RestApiManager()
let baseURL = "http://api.randomuser.me/"
func getRandomUser(onCompletion : #escaping (JSON) -> Void) {
makeHTTPGetRequest(path: baseURL, onCompletion: { json, err -> Void in
onCompletion(json)
})
}
func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let request = NSMutableURLRequest(url : URL(string: path)! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
let json:JSON = JSON(data as Any)
onCompletion(json, error as NSError?)
})
task.resume()
}
}

Answers here were right on adding #escaping before the completion handler parameter declaration, albeit shortly explained.
Here's what I was missing for the full picture, taken from Swift's documentation:
Escaping Closures
A closure is said to escape a function when the
closure is passed as an argument to the function, but is called after
the function returns. When you declare a function that takes a closure
as one of its parameters, you can write #escaping before the
parameter’s type to indicate that the closure is allowed to escape.
So basically, if you want a completion handler to be called AFTER the method returns, it is defined as escaping in swift, and should be declared as such:
func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse)

This is happning due to your parameter onCompletion. By default it is #nonesacping you have to marke it #esacping so it can be worked in completionHandler closure.
func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse)

Use this:
class RestApiManager: NSObject {
static let sharedInstance = RestApiManager()
let baseURL = "http://api.randomuser.me/"
func getRandomUser(onCompletion : #escaping (JSON) -> Void) {
makeHTTPGetRequest(path: baseURL, onCompletion: { json, err -> Void in
onCompletion(json)
})
}
func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let request = NSMutableURLRequest(url : URL(string: path)! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
let json:JSON = JSON(data as Any)
onCompletion(json, error as NSError?)
})
task.resume()
}
}

Related

Mocking URLSession for API Testing is giving unrecognized selector sent to instance

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)
}
}
}

What could be best way to reduce redundant code

I wanted to reduce repetition in this code which I got from ExDeveloper. I have a server model where I've all my networking code. Now I've just shown 3 methods here. But eventually it'll grow & there would be 70+ methods. I don't want to repeat task code which is common in all methods.
What is the best way to do it without using a 3rd party like Alamofire, etc.?
func login(userName: String, password: String, onSuccess: #escaping(JSON) -> Void, onFailure: #escaping(Error) -> Void){
let url : String = ""
let request: NSMutableURLRequest = NSMutableURLRequest(url: NSURL(string: url)! as URL)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if(error != nil){
onFailure(error!)
} else{
print(response as Any)
}
})
task.resume()
}
//====================
func signUp(userName: String, password: String, email: String, onSuccess: #escaping(JSON) -> Void, onFailure: #escaping(Error) -> Void){
let url : String = ""
let request: NSMutableURLRequest = NSMutableURLRequest(url: NSURL(string: url)! as URL)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if(error != nil){
onFailure(error!)
} else{
print(response as Any)
}
})
task.resume()
}
//====================
func forgot(email: String, onSuccess: #escaping(JSON) -> Void, onFailure: #escaping(Error) -> Void){
let url : String = ""
let request: NSMutableURLRequest = NSMutableURLRequest(url: NSURL(string: url)! as URL)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if(error != nil){
onFailure(error!)
} else{
print(response as Any)
}
})
task.resume()
}
You could split up the requests into more modular components like network layers.
Check out this guide: https://medium.com/#danielemargutti/network-layers-in-swift-7fc5628ff789
As you've written the above code, each function is exactly identical. So put that in a separate function that they all call. If there are parts that aren't identical (that you didn't provide), then pass those to the shared function. If there is code that changes, pass that as a function parameter.
In short: extract the parts that don't change, and pass the parts that do as parameters.

How do I set up a success/failure block so that I can pass a string into it using Swift?

I have an API method that returns teams for particular leagues. All I need to do is pass in some parameters.
For example, the API URL looks like this: http://api.website.com/api/teams?season=last&league=0Xc334xUK4
Here is my code:
#objc class APITeam: NSObject {
var leagueObjectID: NSString!
let baseUrl = "http://api.website.com/api"
static let sharedInstance = APITeam()
static let getTeamsEndpoint = "/teams"
static let params = "?season=last&league="
private override init() {}
func getTeams (_ onSuccess: #escaping(Any) -> Void, onFailure: #escaping(Error) -> Void) {
var request = URLRequest(url: URL(string: baseUrl + APITeam.getTeamsEndpoint + APITeam.params)!)
let session = URLSession.shared
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data : Data?, response : URLResponse?, error : Error?) in
if(error != nil){
onFailure(error!)
} else{
do {
let result = try JSONSerialization.jsonObject(with: data!)
onSuccess(result)
} catch {
print(error)
}
}
}
task.resume()
}
}
In order for this API method to return data, I need to pass in the objectID of the league I want teams for when making a request.
I've tried to add parameter to:
func getTeams (_ onSuccess: #escaping(Any) -> Void, onFailure: #escaping(Error) -> Void, leagueObjectID: String) {
var request = URLRequest(url: URL(string: baseUrl + APITeam.getTeamsEndpoint + APITeam.params + leagueObjectID)!)
This doesn't work as expected. When I use the method in another class, "leagueObjectID" acts as an additional part of the success, error block if that makes sense. I need to be able to pass the leagueObjectID into the method so it's used at the end of the URL the request is made to.
This is how I call APITeam in an objective-c class:
[[APITeam sharedInstance] getTeams:^(id result) {
} onFailure:^(NSError * error) {
}];
As you can see, the extra parameter I added to the getTeams function doesn't show up, and when I try to manually add it, I get an error.
How would you handle this?
An example would be much appreciated.
Thanks for your time
Change it to
func getTeams (leagueObjectID: String, onSuccess: #escaping(Any) -> Void, onFailure: #escaping(Error) -> Void)
static might not work as you're used to in other languages. static var behaves like class var.
If I understand you correctly, this would satisfy your needs
#objc class APITeam: NSObject {
private static let baseUrl = "http://api.website.com/api"
private static let getTeamsEndpoint = "/teams"
let params = "?season=last&league="
private override init() {}
func getTeam(id: String, _ onSuccess: #escaping(Any) -> Void, onFailure: #escaping(Error) -> Void) {
var request = URLRequest(url: URL(string: APITeam.baseUrl + APITeam.getTeamsEndpoint + APITeam.params + id)!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error {
onFailure(error)
} else if let responseData = data {
do {
let result = try JSONSerialization.jsonObject(with: responseData)
onSuccess(result)
} catch {
print(error)
}
}
}.resume()
}
}
Usage:
APITeam().getTeam(id: "0Xc334xUK4", ..your completion handlers here..)
This worked for me:
#objc class APITeam: NSObject {
var leagueObjectID: NSString!
let baseUrl = "http://api.website.com/api"
static let sharedInstance = APITeam()
static let getTeamsEndpoint = "/teams"
static let params = "?season=last&league="
private override init() {}
func getTeams (leagueObjectID: String, _ onSuccess: #escaping(Any) -> Void, onFailure: #escaping(Error) -> Void) {
var request = URLRequest(url: URL(string: baseUrl + APITeam.getTeamsEndpoint + APITeam.params + leagueObjectID)!)
let session = URLSession.shared
request.httpMethod = "GET"
let task = session.dataTask(with: request) { (data : Data?, response : URLResponse?, error : Error?) in
if(error != nil){
onFailure(error!)
} else{
do {
let result = try JSONSerialization.jsonObject(with: data!)
onSuccess(result)
} catch {
print(error)
}
}
}
task.resume()
}
}
Then in my Objective-C class:
[[APITeam sharedInstance] getTeamsWithLeagueObjectID:#"93PwHe5e4S" :^(id league) {
NSLog(#"THE LEAGUE: %#", league);
} onFailure:^(NSError * error) {
}];
It's also important to build the app before trying to access the original class. That was a vital step for me. Sometimes I don't need to do this, but this time around, I couldn't access the new changed APITeam getTeams function until I built the app.

Networking Call with Generic Decoding type

I am attempting to create a generic networking function I am looking to make an API handler which will download JSON from the network and convert theme to Swift structs that conform to the Decodable protocol. Currently I am using explicit types:
struct MyObject : Decodable {
let id : Int
let name : String
}
static fileprivate func makeNetworkRequest(url: URL, completionHandler: #escaping(_ error: Error?, _ myObject: MyObject?) -> ()) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
do {
let myNewObject = try JSONDecoder().decode(MyObject.self, from: data!)
completionHandler(nil, myNewObject)
}
catch let error {
completionHandler(error, nil)
return
}
}.resume()
}
I am hoping to create a generic function where I can specify any data type which confirms to Decodable and have the data object returned in the completion handler. Something along the lines of:
static fileprivate func makeNetworkRequest<T>(url: URL, type: <<<Decodable Type>>>, completionHandler: #escaping(_ error: Error?, _ myObject: <<<Deocable Object>>>?) -> ()) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
do {
let myNewObject = try JSONDecoder().decode(<<<Decodable Type>>>.self, from: data!)
completionHandler(nil, myNewObject)
}
catch let error {
completionHandler(error, nil)
return
}
}.resume()
}
However, I can't seem to get the function parameters correct. I do not have a lot of experience working with generics. Any help would be appreciated
You can mimic the declaration of the decode method of JSONDecoder:
open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
Applying the patter above to your code, the definition should be something like this:
static fileprivate func makeNetworkRequest<T>(url: URL, type: T.Type, completionHandler: #escaping (_ error: Error?, _ myObject: T?) -> ())
where T: Decodable
{
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
do {
let myNewObject = try JSONDecoder().decode(T.self, from: data!)
completionHandler(nil, myNewObject)
} catch let error {
completionHandler(error, nil)
return
}
}.resume()
}
Or you can write it in this way:
static fileprivate func makeNetworkRequest<T: Decodable>(url: URL, type: T.Type, completionHandler: #escaping (_ error: Error?, _ myObject: T?) -> ()) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
do {
let myNewObject = try JSONDecoder().decode(T.self, from: data!)
completionHandler(nil, myNewObject)
} catch let error {
completionHandler(error, nil)
return
}
}.resume()
}

Thread 1: break point error to send post method

I would like to send post method (to login user) but when I click on login button in run time I got this message :
my class:
typealias ServiceResponse = (JSON, NSError?) -> Void
class RestApiManager: NSObject {
static let sharedInstance = RestApiManager()
let baseURL = "***********"
func login(body: [String: AnyObject],onCompletion: #escaping (JSON) -> Void) {
let route = baseURL+"o/token/"
makeHTTPPostRequest(path: route,body: body, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
func getCategories(onCompletion: #escaping (JSON) -> Void) {
let route = baseURL+"o/token/"
makeHTTPGetRequest(path: route, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
func getRandomUser(onCompletion: #escaping (JSON) -> Void) {
let route = baseURL
makeHTTPGetRequest(path: route, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
// MARK: Perform a GET Request
private func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let request = NSMutableURLRequest(url: NSURL(string: path)! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if let jsonData = data {
let json:JSON = JSON(data: jsonData)
onCompletion(json, error as NSError?)
} else {
onCompletion(nil, error as NSError?)
}
})
task.resume()
}
// MARK: Perform a POST Request
private func makeHTTPPostRequest(path: String, body: [String: AnyObject], onCompletion: #escaping ServiceResponse) {
let request = NSMutableURLRequest(url: NSURL(string: path)! as URL)
// Set the method to POST
request.httpMethod = "POST"
do {
// Set the POST body for the request
let jsonBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
request.httpBody = jsonBody
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if let jsonData = data {
let json:JSON = JSON(data: jsonData)
onCompletion(json, nil)
} else {
onCompletion(nil, error as NSError?)
}
})
task.resume()
} catch {
// Create your personal error
onCompletion(nil, nil)
}
}
}
in my login controller :
//after click on login button
let parameters = ["grant_type": "password",
"username": "test29#gmail.com",
"password": "1",
"client_id": "toS899lbMGolv8j24piz0JI38VUCi6Mvzru27iBA",
"client_secret":"lG14Tk7m2mGYLMvBndW2yFZ1NGRLNrriIPH6gw30gAnMAcFMa5xJE3wP8H 4SDHAK0ND5nKIoSLZskFQQ1knEYiaPC3i5LNutPJlusiMNiuvhUHWnbvTCjmNkuCBkGgqO"]
RestApiManager.sharedInstance.login(body: parameters as [String : AnyObject]) { (json: JSON) in
print(json)
}
Try to put it back to the main thread:
func login(body: [String: AnyObject],onCompletion:#escaping(JSON) -> Void) {
let route = baseURL+"o/token/"
makeHTTPPostRequest(path: route,body: body, onCompletion: { json, err in
DispatchQueue.main.async {
onCompletion(json as JSON)
}
})
}

Resources