Several co-dependent JSON requests on Swift 4 - swift4

I am refactoring a previous code I made, where I use Alamofire to download some Json files.
The fisrt request is straight forward. I make the request, I got the response and I parse it and store it on Realm. No problem here. Straight forward stuff.
The second request is a little trickier, because I need several ID that was retrieved from the first JSON request.
My solution for that problem was first to create a Completion handler on the function that has the Alamofire request:
func requestData(httpMethod: String, param: Any?, CallType : String, complition: #escaping (Bool, Any?, Error?) -> Void){
My idea was to use the Completion to wait for the Alamofire Response to finish and then start the new request. Turn out that didn't work as well.
I was able to pull this off by adding a delay to the Completion.
DispatchQueue.main.asyncAfter(deadline: .now() + 4)
It does work, but is far from being a good practice for several reasons and I would like to refactor that with something more intelligent.
My questions:
1) How is the best way to make many JSON requests on the same function? A way to correctly wait the first one to start the second on an so on?
2) Right now, I call a function to request the first JSON, and on the middle of the call I make a second request. It seems to me that I am hanging the first request too long, waiting for all requests to finish to then finish the first one. I don't think that is a good practice
Here is the complete code. Appreciate the help
#IBAction func getDataButtonPressed(_ sender: Any) {
requestData(httpMethod: "GET", param: nil, CallType: "budgets") { (sucess, response, error) in
if sucess{
print("ready")
DispatchQueue.main.asyncAfter(deadline: .now() + 4){
accounts = realm.objects(Account.self)
requestAccounts()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4){
users = realm.objects(User.self)
requestUser()
}
}
}
}
func requestData(httpMethod: String, param: Any?, CallType : String, complition: #escaping (Bool, Any?, Error?) -> Void){
let url = "https://XPTO.com/v1/\(CallType)"
guard let urlAddress = URL(string: url) else {return}
var request = URLRequest(url: urlAddress)
request.httpMethod = httpMethod
request.addValue("application/json", forHTTPHeaderField: "accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer appKey", forHTTPHeaderField: "Authorization")
if param != nil{
guard let httpBody = try? JSONSerialization.data(withJSONObject: param!, options:[]) else {return}
request.httpBody = httpBody
}
Alamofire.request(request).responseJSON { (response) in
let statusCode = response.response?.statusCode
print("Status Code \(statusCode!)")
jsonData = try! JSON(data: response.data!)
complition(true, jsonData, nil)
if httpMethod == "GET"{
saveJsonResponse(jsonData: jsonData, CallType: CallType)
}
}
}
func requestAccounts(){
var count = accounts.count
while count != 0{
let account = accounts[0]
RealmServices.shared.delete(account)
count -= 1
}
let numberOfBugdets = budgets.count
for i in 0...numberOfBugdets - 1{
requestData(httpMethod: "GET", param: nil, CallType: "/budgets/\(budgets[i].id)/accounts") { (sucess, response, error) in
print("accounts downloaded")
let numberOfAccounts = jsonData["data"]["accounts"].count
for j in 0...numberOfAccounts - 1{
let realm = try! Realm()
do{
try realm.write {
// Code to save JSON data to Realm
realm.add(newAccount)
}
} catch {
print("something")
}
}
}
}
}
func requestUser(){
var count = users.count
while count != 0{
let user = users[0]
RealmServices.shared.delete(user)
count -= 1
}
requestData(httpMethod: "GET", param: nil, CallType: "user") { (success, response, error) in
print("User data downloaded")
let realm = try! Realm()
do{
try realm.write {
// Code to save JSON data to Realm
realm.add(newUser)
}
} catch {
print("something")
}
}
}
func saveJsonResponse(jsonData: JSON, CallType: String){
case "budgets":
var count = budgets.count
while count != 0{
let budget = budgets[0]
RealmServices.shared.delete(budget)
count -= 1
}
let numberOfBudgets = jsonData["data"]["budgets"].count
for i in 0...numberOfBudgets - 1 {
// Code to save JSON data to Realm
RealmServices.shared.create(newBudget)
}
}

I recommend completionHandlers in such Situation.
This is how your code snippet on how to implement it and use it try to understand it and implement it in your code.
//CompletionHandlers
func firstOperation(completionHandler: #escaping (_ id: String) -> Void){
//preform alamoFire and in .response { } call completionHandler and pass it the id
completionHandler("10")
}
func buttonClicked () {
firstOperation { (id) in
secondFunction(completionHandler: { (data) in
// your data
})
}
}
func secondFunction(completionHandler: #escaping (_ data: String) -> Void){
//preform alamoFire and in .response { } call completionHandler and pass it the id
completionHandler("some Data")
}
This should give you a better understanding on how to implement it, CompletionHandlers are powerful
specially in handling such cases when you have to perform an action that depends on other action results and in networking we can't anyhow predict the time of the operation.
Read more about completionHandlers here

