How to upload data using the new swift async/await methods? - swift

The new features in Swift with async/await allow a better control about the process and a simplification in coding. But I cannot find out how this method can be applied for requests which go above a simple data reading. E.g. I need to pass a parameter in order to get a specific answer from a SQL database in the backend (accessed via php).
At first my code about the "standard" way to start with. This function reads all customer records and stores them into an account-array:
#available(iOS 15.0, *)
static func getCustomerListFromBackend() async throws -> [Account] {
let url = URL(string: "https://xxx.de/xxx/getCustomerList.php")!
let (data, _) = try await URLSession.shared.data(from: url)
var accounts: [Account] = []
accounts = try JSONDecoder().decode([Account].self, from: data)
return accounts
}
In order to make my question clear, now a piece of code in which the central statement does not work and exist. In this function I want to check whether a customer exists in the database and I need to pass the emailAddress as a parameter.
#available(iOS 15.0, *)
static func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var dataString = "EmailAddress=\(emailAddress)"
let dataD = dataString.data(using: .utf8)
// Statement which does not work but for which I need an alternative
let (data, _) = try await URLSession.shared.upload(request: request, data: dataD)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
Unfortunately there is no statement for URLSession.shared.upload(request: request, data: dataD) Until now (before async/await), I used URLSession.shared.uploadTask(with: request, from: dataD) and then used .resume() to process it. This method however gave me too many problems in controlling the right sequence of tasks in the app. Async/await could simplify this very much as in my first example.
So, is there a way to realize this? Any advice would be appreciated.

you could try using URLComponents something like:
func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
if let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php"),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = [URLQueryItem(name: "EmailAddress", value: emailAddress)]
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
let (data, _) = try await URLSession.shared.data(for: request)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
throw URLError(.badURL)
}

My question was answered by Florian Friedrich's comment and workingdog's answer as well. To the later one I had to make a little adoption which I want to reflect here in this wrap up in case it can be helpful for someone with a similar problem. I show here 2 solutions to my problem with a few remarks.
Applying Florian's answer.
This was straightforward and worked right away:
static func checkCustomerExistsInBackend(emailAddress: String) async throws -> String {
let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
let dataString = "EmailAddress=\(emailAddress)"
let dataD = dataString.data(using: .utf8)
let (data, _) = try await URLSession.shared.upload(for: request, from: dataD!, delegate: nil)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
The proposal from workingdog:
Here I noticed that although the url appeared to be correctly set (ending with checkCustomerexists.php?EmailAddress=test#gmx.de), the parameter did not arrive in my php object. After some tests I found out that it works when I use GET instead of POST. So in my php file I changed the line $EmailAddress = $_POST[EmailAddress]; to $EmailAddress = $_GET['EmailAddress'];. (I am sure there is a good reason for this and I am just not experienced enough to recognize this.) Accordingly the code I use for workingdog's proposal is slightly adjusted:
func checkCustomerExistsInBackend3(emailAddress: String) async throws -> String {
if let url = URL(string: "https://xxx.de/xxx/checkCustomerexists.php"),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = [URLQueryItem(name: "EmailAddress", value: emailAddress)]
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
let (data, _) = try await URLSession.shared.data(for: request)
let answer = try JSONDecoder().decode(BackendMessage.self, from: data)
return answer.Message
}
throw URLError(.badURL)
}

Related

Simple post request in SwiftUI

I'm beginner in SwiftUI and I'm not familiar with variable management.
I'd like to send a very simple post request like this one with SwiftUI:
let full_path : String = "https://www.example.com/get_answer?my_message=current temperature"
I've tried with this piece of code but it didn't work.
if (URL(string: full_path) != nil) {
let url = URL(string: full_path)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var decodedAnswer = String("")
do {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
print(response)
decodedAnswer = String(decoding: response, as: UTF8.self)
}
}
I have the following error:
Value of optional type 'URLResponse?' must be unwrapped to a value of
type 'URLResponse'
I don't know how to get the response.
How can I get the response from a simple Post request in SwiftUI?
Multiple issues here.
You are trying to decode the URLResponse object, but what you want is the data object in the decoder.
You seem to not know about optionals. I would refer you to the basic Apple tutorials about this topic. You can find it with your favorite search engine.
You are in an async context here. Everything inside the url datasession closure will be execute after your network request returns. The code in your function will be completed by that moment and your var decodedAnswer will be out of scope. So move it out of the function in to the class/struct.
You probably want something like this:
This should be defined in class scope or you won´t be able to use it:
var decodedAnswer: String = ""
This should be in a function:
let full_path: String = "https://www.example.com/get_answer?my_message=current temperature"
if let url = URL(string: full_path) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
do {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
//This converts the optionals in to non optionals that could be used further on
//Be aware this will just return when something goes wrong
guard let data = data, let response = response, error == nil else{
print("Something went wrong: error: \(error?.localizedDescription ?? "unkown error")")
return
}
print(response)
decodedAnswer = String(decoding: data, as: UTF8.self)
}
task.resume()
}
}

