I try to get some data from server using URLSession.shared.dataTask. It works fine, but I can't save result like a class variable. Many answers recommend to use completion Handler, but it doesn't help for my task.
Here is my testing code:
class PostForData {
func forData(completion: @escaping (String) -> ()) {
if let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php") {
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString : String = "json={\"Ivan Bolgov\":\"050-062-0769\"}"
print(postString)
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) {
data, response, error in
let json = String(data: data!, encoding: String.Encoding.utf8)!
completion(json)
}
task.resume()
}
}
}
class ViewController: UIViewController {
var str:String?
override func viewDidLoad() {
super.viewDidLoad()
let pfd = PostForData()
pfd.forData { jsonString in
print(jsonString)
DispatchQueue.main.async {
self.str = jsonString
}
}
print(str ?? "not init yet")
}
}
This closure is @escaping
(i.e. it's asynchronously called later), so you have to put it inside the closure:
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
var str: String?
override func viewDidLoad() {
super.viewDidLoad()
let pfd = PostForData()
pfd.performRequest { jsonString, error in
guard let jsonString = jsonString, error == nil else {
print(error ?? "Unknown error")
return
}
// use jsonString inside this closure ...
DispatchQueue.main.async {
self.str = jsonString
self.label.text = jsonString
}
}
// ... but not after it, because the above runs asynchronously (i.e. later)
}
}
Note, I changed your closure to return String?
and Error?
so that the the view controller can know whether an error occurred or not (and if it cares, it can see what sort of error happened).
Note, I renamed your forData
to be performRequest
. Generally you'd use even more meaningful names than that, but method names (in Swift 3 and later) should generally contain a verb that indicates what's being done.
class PostForData {
func performRequest(completion: @escaping (String?, Error?) -> Void) {
// don't try to build JSON manually; use `JSONSerialization` or `JSONEncoder` to build it
let dictionary = [
"name": "Ivan Bolgov",
"ss": "050-062-0769"
]
let jsonData = try! JSONEncoder().encode(dictionary)
// It's a bit weird to incorporate JSON in `x-www-form-urlencoded` request, but OK, I'll do that.
// But make sure to percent escape it.
let jsonString = String(data: jsonData, encoding: .utf8)!
.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
let body = "json=" + jsonString
let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body.data(using: .utf8)
// It's not required, but it's good practice to set `Content-Type` (to specify what you're sending)
// and `Accept` (to specify what you're expecting) headers.
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// now perform the prepared request
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
completion(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completion(responseString, nil)
}
task.resume()
}
}
There are also some modifications to that routine, specifically:
Don't ever use !
forced unwrapping when processing server responses. You have no control over whether the request succeeds or fails, and the forced unwrapping operator will crash your app. You should gracefully unwrap these optionals with guard let
or if let
patterns.
It's exceedingly unusual to use json=...
pattern where the ...
is the JSON string. One can infer from that you're preparing a application/x-www-form-urlencoded
request, and using $_POST
or $_REQUEST
to get the value associated with the json
key. Usually you'd either do true JSON request, or you'd do application/x-www-form-urlencoded
request, but not both. But to do both in one request is doubling the amount of work in both the client and server code. The above code follows the pattern in your original code snippet, but I'd suggest using one or the other, but not both.
Personally, I wouldn't have performRequest
return the JSON string. I'd suggest that it actually perform the parsing of the JSON. But, again, I left this as it was in your code snippet.
I notice that you used JSON in the form of "Ivan Bolgov": "050-062-0769"
. I would recommend not using "values" as the key of a JSON. The keys should be constants that are defined in advantage. So, for example, above I used "name": "Ivan Bolgov"
and "ss": "050-062-0769"
, where the server knows to look for keys called name
and ss
. Do whatever you want here, but your original JSON request seems to conflate keys (which are generally known in advance) and values (what values are associated with those keys).
If you're going to do x-www-form-urlencoded
request, you must percent encode the value supplied, like I have above. Notably, characters like the space characters, are not allowed in these sorts of requests, so you have to percent encode them. Needless to say, if you did a proper JSON request, none of this silliness would be required.
But note that, when percent encoding, don't be tempted to use the default .urlQueryAllowed
character set as it will allow certain characters to pass unescaped. So I define a .urlQueryValueAllowed
, which removes certain characters from the .urlQueryAllowed
character set (adapted from a pattern employed in Alamofire):
extension CharacterSet {
/// Returns the character set for characters allowed in the individual parameters within a query URL component.
///
/// The query component of a URL is the component immediately following a question mark (?).
/// For example, in the URL `http://www.example.com/index.php?key1=value1#jumpLink`, the query
/// component is `key1=value1`. The individual parameters of that query would be the key `key1`
/// and its associated value `value1`.
///
/// According to RFC 3986, the set of unreserved characters includes
///
/// `ALPHA / DIGIT / "-" / "." / "_" / "~"`
///
/// In section 3.4 of the RFC, it further recommends adding `/` and `?` to the list of unescaped characters
/// for the sake of compatibility with some erroneous implementations, so this routine also allows those
/// to pass unescaped.
static var urlQueryValueAllowed: CharacterSet = {
let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4
let subDelimitersToEncode = "!$&'()*+,;="
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
return allowed
}()
}
I would suggest changing your PHP to accept a JSON request, e.g.:
<?php
// read the raw post data
$handle = fopen("php://input", "rb");
$raw_post_data = '';
while (!feof($handle)) {
$raw_post_data .= fread($handle, 8192);
}
fclose($handle);
// decode the JSON into an associative array
$request = json_decode($raw_post_data, true);
// you can now access the associative array how ever you want
if ($request['foo'] == 'bar') {
$response['success'] = true;
$response['value'] = 'baz';
} else {
$response['success'] = false;
}
// I don't know what else you might want to do with `$request`, so I'll just throw
// the whole request as a value in my response with the key of `request`:
$raw_response = json_encode($response);
// specify headers
header("Content-Type: application/json");
header("Content-Length: " . strlen($raw_response));
// output response
echo $raw_response;
?>
Then you can simplify the building of the request, eliminating the need for all of that percent-encoding that we have to do with x-www-form-urlencoded
requests:
class PostForData {
func performRequest(completion: @escaping (String?, Error?) -> Void) {
// Build the json body
let dictionary = [
"name": "Ivan Bolgov",
"ss": "050-062-0769"
]
let data = try! JSONEncoder().encode(dictionary)
// build the request
let url = URL(string: "http://odnakrov.info/MyWebService/api/test.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = data
// It's not required, but it's good practice to set `Content-Type` (to specify what you're sending)
// and `Accept` (to specify what you're expecting) headers.
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
// now perform the prepared request
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
completion(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completion(responseString, nil)
}
task.resume()
}
}