Related

How to send Apple Pay Token to PayU

I haver an application which should use Apple Pay.
As soon as user accept payment and Apple Pay works well, like Face ID or PIN,
I would like to send Apple Pay Token to PayU.
Unfortunatelly I did not find and support in PayU (https://czech.payu.com/en/)
In my ApplePayViewController I have method like:
class ApplePayViewController: {
func submitToken(payment: PKPayment, completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
let result = PKPaymentAuthorizationResult(status: .failure, errors: nil)
let jsonPayment = try? JSONSerialization.jsonObject(with: payment.token.paymentData, options: []) as? Dictionary<String, AnyObject>
if let jsonData = jsonPayment {
let jsonTokenString = String(decoding: payment.token.paymentData, as: UTF8.self)
print("Json Data String: \(jsonTokenString)")
}
// What should be sent to PayU?
// paymentData? https://developer.apple.com/documentation/passkit/pkpaymenttoken/1617000-paymentdata
// Or data from Payment Token Structure https://developer.apple.com/documentation/passkit/apple_pay/payment_token_format_reference#//apple_ref/doc/uid/TP40014929
// Data are sent to my server over API like
// https://<URL>/api/send_payment&user=<username>&token=<token>&payment_data=<ApplePayToken>
do {
send_payment(data: paymentData, completion: { apiResponse in
switch apiResponse {
case .success(let zapisStatus):
result.status = PKPaymentAuthorizationStatus.success
completion(result)
case .failure(let error):
result.status = PKPaymentAuthorizationStatus.failure
completion(result)
}
})
}
}
extension ApplePayViewController: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController!, didAuthorizePayment payment: PKPayment!, handler completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
let status = PKPaymentAuthorizationStatus(rawValue: 0)!
switch status.rawValue {
case 0:
self.paymentStatus = PKPaymentAuthorizationStatus.success
completion(PKPaymentAuthorizationResult(status: PKPaymentAuthorizationStatus.success, errors: nil))
case 1:
self.paymentStatus = PKPaymentAuthorizationStatus.failure
completion(PKPaymentAuthorizationResult(status: PKPaymentAuthorizationStatus.failure, errors: nil))
default:
self.paymentStatus = PKPaymentAuthorizationStatus.failure
completion(PKPaymentAuthorizationResult(status: PKPaymentAuthorizationStatus.failure, errors: nil))
}
if payment.token.paymentData.count > 0 {
self.submitToken(payment: payment)
}
}
The data are send over by this code:
public func getURLRequest(endpoint: String, data: Data) -> URLRequest{
guard let url = URL(string: RestAPIHelper.RestAPIKeys.url_pub + endpoint) else {
fatalError()
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = data
return urlRequest
}
func send_payment(data: Data, completion: #escaping(Result<[UserPlatbyInfo], APIError>) -> Void) {
let urlRequest = getURLRequest(endpoint: APIInterface.user_zapisplatbu.rawValue, data: data)
let dataTask = URLSession.shared.dataTask(with: urlRequest) { respData, resp, _ in
// Handle return status
dataTask.resume()
}
Basically I don't know what PayU expects in order to accept ApplePayToken.

Generic parameter 'T' could not be inferred: in Common Webservice method

I am trying to make generic post method for API call.In my loadNew method I want to add normal dictionary inside resource object.Resource contains normal data which will pass from controller class.And dictionary is passed as body of request. but while encoding "Generic parameter 'T' could not be inferred" showing. How do I use dictionary in it?
struct Resource<T> {
let url: URL
let request: URLRequest
let dictionary : [String:Any]
let parse: (Data) -> T?
}
final class Webservice {
// MARK:- Generic
func load<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
URLSession.shared.dataTask(with: resource.url) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
func loadNew<T>(resource: Resource<T>, completion: #escaping (T?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
do {
//FIXIT: error is getting here
let jsonBody = try JSONEncoder().encode(resource.dictionary)
request.httpBody = jsonBody
}catch{}
let session = URLSession.shared
session.dataTask(with: request) { data, response, error in
if let data = data {
//completion call should happen in main thread
DispatchQueue.main.async {
completion(resource.parse(data))
}
} else {
completion(nil)
}
}.resume()
}
}
This method is called inside my Login controller.I have also tried assign it directly to request object but same error is showing
func APICall(){
guard let url = URL(string: Constants.HostName.local + Constants.API.User_Login) else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let resources = Resource<LoginReponse>(url: url, request: request, dictionary: dict){
data in
let loginModel = try? JSONDecoder().decode(LoginReponse.self, from: data)
return loginModel
}
// var response = LoginReponse()
Webservice().loadNew(resource: resources) {
result in
if let model = result {
print(model)
}
}
}
The error is a bit misleading, and may indicate you're using an older version of Xcode. In 11.4.1, the error is much more explicit:
error: value of protocol type 'Any' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols
The problem is that [String: Any] is not Encodable, because there's no way to encode "Any" (what should happen if you passed a UIViewController here? Or a CBPeripheral?)
Instead of a dictionary here, looking at your code I would expect you to pass an encodable object here. For example:
struct Resource<Value: Decodable, Parameters: Encodable> {
let url: URL
let request: URLRequest
let parameters : Parameters?
let parse: (Data) -> Value?
}
final class Webservice {
func loadNew<Value, Parameters>(resource: Resource<Value, Parameters>, completion: #escaping (Value?) -> ()) {
var request = resource.request
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
if let parameters = resource.parameters {
request.httpBody = try? JSONEncoder().encode(parameters)
}
// ...
}
That said, I'd probably turn this system around a bit. If you want to have a Request<T> (parameterized on the thing it returns, and not on the parameters it takes to generate it), that's fine. You can pack a bit more into the struct. For example:
let baseURL = URL(string: "https://example.com/api/")!
struct Resource<Value> {
let urlRequest: URLRequest
let parse: (Data) -> Result<Value, Error>
// Things you want as default for every request
static func makeStandardURLRequest(url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
return request
}
}
// It would be nice to have a default parser when you can, but you don't have to put that
// into Webservice. The Resource can handle it.
extension Resource where Value: Decodable {
init(urlRequest: URLRequest) {
self.init(urlRequest: urlRequest, parse: { data in
Result { try JSONDecoder().decode(Value.self, from: data) }
})
}
}
And then Resources are smart about themselves:
struct LoginParameters: Encodable {
let username: String
let password: String
}
struct LoginResult: Decodable {
let authToken: String
}
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent("login"))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
Of course that may get repeated a lot, so you can hoist it out:
extension Resource where Value: Decodable {
static func makeStandardURLRequest<Parameters>(endpoint: String, parameters: Parameters) -> URLRequest
where Parameters: Encodable {
var urlRequest = makeStandardURLRequest(url: baseURL.appendingPathComponent(endpoint))
urlRequest.httpBody = try? JSONEncoder().encode(parameters)
return Resource(urlRequest: urlRequest)
}
}
And then Login looks like:
extension Resource where Value == LoginResult {
static func login(parameters: LoginParameters) -> Resource {
return makeStandardURLRequest(endpoint: "login", parameters: parameters)
}
}
The point is that you can pull duplicated code into extensions; you don't need to stick it in the Webservice, or add more generic.
With that, your load gets a bit simpler and much more flexible. It focuses just on the networking part. That means that it's easier to swap out with something else (like something for unit tests) without having to mock out a bunch of functionality.
func load<Value>(request: Resource<Value>, completion: #escaping (Result<Value, Error>) -> ()) {
let session = URLSession.shared
session.dataTask(with: request.urlRequest) { data, response, error in
DispatchQueue.main.async {
if let data = data {
//completion call should happen in main thread
completion(request.parse(data))
} else if let error = error {
completion(.failure(error))
} else {
fatalError("This really should be impossible, but you can construct an 'unexpected error' here.")
}
}
}.resume()
}
There's a lots of ways to do this; for another, see this AltConf talk.

How to execute a synchronous api call after an asynchronous api call

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

Where do Dispatch Group commands go in code?

I am trying to run a function X times in a for in loop but when all the functions have returned I want to run another function.
Currently I have it working by delaying the final function 1 second but I would really like to get Dispatch Group working.
I've been through various online examples and other questions but nothing I try seems to work, The code I have at the moment I know won't work as it is running dispatchGroup.leave() each time the for in functions are sent rather than when they return.
I've tried puting the DispatchGroup code in the function (which is in another file) but I'm stumped, however I think I am close to a solution.
I've also looked at semaphores and using count and incrementing a value each time the loop runs but I keep coming back to DispatchGroups.
My last resort is to ask a question!
ViewController code
#IBAction func removeDeviceBtn(_ sender: Any) {
let dispatchGroup = DispatchGroup()
for owner in arrOwnerList {
dispatchGroup.enter()
self.removeDevice(device: self.device, account: owner as! String, completion: self.completed)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
self.removeDeviceFromServer(device: self.device)
self.sendEmail(to:"gordon#example.co.uk", subject:self.device+" has been removed", text:self.device+" has been removed from the server, please check the sim for bar and termination")
})
Function code in other file as an extension
func completed(isSuccess: Bool) {
}
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader : [String:String] = ["username":Username,"password":Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { (response) in
if response.count == 0 {
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
}
}
else {
if response.count != 0 {
let isSuccess = true
completion(isSuccess)
}
else{
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.NoDataFound, on: self)
}
}
}
}) { (error) in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
}
}
}
Code from WebHelper file
class func requestPUTAPIRemoveDevice(_ strURL: String,header: Dictionary<String,String>,dictArray: Array<Any>, controllerView viewController: UIViewController, success: #escaping (_ response: [AnyHashable: Any]) -> Void, failure: #escaping (_ error: Error?) -> Void) {
if GlobalConstant.isReachable() {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.showHUD()
}
let loginString = String(format: "%#:%#", header["username"]!, header["password"]!)
let loginData: Data = loginString.data(using: String.Encoding.utf8)!
let base64LoginString = loginData.base64EncodedString(options: NSData.Base64EncodingOptions())
let headers = ["Authorization": "Basic "+base64LoginString, "Referer": "http://www.example.com"]
let postData = try? JSONSerialization.data(withJSONObject: dictArray, options: [])
let request = NSMutableURLRequest(url: NSURL(string: strURL)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "PUT"
request.allHTTPHeaderFields = headers
request.httpBody = postData
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
} else {
if let httpResponse = response as? HTTPURLResponse {
print("Server code \(httpResponse.statusCode)")
if httpResponse.statusCode == 200 || httpResponse.statusCode == 208 {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
let jsonResult = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers)
if (jsonResult is NSDictionary) {
success(jsonResult as! [AnyHashable : Any])
}
else if (jsonResult is NSArray) {
success(["response":jsonResult as! NSArray])
}
else{
success(["response":httpResponse.statusCode])
DispatchQueue.main.async {
}
}
}
else{
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
}
}
}
})
dataTask.resume()
}
else {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
GlobalConstant.showAlertMessage(withOkButtonAndTitle: "", andMessage: "Internet not connected", on: viewController)
}
}
}
The final solution (apart from tidying up the various other issues) was to add success(["response":httpResponse.statusCode]) to the WebHelper file, corrected code above
Put the leave inside the completion handler:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { [weak self] success in
self?.completed(isSuccess: success)
dispatchGroup.leave()
}
}
Or given that you’re not really doing anything in completed function, I’d just remove that:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { _ in
dispatchGroup.leave()
}
}
I notice that you have paths of execution in removeDevice that aren’t calling the completion handler. Make sure every path of execution calls the completion handler or else your dispatch group will never get resolved.
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader = ["username": Username, "password": Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { response in
DispatchQueue.main.async {
if response.count == 0 {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
completion(false)
} else {
completion(true)
}
}
}, failure: { error in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
completion(false)
}
})
}
By the way, I don’t know the name of the “failure” closure, so I assumed it was failure, but adjust as required by your requestPUTAPIRemoveDevice method. We generally avoid the multiple closure pattern in Swift, but if you’re going to do that, I’d avoid the trailing closure syntax. It makes the functional intent of this second closure a bit more explicit.
Or, this all begs the question as to why requestPUTAPIRemoveDevice is initiating UI updates at all. I’d probably put that in the view controller method. So requestPUTAPIRemoveDevice should just pass back enough information so the removeDeviceBtn routines knows what error to present. And this idea of presenting a separate error message for each failure is probably suspect, too. (E.g. if you have lost internet connection and are trying to remove a dozen devices, do you really want to show a dozen separate error messages?) But this is beyond the scope of this question.