confused on how to create the Stripe ephemeral key in the APIClient.swift class

So for the last 2 days I've been stumped on how to implement this Stripe API, it's by far been the hardest thing to wrap my head around with. So I decided to integrate the Stripe functionality using Firebase and Cloud Functions and I've been seeing that it's server-less which is great.
I've been trying to follow this article on iOS Stripe API integration and this article showing how to create the cloud functions involving Stripe and I so far have been able to create a Stripe customer upon new user creation. After that, I'm pretty much lost on how to do what I want to do next, which is create ephemeral keys.
I have this function I snagged from another SO post:
exports.createEphemeralKeys = functions.https.onRequest((req, res) => {
var api_version = req.body.api_version;
var customerId = req.body.customerId;
if (!api_version) {
res.status(400).end();
return;
}
stripe.ephemeralKeys.create(
{ customer: customerId },
{ stripe_version: api_version },
function(err, key) {
return res.send(key);
});
});
And this is the method in the MyAPIClient.swift file:
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
let url = self.baseURL.appendingPathComponent("ephemeral_keys")
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)!
urlComponents.queryItems = [URLQueryItem(name: "api_version", value: apiVersion)]
var request = URLRequest(url: urlComponents.url!)
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
guard let response = response as? HTTPURLResponse,
response.statusCode == 200,
let data = data,
let json = ((try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]) as [String : Any]??) else {
completion(nil, error)
return
}
completion(json, nil)
})
task.resume()
}
Now this is where I get confused, since integrating Stripe with Firebase is server-less, what are we supposed to input in the baseURL? Currently the baseURL variable is empty, but I'm also getting thrown errors that .appendingPathComponent is not available as well. Forgive me if this is a dumb question, but I'd much rather look like a complete idiot and eventually figure out how to successfully integrate this API, than not ask anything at all. Thanks in advance.
The baseURL should be set to the URL where your Firebase functions live. Have a look at the Firebase documentation for invoking an HTTP function for details.
The URL will be something like this:
https://<region>-<project-id>.cloudfunctions.net/

Passing headers to URL with Swift URLSession

I don't feel like sharing the actual link as it may have some private information so don't be surprised if it doesn't work.
I have a link that looks like this: www.somelink.com/stuff/searchmembers?member=John
And some headers that I need to pass, like Login: Admin, Password: Admin
When I use this site everything seems to be working just fine, I put the link, make it GET and put headers in key:value format and as a result I get the list of all members, but how can I do the same with URLSession? Here's what I currently have and I don't get anything at all. What am I doing wrong there?
func getAllMembers(urlString: String) {
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Admin", forHTTPHeaderField: "Login")
request.setValue("Admin", forHTTPHeaderField: "Password")
request.httpBody = "member=John".data(using: .utf8)!
URLSession.shared.dataTask(with: request) { (data, response, error) in
print(response)
print(data)
}.resume()
}
Here is What You're Doing Wrong:
Your member=John is actually a URL query parameter. In general, URL requests have query parameters as a part of the URL string itself and not the httpbody.
Quick and Dirty Solution:
You should be good to go if you remove
request.httpBody = "member=John".data(using: .utf8)!
and instead pass the whole "www.somelink.com/stuff/searchmembers?member=John" into your getAllMembers(urlString:) function.
A Better Solution:
Let's say John's username is j o h n. Your function wouldn't make it past that first guard because spaces aren't valid URL string characters.
I like to use URLComponents because it saves me the trouble of having to handle spaces and such.
Here's how I'd write your function:
func getJohnMember(urlString: String) {
//URLComponents to the rescue!
var urlBuilder = URLComponents(string: urlString)
urlBuilder?.queryItems = [
URLQueryItem(name: "member", value: "j o h n")
]
guard let url = urlBuilder?.url else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Admin", forHTTPHeaderField: "Login")
request.setValue("Admin", forHTTPHeaderField: "Password")
URLSession.shared.dataTask(with: request) { (data, response, error) in
print(response)
print(String(data: data, encoding: .utf8)) //Try this too!
}.resume()
}
Just to be clear, I would pass just "www.somelink.com/stuff/searchmembers" into the first parameter.
Now if I were to print(url) after the guard let, I'd get
www.somelink.com/stuff/searchmembers?member=j%20o%20h%20n
Much easier this way, no?
That member=John is a URL-query parameter, not part of the request body. So you need to add it to the URL itself.
func getAllMembers(urlString: String) {
guard let url = URL(string: "\(urlString)?member=John") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Admin", forHTTPHeaderField: "Login")
request.setValue("Admin", forHTTPHeaderField: "Password")
URLSession.shared.dataTask(with: request) { (data, response, error) in
print(response)
print(data)
}.resume()
}

