How to correct the order of execution of code in Swift 5? - swift5

The code within the function is executed in a different order than it is expected. I wanted to change the state of the login Boolean variable inside the if statement, but the function returns the initial value before if statement is completed.
Code sample:
class ClassName {
func loginRequest (name: String, pwd: String) -> Bool {
var login:Bool
//Initial value for login
login = false
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if (httpResponse.statusCode) == 200 {
//Change the value of login if login is successful
login = true
if let data = data, let dataString = String(data: data, encoding: .utf8) {
do {
...
} catch {print(error.localizedDescription)}
}
}
}
}
task.resume()
//Problem return false in any case because return is completed before if statement
return login
}
}

Completion Handlers is your friend
The moment your code runs task.resume(), it will run your uploadTask and only when that function is finished running it will run the code where you change your login variable.
With That said: That piece of code is running asynchronously. That means your return login line of code won't wait for your network request to come back before it runs.
Your code is actually running in the order it should. But i myself wrote my first network call like that and had the same problem. Completion Handles is how i fixed it
Here is a very nice tutorial on Completion Handlers or you might know it as Callbacks :
Link To Completion Handlers Tutorial
If i can give you a little hint - You will have to change your function so it looks something like this: func loginRequest (name: String, pwd: String, completionHandler: #escaping (Bool) -> Void)
And replace this login = true with completionHandler(true)
Wherever it is you call your function it will look something like this:
loginRequest(name: String, pwd: String) {didLogIn in
print("Logged In : \(didLogIn)")
}
One last thing... You're actually already using Completion Handlers in your code.
let task = session.uploadTask(with: request, from: jsonData) { data, response, error in
... ... But hopefully now you understand a little bit better, and will use a completion handler approach when making network calls.
GOOD LUCK !

Related

API network request error on iOS device, not simulator

ok so i've been trying to get this problem figured out for 2 days now, hoping someone can help.
quick background, i’m making an api request for data. using a function that calls a service function i made. now everything works good on the first load, collectionview loads fine. at some point i run another call for more data. accept now i get a URL Error.
This doesn’t work on my iphone, but does work perfectly on simulator, so not sure what it could be.
heres the service function that makes the api request:
func fetchYoutubeData(interest: String, maxResult: Int, pageToken: String, completion: #escaping(Result<Youtube, WHError>) -> Void) {
let urlString = baseYoutubeURL+interest+youtubeAPIKey+"&maxResults=\(maxResult)&pageToken=\(pageToken)"
guard let url = URL(string: urlString) else {
completion(.failure(.URLError))
return
}
let session = URLSession.shared
let task = session.dataTask(with: url) { (data, _, error) in
if let _ = error {
completion(.failure(.DataError))
return
}
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(Youtube.self, from: data)
print(result)
completion(.success(result))
} catch {
completion(.failure(.JSONError))
}
}
task.resume()
}
heres my controller function that calls service and handles the data on completion:
func fetchNewData(maxResult: Int, pageToken: String) {
guard let interest = self.interest.text else { return }
print(pageToken)
NetworkServices.shared.fetchYoutubeData(interest: interest, maxResult: maxResult, pageToken: pageToken) { [unowned self] (result) in
switch result {
case .success(let youtubeGroup):
let items = youtubeGroup.items
self.youtubeData.items.append(contentsOf: items)
DispatchQueue.main.async {
self.horizontalCollectionView.reloadData()
}
case .failure(let error):
print("DOES GET ERROR")
print(error)
}
}
}
again it works perfectly on simulator, but not on my device, i can get first call to work, but after that, once i use pageToken to get more data, i get a URL Error.
any help would be seriously appreciated
so of course like alot of things , it's something small I missed. I'll keep the question here, incase someone finds themself in a similar situation.
the problem was in the first request I reformat the "interest" to remove spaces from the string and replace them with +. you need to do this for the Youtube API query string that you provide.
i did this in the first request, but for my query for additional data, I forgot to reformat the request in the separate call.
easy fix thankfully
I added "App Transport Security Settings" to the Info.plist file then selected "Allow Arbitrary Loads" and set that value to "YES" and that worked. My assumption is that there was an issue with the security cert on the client development environment that was preventing the app from making calls over HTTPS.
And never fear, I only allow arbitrary loads in the dev and qa environments.

Alamofire request or Firebase query is not working in a non UIViewController Class

Imperial trying to perform a request to a website using alamofire and my problem is the following:
When I use the corresponding code in an ViewController cocoaTOuch class viewDidLoad() function everything works fine.(here is the code)
super.viewDidLoad()
let loginActionUrl = url
do{
let parameters = [
"p_user":user,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseJSON
{response in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
......
If I repeat the same code inside a private function on a swift (non cocoa touch) class, then,I have no response, While debugging it tries to perform the request task twice and then jumps out of the {response in code block.
The code is the following:
private func checkInWithAeA(withLogIn: String, password: String) -> (Bool){
var companyUSerRecognized: Bool = false
var startIndex: String.Index!
let loginActionUrl = url
do{
let parameters = [
"p_user" : withLogIn,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseJSON
{response in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
companyUSerRecognized = true
......
I don't now what is happening but is the second time I have the same problem. What I'm dong is trying to avoid to set up to much code in the viewController using other support classes, following best practices, but I already tried to do this with firebase, and I have the same problem, the query to the database only worked in UIViewcontroller classes (in certain) and now is the same, I am not able to obtain any result when I execute the code in the clean swift file.
Is there any kind of limitation on this. Why I cannot do anything like an alamofire request or a firebase query to the realtime database out of a UIViewController class?
Here I add some information:
var myConnectionController: ConnectionController = ConnectionController()
let (companyUSerRecognized, error) = myConnectionController.chekUserIDandPassWord(forTheCompany: self.companyName, logInName: self.companyLogIn, password: self.companyPassword)
This call to the ConnectionController class (that is a swift plain class) asks for a connexion to a web page. If the response is good, then a true is obtained and the process is continued.
The function called has a switch statement:
public func chekUserIDandPassWord(forTheCompany: String, logInName: String, password: String) -> (Bool, String){
var companyUSerRecognized: Bool!
var error: String!
switch forTheCompany {
case "(AEA)":
companyUSerRecognized = checkInWithAeA(withLogIn: logInName, password: password)
break
.....
This is what calls Check in With AeA. (The function I just mentioned before). What I want to is get the cookies of the connection in return check them and if they are good, true is returned.
I already have done this in the viewDidLoad() function of a ViewController, In fact I can parse the response with SwiftSoup, etc. But If I do it this way I am not able to do it.
Thanks again
I finally made up the solution by reviewing some bibliography. I did not notice that, any alamofire call opens a pipeline. That is, we are obviously talking about asynchronous operations. There are two ways to handle with this kind of operations in swift. One is the use of future objects. This option allows the continuation of the execution by substituting the results from the async call when they are ready. Depending on the situation this is not so good. The other is to make the execution wait for the response of the async call. This is done with a closure. I took this lastoption.
The closer is to be performed by using a completion handler function that is called at the end of the async call block to return any value you need from the async call. In this case. This is what I called completion
private func checkInWithAeA(completion: #escaping (Bool)-> Void){
let loginActionUrl = url1
let postLoginUrl = url2
let parameters = [
"p_user" : logInName,
"p_password": password
]
AF.request(loginActionUrl, method: .post, parameters: parameters).responseData
{(response) in
if let header = response.response?.allHeaderFields as? [String: String],
let responseUrl = response.request?.url{
let sessionCookies = HTTPCookie.cookies(withResponseHeaderFields: header, for: responseUrl)
let cookieToSend = sessionCookies[0]
//Debug
print(cookieToSend)
AF.session.configuration.httpCookieStorage?.setCookie(cookieToSend)
completion(true)
}else{
completion(false)
}
}
}
That's it. Hope it helps
BTW. I think that this is the same problem with the firebase queries.
Thanks!

Swift program never enters CompletionHandler for a dataTask

I am in the process of implementing a REST API with Swift. Of course, part of this API is using HTTP requests to retrieve and send data.
Full disclosure, I am inexperienced with Swift and am using this as a learning project to get my feet wet, so to speak. But it's turned into much more of a difficult project than I anticipated.
In implementing the first get method, I have (finally) gotten rid of all the compilation errors. However, when I call the function which utilizes the URLRequest, URLSession, dataTask, etc, it is never entered.
Upon debugging the program, I can watch the program execution reach the CompletionHandler, and skip over it right to "task.resume()."
A similar construction works in a Swift Playground, but does not work in the actual project proper.
So far I have tried a few things, namely making the function access a class instance variable, in hopes that that would force it to execute. But it does not.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
import Foundation
/**
A class to wrap all GET and POST requests, to avoid the necessity of repeatedly writing request code in each API method.
*/
class BasicRequest {
private var url: URL
private var header: [String: String]
private var responseType: String
private var jsonResponse: Any?
init(url: URL, header: [String: String], responseType: String) {
self.url = url
self.header = header
self.responseType = responseType
} //END INIT
public func requestJSON() -> Any {
// Create the URLRequest object, and fill the header with the header fields as provided.
var urlRequest = URLRequest(url: self.url)
for (value, key) in self.header {
urlRequest.addValue(value, forHTTPHeaderField: key)
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
print("Entered the completion handler")
if error != nil {
return
}
guard let httpResponse = response as? HTTPURLResponse, 200 == httpResponse.statusCode else {
print("HTTP Request unsuccessful")
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Not a JSON response")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
self.jsonResponse = json
} catch {
print("Could not transform to JSON")
return
}
}
task.resume()
return "Function has returned"
} //END REQUESTJSON
}
The expected result would be returning a JSON object, however that does not seem to be the case.
With respect to error messages, I get none. The only log I get in the debugger is the boilerplate "process exited with code 0."
To be truthful, I'm at a loss with what is causing this not to work.
It appears you're writing this in a command-line app. In that case the program is terminating before the URLRequest completes.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
Exactly.
The typical tool in Swift is DispatchGroup, which is just a higher-level kind of semaphore. Call dispatchGroup.enter() before starting the request, and all dispatchGroup.leave() at the end of the completion handler. In your calling code, include dispatchGroup.wait() to wait for it. (If that's not clear, I can add code for it, but there are also a lot of SO answers you can find that will demonstrate it.)

REST API calls not working in swift

I'm following this tutorial for making a simple REST API call in swift: https://grokswift.com/simple-rest-with-swift/
The problem I'm running into is that the data task completion handler next gets executed. When I'm debugging it step by step, it just jumps over the completion handler block. Nothing is printed in the console, either.
I've searched for other methods of making REST API calls, but they are all very similar to this one and not working, either.
Here is my code:
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
guard error == nil else {
print("Error calling GET")
return
}
guard let responseData = data else {
print("Error receiving data")
return
}
do {
print ("Parsing response...")
}
}
task.resume()
Your code looks right to me. I tested it in a Playground and I'm getting the Parsing response... message printed to the console which makes me think the issue is elsewhere in your code or environment. I'd be happy to take a look at the whole project if you can post a Github link or something similar.
Here are the steps I would take to debug an issue like this:
1) Confirm my execution environment has an active internet connection. The Safari app can be used to confirm on iOS devices or the Simulator. Playgrounds can be tested by pasting the following lines.
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
print (try? String(contentsOf: url))
Look for a line in the console output similar to:
Optional("{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}")
2) Confirm the url is valid and returns data by pasting it into a web browser url bar and hitting enter. You will either see JSON printed in the browser or not.
3) Confirm my code is actually getting called when the application runs. You can do this with either breakpoints or print() statements. As OOPer2 pointed out asynchronous callback closures like that used in session.dataTask() execute in a different time than the rest of your code which is why "it just jumps over the completion handler block" while stepping through with the debugger. You'll need to put another breakpoint or print() statement inside the completion handler closure. I'd put the breakpoint on the guard error == nil else { line.
4) Make sure the application is still executing when the network request finishes and the completion handler closure executes. If your code is in a ViewController running in an iOS application it's probably fine, but if it's running in a Playground it may not be. Playgrounds by default stop execution once the last line of code has been evaluated which means the completion closure will never execute. You can tell a Playground to continue executing indefinitely by importing the PlaygroundSupport framework and setting needsIndefiniteExecution = true on the current Playground page. Paste the entire code block below into a Playground to see it in action:
import Foundation
import PlaygroundSupport
// Keep executing the program after the last line has evaluated so the
// closure can execute when the asynchronous network request finishes.
PlaygroundPage.current.needsIndefiniteExecution = true
// Generic Result enum useful for returning values OR an error from
// asynchronous functions.
enum Result<T> {
case failure(Error)
case success(T)
}
// Custom Errors to be returned when something goes wrong.
enum NetworkError: Error {
case couldNotCreateURL(for: String)
case didNotReceiveData
}
// Perform network request asynchronous returning the result via a
// completion closure called on the main thread.
//
// In really life the result type will not be a String, it will
// probably be an array of custom structs or similar.
func performNetworkRequest(completion: #escaping (Result<String>)->Void ) {
let endpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: endpoint) else {
let error = NetworkError.couldNotCreateURL(for: endpoint)
completion(Result.failure(error))
return
}
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { (data, response, error) -> Void in
// This closure is still executing on a background thread so
// don't touch anything related to the UI.
//
// Remember to dispatch back to the main thread when calling
// the completion closure.
guard error == nil else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(error!))
}
return
}
guard let responseData = data else {
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.failure(NetworkError.didNotReceiveData))
}
return
}
// Parse response here...
// Call the completion handler on the main thread.
DispatchQueue.main.async {
completion(Result.success("Sucessfully parsed results"))
}
}
task.resume()
}
performNetworkRequest(completion: { result in
// The generic Result type makes handling the success and error
// cases really nice by just using a switch statement.
switch result {
case .failure(let error):
print(error)
case .success(let parsedResponse):
print(parsedResponse)
}
})
Why you dont use this Library Alamofire is an HTTP networking library written in Swift.
Add this line to your Podfile
pod 'Alamofire', '~> 4.4'
Then, run the following command:
pod install
Then in your ViewController file:
import Alamofire
Alamofire.request("https://jsonplaceholder.typicode.com/todos/1").responseJSON { response in
print("Request: \(String(describing: response.request))") // original url request
print("Response: \(String(describing: response.response))") // http url response
print("Result: \(response.result)") // response serialization result
if let json = response.result.value {
print("JSON: \(json)") // serialized json response
}
If let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)") // original server data as UTF8 string
}
}
And in here are an example of how to parse the result.
https://github.com/CristianCardosoA/JSONParser
For more info about Alamofire:
https://github.com/Alamofire/Alamofire
I hope this help.

Unit test with Swift: body of closure not executed

There are functions that send a request to server, get response and print result. They always work in the iOS app itself but only sometimes (looks like randomly) in unit-tests of this app.
Main issue: Xcode doesn't enter the body of a closure in unit-tests, just skips it.
Any ideas how can it be fixed? Image of the problem in Xcode.
The most likely reason because the completion closure of your requests are not being exectued is that they are performing an asynchronous operation, while the tests run synchronously. This means that the test finishes running while your network request is still processing.
Try using XCTestExpectation:
func testIt() {
let expectation = expectationWithDescription("foobar")
// request setup code here...
Alamofire.request(.POST, "...")
.responseJSON { response in
//
// Insert the test assertions here, for example:
//
if let JSON = response.result.value as? [String: AnyObject] {
XCTAssertEqual(JSON["id"], "1")
} else {
XCTFail("Unexpected response")
}
//
// Remember to call this at the end of the closure
//
expectation.fulfill()
}
//
// This will make XCTest wait for up to 10 seconds,
// giving your request expectation time to fulfill
//
waitForExpectationsWithTimeout(10) { error
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
}
}
}