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 load my json info from my server as follows, but when I click away to a different page too soon, the request keeps trying in the background and there is a warning that the viewController can't be found anymore. How would I cancel all requests onViewDisappear?
if let requestURL = URL(string: "https://www.example.com/file.php") {
var urlRequest = URLRequest(url: requestURL)
urlRequest.httpMethod = "POST"
let postString = "email=\(loginUsername.text!)"
urlRequest.httpBody = postString.data(using: .utf8)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest as URLRequest) { (data, response, error) in
if let data = data {
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] {
// Async Stuff
DispatchQueue.main.async{
// do things
}
DispatchQueue.main.async(execute: {
})
}
} catch {
print("Error: \(error)")
}
}
}
task.resume()
}
Save your requests somewhere and then on moving away from the controller call:
task.cancel()
class DataCall {
var task: URLSessionDataTask?
func load() {
guard let requestURL = URL(string: "https://www.example.com/file.php") else { return }
var urlRequest = URLRequest(url: requestURL)
urlRequest.httpMethod = "POST"
let postString = "email=\(loginUsername.text!)"
urlRequest.httpBody = postString.data(using: .utf8)
let session = URLSession.shared
task = session.dataTask(with: urlRequest as URLRequest) { (data, response, error) in
if let data = data {
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] {
// Async Stuff
DispatchQueue.main.async{
// do things
}
}
} catch {
print("Error: \(error)")
}
}
}
task.resume()
}
func stopTask() {
task.cancel()
}
}
Then in your viewWillDissapear you call dataCall.stopTask().
I currently have a class which is below:
class Anton {
//URL to web service (Internal)
let URL_DISPLAY_MENU = "http://192.168.1.100/api/DisplayMenu.php"
func displayMenu(completion: #escaping ([[String:Any]]) ->()) {
let requestURL = URL(string: URL_DISPLAY_MENU)
var request = URLRequest(url: requestURL!)
request.httpMethod = "POST"
var menu: [[String:Any]]?
let task = URLSession.shared.dataTask(with: request) { data,response,error in guard let data = data, error == nil else {
print("error=\(String(describing: error))")
return
}
do {
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
} else {
menu = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] ?? []
var dictionary = [Int:Any]()
for (index,item) in menu!.enumerated() {
let uniqueID = index
dictionary[uniqueID] = item
}
completion(menu!)
}
} catch let error as NSError {
print(error)
}
}
task.resume()
}
}
At the moment I use the class & contained function as follows:
var anton = Anton()
anton.displayMenu { menu in
print(menu)
}
What I want to do is have a way of saving the #escaping result into a global variable. I'm very new to escaping closures so not sure how to go about this.
You can save your value directly in completion.
Example:
var anton = Anton()
anton.displayMenu { menu in
self.myMenu = menu
}
I wan't to check if my url statusCode equals to 200, I created a function returning a Boolean if the statusCode equals to 200, I'm using a dataTask, but I don't know how to return a value:
class func checkUrl(urlString: String) -> Bool{
let urlPath: String = urlString
var url: NSURL = NSURL(string: urlPath)!
var request: NSURLRequest = NSURLRequest(url: url as URL)
var response: URLResponse?
let session = Foundation.URLSession.shared
var task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let error = error {
print(error)
}
if let data = data{
print("data =\(data)")
}
if let response = response {
print("url = \(response.url!)")
print("response = \(response)")
let httpResponse = response as! HTTPURLResponse
print("response code = \(httpResponse.statusCode)")
if httpResponse.statusCode == 200{
return true
} else {
return false
}
}
})
task.resume()
}
The returns in if else are returning an error:
Unexpected non-void return value in void function
in order to return value you should use blocks. Try declaring your function like this:
class func checkUrl(urlString: String, finished: ((isSuccess: Bool)->Void) {
let urlPath: String = urlString
var url: NSURL = NSURL(string: urlPath)!
var request: NSURLRequest = NSURLRequest(url: url as URL)
var response: URLResponse?
let session = Foundation.URLSession.shared
var task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let error = error {
print(error)
}
if let data = data{
print("data =\(data)")
}
if let response = response {
print("url = \(response.url!)")
print("response = \(response)")
let httpResponse = response as! HTTPURLResponse
print("response code = \(httpResponse.statusCode)")
if httpResponse.statusCode == 200{
finished(isSuccess: true)
} else {
finished(isSuccess: false)
}
}
})
task.resume()
}
And then call it like this:
checkUrl("http://myBestURL.com", finished { isSuccess in
// Handle logic after return here
})
Hope that this will help.
Consider semaphore if you want to keep your original return pattern.
func checkUrl(urlString: String) -> Bool {
if let url = URL(string: fileUrl) {
var result: Bool!
let semaphore = DispatchSemaphore(value: 0) //1. create a counting semaphore
let session = URLSession.shared
session.dataTask(with: url, completionHandler: { (data, response, error) in
result = true //or false in case
semaphore.signal() //3. count it up
}).resume()
semaphore.wait() //2. wait for finished counting
return result
}
return false
}
Swift4, work in my case
Try to add guard let data = data else { return } in dataTask like:
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
print("get some data")
}.resume()
You're returning a value from a Void function that is the completionHandler closure of dataTask(_:, _:)
Regarding your code, there is something wrong: you can't return that value because it's executed on a different thread, it's an asynchronous operation. Please take a look at this thread: Returning data from async call in Swift function
I want to wait for a responseString to complete before calling the next function "nextScreen()" (segue). At the moment I have an if statement to make sure it is not nil before proceeding, but sometimes the the next function/segue is called because the responseString is still downloading.
Could you help with a completion block? I have found completion blocks for NSURLSession, but these just wait for the initial HTTP call to complete, not the response string.
func getProfiles(){
func post(completion: (message: String?) -> Void) {
let request = NSMutableURLRequest(URL: NSURL(string: "http://**.**.**.**/EPG/XML/QueryProfile")!)
request.HTTPMethod = "POST"
let postString = "<QueryProfileReq><type>1</type></QueryProfileReq>"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task: Void = NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {(data: NSData!,
response: NSURLResponse!,
error: NSError!) in
if error != nil {
println("error=\(error)")
let alert = UIAlertView()
alert.delegate = self
alert.title = "Login Error"
alert.message = "\(error)"
alert.addButtonWithTitle("OK")
alert.show()
self.view.endEditing(true)
return
}
if let responseString = NSString(data: data, encoding: NSUTF8StringEncoding) {
if response != nil {
println("got profiles")
self.nextScreen()
}
self.dataVar = data // UPDATES VARIABLE TO SEND
}
}).resume()
}
}
The convenience method of dataTaskWithRequest essentially returns data or error, with usually some response header type information. If you have an error then you won't have data (99% sure about this). I have re formatted your method to help. The NSString Init Convenience method is synchronous so not quite sure by what you mean by waiting to complete instead of http call?
func getStringFromRequest(completionHandler:(success:Bool, data: NSData?) -> Void) {
let request = NSMutableURLRequest(URL: NSURL(string: "http://##.##.##.##/EPG/XML/QueryProfile")!)
request.HTTPMethod = "POST"
let postString = "<QueryProfileReq><type>1</type></QueryProfileReq>"
request.HTTPBody = postString.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
if let unwrappedError = error {
print("error=\(unwrappedError)")
}
else {
if let unwrappedData = data {
completionHandler(success: true, data: unwrappedData)
return
}
}
completionHandler(success: false, data: nil)
}
task?.resume()
}
func performPost() {
getStringFromRequest { (success, data) -> Void in
if (success) {
if let unwrappedData = data {
self.dataVar = unwrappedData
if let responseString = NSString(data: unwrappedData, encoding: NSUTF8StringEncoding) {
self.nextScreen()
}
}
}
else {
print("Failed")
}
}
}