Im not using properly DispatchQueue function since I have to click 2-3 times in order to change screen, since the data dont load on time. I tried couple of positions in the code but Im always getting the same result.
What is a proper use?
Here is my code:
func startLogin() {
userNameData = userName.text!.trimmingCharacters(in: .whitespacesAndNewlines)
passwordData = password.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if userNameData == "" || passwordData == "" { return } else {
let parameters = "{\n\t\"user\": \"\(userNameData)\",\n\t\"password\": \"\(passwordData)\"\n}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://someurl.io:18999/salesAPI/login")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = postData
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
print("User no encontrado!")
return
}
print(String(data: data, encoding: .utf8)!)
// semaphore.signal()
let decoder = JSONDecoder()
do {
let jsonPetitions = try decoder.decode(Token.self, from: data)
token = jsonPetitions.access_token
let defaults = UserDefaults.standard
defaults.set(true, forKey: "didLogin")
defaults.set(userNameData, forKey: "userNameData")
defaults.set(passwordData, forKey: "passwordData")
defaults.set(token, forKey: "enterKey")
}
catch {
print("No Json output!!")
return
}
}
func changeScreen() {
// performSegue(withIdentifier: "switchScreens", sender: nil)
let homeViewController = self.storyboard?.instantiateViewController(identifier: Constance.Storyboard.homeViewController) as? HomeViewController
self.view.window?.rootViewController = homeViewController
self.view.window?.makeKeyAndVisible()
}
dispatchGroup.enter()
task.resume()
self.dispatchGroup.leave()
dispatchGroup.notify(queue: DispatchQueue.main) {
if token == "" {
self.errorLabel.alpha = 0.5
self.errorLabel.text = "Algun dato esta mal"
self.errorLabel.textColor = UIColor.red
print ("No Token! User or the pass is wrong")
} else {
changeScreen()
}
}
} // end User Login
}
The code is just part of it but all the important part are there
The dispatch group isn’t used correctly. But rather than trying to fix that, we should just remove it, as it’s unnecessary. Just move the code inside the notify block into the dataTask closure, after you parse the data.
For example:
func startLogin() {
userNameData = userName.text!.trimmingCharacters(in: .whitespacesAndNewlines)
passwordData = password.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if userNameData == "" || passwordData == "" { return }
let parameters = ["user": userNameData, "password": passwordData]
var request = URLRequest(url: URL(string: "https://someurl.io:18999/salesAPI/login")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(parameters)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
print("User no encontrado!")
return
}
let decoder = JSONDecoder()
do {
let jsonPetitions = try decoder.decode(Token.self, from: data)
token = jsonPetitions.access_token
let defaults = UserDefaults.standard
defaults.set(true, forKey: "didLogin")
defaults.set(userNameData, forKey: "userNameData")
defaults.set(passwordData, forKey: "passwordData")
defaults.set(token, forKey: "enterKey")
DispatchQueue.main.async {
if token == "" {
self.errorLabel.alpha = 0.5
self.errorLabel.text = "Algun dato esta mal"
self.errorLabel.textColor = UIColor.red
print ("No Token! User or the pass is wrong")
} else {
self.changeScreen()
}
}
} catch {
print("No Json output!!")
return
}
}
task.resume()
}
func changeScreen() {
// performSegue(withIdentifier: "switchScreens", sender: nil)
let homeViewController = self.storyboard?.instantiateViewController(identifier: Constance.Storyboard.homeViewController) as? HomeViewController
self.view.window?.rootViewController = homeViewController
self.view.window?.makeKeyAndVisible()
}
I have two services that are working perfectly independently one is a synchronous call to get shopping-lists and another is an asynchronous call to add shopping-lists. The problem comes when i try to get a shopping-lists just after the add-Shopping-lists call has successfully completed.
The function to get shopping-lists never returns it just hangs after i call it in the closure of the add-Shopping-lists function. What is the best way to make these two calls without promises.
Create ShoppingList
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: String) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
guard let requestUrl = URLComponents(string: urlString!)?.url else {
fatalError("Nil url")
}
var request = URLRequest(url:requestUrl)
request.httpMethod = method
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
func createShoppingList(with shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
let serviceURL = environment + Endpoint.createList.rawValue
let request = createURLRequest(with: serviceURL, data: shoppingList, httpMethod: HttpBody.post.rawValue)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
guard let _ = data,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode,
error == nil else {
completion(false, error)
return
}
completion(true, nil)
})
task.resume()
}
Get shoppingLists
func fetchShoppingLists(with customerId: String) throws -> [ShoppingList]? {
var serviceResponse: [ShoppingList]?
var serviceError: Error?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: HttpBody.get.rawValue)
let semaphore = DispatchSemaphore(value: 0)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
defer { semaphore.signal() }
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
(200 ..< 300) ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
serviceError = error
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
serviceResponse = shoppingList
} catch let error {
serviceError = error
}
})
task.resume()
semaphore.wait()
if let error = serviceError {
throw error
}
return serviceResponse
}
Usage of function
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList, completion: { (success, error) in
if success {
self.shoppingListCache.clearCache()
let serviceResponse = try? self.fetchShoppingLists(with: customerId)
if let _ = serviceResponse {
completion(true, nil)
} else {
let fetchListError = NSError().error(description: "Unable to fetch shoppingLists")
completion(false, fetchListError)
}
} else {
completion(false, error)
}
})
}
I would like to call the fetchShoppingLists which is a synchronous call and get new data then call the completion block with success.
This question is predicated on a flawed assumption, that you need this synchronous request.
You suggested that you needed this for testing. This is not true: One uses “expectations” to test asynchronous processes; we don’t suboptimize code for testing purposes.
You also suggested that you want to “stop all processes” until the request is done. Again, this is not true and offers horrible UX and subjects your app to possibly be killed by watchdog process if you do this at the wrong time while on slow network. If, in fact, the UI needs to be blocked while the request is in progress, we usually just throw up a UIActivityIndicatorView (a.k.a. a “spinner”), perhaps on top of a dimming/blurring view over the whole UI to prevent users from interacting with the visible controls, if any.
But, bottom line, I know that synchronous requests feel so intuitive and logical, but it’s invariably the wrong approach.
Anyway, I’d make fetchShoppingLists asynchronous:
func fetchShoppingLists(with customerId: String, completion: #escaping (Result<[ShoppingList], Error>) -> Void) {
var serviceResponse: [ShoppingList]?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: .get)
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
200 ..< 300 ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
completion(.failure(error ?? ShoppingError.unknownError))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
completion(.success(shoppingList))
} catch let jsonError {
completion(.failure(jsonError))
}
}
task.resume()
}
And then you just adopt this asynchronous pattern. Note, while I’d use the Result pattern for my completion handler, I left yours as it was to minimize integration issues:
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList) { success, error in
if success {
self.shoppingListCache.clearCache()
self.fetchShoppingLists(with: customerId) { result in
switch result {
case .failure(let error):
completion(false, error)
case .success:
completion(true, nil)
}
}
} else {
completion(false, error)
}
}
}
Now, for example, you suggested you wanted to make fetchShoppingLists synchronous to facilitate testing. You can easily test asynchronous methods with “expectations”:
class MyAppTests: XCTestCase {
func testFetch() {
let exp = expectation(description: "Fetching ShoppingLists")
let customerId = ...
fetchShoppingLists(with: customerId) { result in
if case .failure(_) = result {
XCTFail("Fetch failed")
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
}
FWIW, it’s debatable that you should be unit testing the server request/response at all. Often instead mock the network service, or use URLProtocol to mock it behind the scenes.
For more information about asynchronous tests, see Asynchronous Tests and Expectations.
FYI, the above uses a refactored createURLRequest, that uses the enumeration for that last parameter, not a String. The whole idea of enumerations is to make it impossible to pass invalid parameters, so let’s do the rawValue conversion here, rather than in the calling point:
enum HttpMethod: String {
case post = "POST"
case get = "GET"
}
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: HttpMethod) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
guard
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let requestUrl = URLComponents(string: urlString)?.url
else {
fatalError("Nil url")
}
var request = URLRequest(url: requestUrl)
request.httpMethod = method.rawValue
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
I am sure it could be alot better, but this is my 5 minute version.
import Foundation
import UIKit
struct Todo: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
enum TodoError: String, Error {
case networkError
case invalidUrl
case noData
case other
case serializationError
}
class TodoRequest {
let todoUrl = URL(string: "https://jsonplaceholder.typicode.com/todos")
var todos: [Todo] = []
var responseError: TodoError?
func loadTodos() {
var responseData: Data?
guard let url = todoUrl else { return }
let group = DispatchGroup()
let task = URLSession.shared.dataTask(with: url) { [weak self](data, response, error) in
responseData = data
self?.responseError = error != nil ? .noData : nil
group.leave()
}
group.enter()
task.resume()
group.wait()
guard responseError == nil else { return }
guard let data = responseData else { return }
do {
todos = try JSONDecoder().decode([Todo].self, from: data)
} catch {
responseError = .serializationError
}
}
func retrieveTodo(with id: Int, completion: #escaping (_ todo: Todo? , _ error: TodoError?) -> Void) {
guard var url = todoUrl else { return }
url.appendPathComponent("\(id)")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let todoData = data else { return completion(nil, .noData) }
do {
let todo = try JSONDecoder().decode(Todo.self, from: todoData)
completion(todo, nil)
} catch {
completion(nil, .serializationError)
}
}
task.resume()
}
}
class TodoViewController: UIViewController {
let request = TodoRequest()
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .background).async { [weak self] in
self?.request.loadTodos()
self?.request.retrieveTodo(with: 1, completion: { [weak self](todoData, error) in
guard let strongSelf = self else { return }
if let todoError = error {
return debugPrint(todoError.localizedDescription)
}
guard let todo = todoData else {
return debugPrint("No todo")
}
debugPrint(strongSelf.request.todos)
debugPrint(todo)
})
}
}
}
I need add a new function that returns songs from Apple Music API. The code below returns nil however, it works for Album ID. I modified the code from APIClient.swift. See source for details.
It returns song(id:completion:) JSON Decode Failed.
Source: https://github.com/shingohry/AppleMusicSearch
SongModel.swift
func song(id: String, completion: #escaping (Resource?) -> Swift.Void) {
let completionOnMain: (Resource?) -> Void = { resource in
DispatchQueue.main.async {
completion(resource)
}
}
guard let url = URL(string: "https://api.music.apple.com/v1/catalog/\(APIClient.countryCode)/songs/\(id)") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(APIClient.developerToken)",
forHTTPHeaderField: "Authorization")
data(with: request) { data, error -> Void in
guard error == nil else {
print(#function, "URL Session Task Failed", error!)
completionOnMain(nil)
return
}
guard let jsonData = try? JSONSerialization.jsonObject(with: data!),
let dictionary = jsonData as? [String: Any],
let dataArray = dictionary["data"] as? [[String: Any]],
let songDictionary = dataArray.first,
let songData = try? JSONSerialization.data(withJSONObject: albumDictionary),
let album = try? JSONDecoder().decode(Resource.self, from: songData) else {
print(#function, "JSON Decode Failed");
completionOnMain(nil)
return
}
completionOnMain(song)
}
}
I am working on user authentication process but i stuck in the moment when reciving data from rest with token. Whenever i create the new task it does not enter on the first time into the function but after creating it skipping doing smth else which is showing a next hooked up UIViewController to segue.
My rest service with post method hashing user password, creating json, URL request and at the end creating URLSession. How could i wait for finish of this task ? To not let to do anything else before it is not complited ?
EDIT
I've added OpeartionQueue to liquidate nil's from next view.
func postLogin(name:String, pass:String, completion: #escaping (Bool) -> () ) {
let md5Data = self.MD5(string:pass)
let hashPass = md5Data!.map { String(format: "%02hhx", $0) }.joined()
let json: [String: Any] = ["username": name,
"passwordHash": hashPass ]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
// create post request
let url = URL(string: LOGIN_URL)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// insert json data to the request
request.httpBody = jsonData
request.setValue("application/json;charest=utf-8", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
print(responseJSON)
let message:String = responseJSON["message"] as! String
if !(message.range(of: "ERROR") != nil){
SessionMenager.Instance.token = message
completion(true)
}
} else{
print(error.debugDescription)
}
}
task.resume()
}
Then simply in my LoginViewController action with button :
#IBAction func LoginButton(_ sender: Any) {
let username = usernameTextField.text
let password = passwordTextField.text
if username == "" {
AlertWindow(title: "Username", message: "Wrong username")
} else if password == "" {
AlertWindow(title: "Password", message: "Wrong password")
} else {
let usernameToUpper = username!.uppercased()
RestService.Instance.postLogin(name: usernameToUpper, pass: password!, completion: { sth in
if sth {
OperationQueue.main.addOperation {
[weak self] in
self?.performSegue(withIdentifier: "mapSegue", sender: self)
}
} else {
return
}
})
}
}
The segue was hooked up into LoginButton which took me instantly to the next page. I've changed it into hooking up all view controllerr.
Thanks!
Because your segue is hooked up into LoginButton, it will automatically show the next viewController once you press the button.
Just hoop up the segue to the whole viewController and it should work.
I want to connect in an app in swift 2.1
I have a button logIn and I make a function loginButton.
I want to recover my url: localhost/connexion/login/password
And with that I want to say if the user is in the database it's ok !
But I don't really anderstant swift, I'm a beginner in this language.
So there is my code:
#IBAction func loginButton(sender: AnyObject) {
NSLog("login ok")
let _login = loginText.text
let _password = passwordText.text
if(_login!.isEmpty || _password!.isEmpty){
var alert:UIAlertView = UIAlertView()
alert.title = "Error"
alert.message = "Entrez vos identifiants"
alert.delegate = self
alert.addButtonWithTitle("OK")
alert.show()
} else{
let request = NSMutableURLRequest(URL: NSURL(string: "http://localhost:8888/connexion/"+_login!+"/"+_password!)!)
request.HTTPMethod = "GET"
let postString = "login=\(_login!)&pass=\(_password)"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request)
task.resume();
}
}
I have follow this before How to make an HTTP request in Swift?
but it doesn't work.
I tried a lot of things, but without really understand what happened and I don't find a great tutorial with very good explanation. If someone can explain me how to do it I will be very happy !
I think for sending data to server you should create a "POST" request and use NSURLSession API to send data
#IBAction func loginButton(sender: AnyObject) {
NSLog("login ok")
let _login = loginText.text
let _password = passwordText.text
if(_login.isEmpty || _password.isEmpty){
var alert:UIAlertView = UIAlertView()
alert.title = "Error"
alert.message = "Entrez vos identifiants"
alert.delegate = self
alert.addButtonWithTitle("OK")
alert.show()
} else{
let request = NSMutableURLRequest(URL: NSURL(string: "http://localhost:8888/connexion/login")!)
request.HTTPMethod = "POST"
let params = ["login": _login, "pass": _password]
do {
let data = try NSJSONSerialization.dataWithJSONObject(params, options: .PrettyPrinted)
request.HTTPBody = data
} catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
let loginTask = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data, response, error) in
guard let data = data, let _ = response where error == nil else {
print("error")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
print(json)
} catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
})
loginTask.resume()
}
}
for using "GET" replace else part with
let url = "http://localhost:8888/connexion/login=\(_login)&pass=\(_password)"
let urlString = url.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
let request = NSURLRequest(URL: NSURL(string: urlString)!)
let loginTask = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data, response, error) in
guard let data = data, let _ = response where error == nil else {
print("error")
return
}
/*do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
print(json)
} catch let error as NSError {
print("json error: \(error.localizedDescription)")
}*/
if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = String(data: data!, encoding:NSUTF8StringEncoding)
print("responseString = \(responseString)")
})
loginTask.resume()