HttpRequest with multiple parameters swift

I'm trying to create a request with multiple parameters using Swift. So far I managed to create with one parameter but not with multiple.
I tried to use a Dictionary but couldn't do it.
Here is my actual code:
let protocolo = txtProtocolo.text!
var request = URLRequest(url: url)
let parameters = "protocolo=\(protocolo) "
request.httpMethod = "POST"
request.httpBody = parameters.data(using: String.Encoding.utf8)
URLSession.shared.dataTask(with: request)
{ (data, response, error) in
....
I'm trying to do something like this:
let dictionary = ["protocolo":protocolo,
"secondParameter": "value"]
And use this dictionary as httpBody.
Thanks in advance for your help.
If you have the option use Alamofire. It is very good :)
But if you want to use the dictionary. It seems you have to convert it to a string. Did you try something like
let parameters = ["auth":"asdf", "width":"123"]
let parametersString = (parameters.compactMap({ (key, value) -> String in
return "\(key)=\(value)"
}) as Array).joined(separator: "&")
And use the parametersString as the parameter

Alamofire multiple parameters (Query and Form) Swift 4

I'm having a problem sending a POST request with Alamofire.
I need to send the usser and password fields as application/x-www-form-urlencode and also some query data in the url.
I am creating a URLRequest to handle the process, but I'm getting always a 400 response from the server, so I guess the problem must be in the way I create the request.
This is the example in Postman:
I need to send a param in the url and two more in as application/x-www-form-urlencode
Postman 1 - Parameters
Postman 2 - ContentType
I need to do this (that i have in Android)
#FormUrlEncoded
#POST(Constants.AUTH_LDAP)
Call<ResponseBody> authLdap(
#Query(value = Constants.PARAM_REQ, encoded = true) String req,
#Field(Constants.PARAM_LOGIN) String login,
#Field(Constants.PARAM_PASSWORD) String password
);
And this is what I have in swift
let queryParamters = [Constants.Params.PARAM_REQ:req]
let headers = ["Content-Type": "application/x-www-form-urlencoded"]
let fieldParameters = [
Constants.Params.PARAM_LOGIN : user,
Constants.Params.PARAM_PASSWORD : pass]
let url = URL(string: Constants.EndPoints.AUTH_LDAP)
let request = URLRequest(url: url!)
let encoding = try URLEncoding.default.encode(request, with: queryParamters as Parameters)
let encodingpa = try URLEncoding.httpBody.encode(request, with: fieldParameters as Parameters)
var urlRequest = encodingpa
urlRequest.url = encoding.url
urlRequest.allHTTPHeaderFields = headers
Alamofire.request(urlRequest).responseData(completionHandler: { response in
switch response.result {
case .success:
print("sucess")
print(response.response)
case .failure(let error):
print(error)
}
})
Thanks for your help.
Try to create url from queryParameters using URLComponents like
var urlComponents = URLComponents(string: Constants.EndPoints.AUTH_LDAP)!
urlComponents.queryItems = [
URLQueryItem(name: Constants.Params.PARAM_REQ, value: req)
]
let headers = ["Content-Type": "application/x-www-form-urlencoded"]
var request = URLRequest(url: urlComponents.url!)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject: fieldParameters)
request.allHTTPHeaderFields = headers
Alamofire.request(request).responseJSON { response in
}