iosswiftsession

Returning data from URLSession and saving in property variable


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

Solution

  • 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:

    1. 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.

    2. 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.

    3. 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.

    4. 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).

    5. 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()
        }
    }