Returning data from async call that takes multiple params in Swift function

I was trying to create a post method so I could reuse it further in my code.
I saw this example Returning data from async call in Swift function that gives partial solution to my problem but don't know how to call the function once I define it.
This is the function I am trying to call:
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) {
(data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
This is the caller function:
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
//in the line below I get the error message, extra argument "request" in call.
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
You cannot make loginPostRequest return NSDictionary because you are making async call with what you need is to create completion block same way you have create with postRequest method also from Swift 3 you need to use URLRequest with mutable var object instead of NSMutableURLRequest you need to also change the postRequest function's request argument type to URLRequest so latter no need to convert NSMutableURLRequest to URLRequest and use Swift type dictionary instead of NSDictionary
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key as! String) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
Now simply changed the argument type of request to URLRequest from NSMutableURLRequest in method postRequest
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) { (data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
Now when you call this loginPostRequest you are having response in completion block of it.
Functions that receive a closure as parameter can be called like any other functions:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false, completionHandler: { postRequestStatus in
// ... code that will run once the request is done
})
If the closure is the last parameter you can pass it outside the parenthesis:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false) { postRequestStatus in
// ... code that will run once the request is done
})
You can check the Swift book to learn more about closures and functions.
By the way, your postRequest method looks weird, I haven't checked deeply into it, but for instance I believe although url is one of the parameters it isn't actually used. Some other answer pointed other problems into that function.