Swift: completion handler after network request - json

I know, there are lots of similar threads regarding asynchronous functions and completion handlers in Swift... But after reading quite a lot of them, I still can't figure out how to process the responded data of a network request and save certain values within global variables.
Here is my code (yes, I'm a Swift rookie):
let equitySymbol = "AAPL"
var companyCountry = String()
func sendRequest(_ url: String, parameters: [String: String], completion: #escaping ([String: Any]?, Error?) -> Void) {
var components = URLComponents(string: url)!
components.queryItems = parameters.map { (key, value) in
URLQueryItem(name: key, value: value)
}
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
let request = URLRequest(url: components.url!)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode,
error == nil {
let responseObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
completion(responseObject, nil)
}
else {
completion(nil, error)
return
}
}
task.resume()
}
sendRequest("myUrl", parameters: ["token":"..."]) { (responseObject, error) -> Void in
if let responseObject = responseObject, error == nil {
companyCountry = responseObject["country"] as! String
}
else {
print(error ?? "Details Not Available")
return
}
}
print(companyCountry)
As you may assume, my variable "companyCountry" is still nil after my func "sendRequest" has been called (playground), when it should be "US" in this case. What's my mistake? Thanks a lot for your help!!!

A successful network request can take a long time. Usually it's much less than a second, but up to 60 seconds is possible. Even if it is just a millisecond, you are printing companyCountry a loooooong time before the callback function of the network request is called.
If you want to print companyCountry for debugging purposes, print it in the callback. If you want to keep it, store it into a permanent location in the callback.

Related

Could not cast value of type 'NSNull' to 'NSString' and then the app crashes

We are trying to make a function to get JSON from an API..We know that this is giving us NIL but we dont know why the error is occuring. The exact error message that we got was
[]
2020-08-01 16:29:26.501199-0400 HEFT[97766:2952325] [] nw_proxy_resolver_create_parsed_array [C1 proxy pac] Evaluation error: NSURLErrorDomain: -1003
Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8).
2020-08-01 16:29:26.670549-0400 HEFT[97766:2952139] Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8).
(lldb)
We have tried messing around the code to find a solution and we tried to use some other questions but none of them were related with what we were trying to achieve.
func getJson() {
if let url = URL(string: "https://api.weather.gov/alerts/active?area=GA") {
URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?) in
if error == nil {
if data != nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String:AnyObject] {
DispatchQueue.main.async {
//if let rawfeatures = json["features"] {
var rawfeatures = json["features"] as! [Dictionary< String, AnyObject>]
var keepgoingfeatures = rawfeatures.count
var FeatureIndex = 0
while keepgoingfeatures != 0{
let currentRawFeature = rawfeatures[FeatureIndex]
let currentRawFeatureProperties = currentRawFeature["properties"]
let currentFeature = Feature()
currentFeature.event = currentRawFeatureProperties!["event"] as! String
currentFeature.description = currentRawFeatureProperties!["description"] as! String
currentFeature.instructions = currentRawFeatureProperties!["instruction"] as! String
currentFeature.urgency = currentRawFeatureProperties!["urgency"] as! String
keepgoingfeatures -= 1
FeatureIndex += 1
}
}
}
}
} else {
print("We have an error")
}
}.resume()
}
}
Some of these alerts have null for instructions. I’d suggest defining your object to acknowledge that this field is optional, i.e. that it might not be present. E.g.
struct Feature {
let event: String
let description: String
let instruction: String?
let urgency: String
}
And, when parsing it, I might suggest getting rid of all of those forced unwrapping operators, e.g.
enum NetworkError: Error {
case unknownError(Data?, URLResponse?)
case invalidURL
}
#discardableResult
func getWeather(area: String, completion: #escaping (Result<[Feature], Error>) -> Void) -> URLSessionTask? {
// prepare request
var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
components.queryItems = [URLQueryItem(name: "area", value: area)]
var request = URLRequest(url: components.url!)
request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")
// perform request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let responseData = data,
let responseDictionary = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
let rawFeatures = responseDictionary["features"] as? [[String: Any]]
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknownError(data, response)))
}
return
}
let features = rawFeatures.compactMap { feature -> Feature? in
guard
let properties = feature["properties"] as? [String: Any],
let event = properties["event"] as? String,
let description = properties["description"] as? String,
let urgency = properties["urgency"] as? String
else {
print("required string absent!")
return nil
}
let instruction = properties["instruction"] as? String
return Feature(event: event, description: description, instruction: instruction, urgency: urgency)
}
DispatchQueue.main.async {
completion(.success(features))
}
}
task.resume()
return task
}
A few other observations:
I’ve removed all of the forced casting (the as!). You don’t want your app crashing if there was some problem in the server. For example, not infrequently I receive a 503 error. You don’t want to crash if the server is temporarily unavailable.
The docs say that you should set the User-Agent, so I’m doing that above. Obviously, set the domain and email string constants accordingly.
While you can build the URL manually, it’s safest to use URLComponents, as that will take care of any percent escaping that might be needed. It’s not needed here, but will be a useful pattern if you start to get into more complicated requests (e.g. need to specify a city name that has a space in it, such as “Los Angeles”).
I’d suggest the above completion handler pattern so that the caller can know when the request is done. So you might do something like:
getWeather(area: "GA") { result in
switch result {
case .failure(let error):
print(error)
// update UI accordingly
case .success(let features):
self.features = features // update your model object
self.tableView.reloadData() // update your UI (e.g. I'm assuming a table view, but do whatever is appropriate for your app
}
}
I’m returning the URLSessionTask in case you might want to cancel the request (e.g. the user dismisses the view in question), but I’ve marked it as a #discardableResult, so you don’t have to use that if you don’t want.
I’ve replaced the tower of if statements with a guard statement. It makes the code a little easier to follow and adopts an “early exit” pattern, where you can more easily tie the exit code with the failure (if any).
Personally, I’d suggest that you take this a step further and get out of manually parsing JSONSerialization results. It’s much easier to let JSONDecoder do all of that for you. For example:
struct ResponseObject: Decodable {
let features: [Feature]
}
struct Feature: Decodable {
let properties: FeatureProperties
}
struct FeatureProperties: Decodable {
let event: String?
let description: String
let instruction: String?
let urgency: String
}
enum NetworkError: Error {
case unknownError(Data?, URLResponse?)
case invalidURL
}
#discardableResult
func getWeather(area: String, completion: #escaping (Result<[FeatureProperties], Error>) -> Void) -> URLSessionTask? {
var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
components.queryItems = [URLQueryItem(name: "area", value: area)]
var request = URLRequest(url: components.url!)
request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let responseData = data
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknownError(data, response)))
}
return
}
do {
let responseObject = try JSONDecoder().decode(ResponseObject.self, from: responseData)
DispatchQueue.main.async {
completion(.success(responseObject.features.map { $0.properties }))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
task.resume()
return task
}
The short answer is because you force cast everything and assume a very specific format which the json doesnt have.
so at some point you read a value that just insnt there.
Concretely instruction.
as a working/non crashing fix (which I locally ran!):
let currentFeature = Feature()
currentFeature.event = currentRawFeatureProperties!["event"] as? String ?? ""
currentFeature.description = currentRawFeatureProperties!["description"] as? String ?? ""
currentFeature.instructions = currentRawFeatureProperties!["instruction"] as? String ?? ""
currentFeature.urgency = currentRawFeatureProperties!["urgency"] as? String ?? ""
I'd urge you to refactor your function broadly

-- function-call with completion URLRequest - JSON

i did read a lot about functions with completion-handler, but now i have a problem how to call this function (downloadJSON) in the correct way. Which parameters do i have to give in the function and handle the result-data (json) in my own class, where the function was called.
This is the code from David Tran. Hi makes wonderful tutorials, but in the code there is no call of this function.
let request: URLRequest
lazy var configuration: URLSessionConfiguration = URLSessionConfiguration.default
lazy var session: URLSession = URLSession(configuration: self.configuration)
typealias JSONHandler = (JSON?, HTTPURLResponse?, Error?) -> Void
func downloadJSON(completion: #escaping JSONHandler)
{
let dataTask = session.dataTask(with: self.request) { (data, response, error) in
// OFF THE MAIN THREAD
// Error: missing http response
guard let httpResponse = response as? HTTPURLResponse else {
let userInfo = [NSLocalizedDescriptionKey : NSLocalizedString("Missing HTTP Response", comment: "")]
let error = NSError(domain: DANetworkingErrorDomain, code: MissingHTTPResponseError, userInfo: userInfo)
completion(nil, nil, error as Error)
return
}
if data == nil {
if let error = error {
completion(nil, httpResponse, error)
}
} else {
switch httpResponse.statusCode {
case 200:
// OK parse JSON into Foundation objects (array, dictionary..)
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String : Any]
completion(json, httpResponse, nil)
} catch let error as NSError {
completion(nil, httpResponse, error)
}
default:
print("Received HTTP response code: \(httpResponse.statusCode) - was not handled in NetworkProcessing.swift")
}
}
}
dataTask.resume()
}
Let Xcode help you. Type downlo and press return. Xcode completes the function
Press return again and you get the parameters
You have to replace the placeholders with parameter names for example
downloadJSON { (json, response, error) in
if let error = error {
print(error)
} else if let json = json {
print(json)
}
}
Note:
There is a fatal type mismatch error in your code: The result of the JSONSerialization line is [String:Any] but the first parameter of the completion handler is JSON

Clear URL cached

I have a code below which works fine except pull to refresh. It returns cached version of .json. If I use different URL function it reloads new .json on the fly, but if I want to perform pull to refresh with same URL it serving cached version of it.
Thank you
static func loadDataFromURL(url: URL,completion: #escaping (_ data: Data?, _ error: Error?) -> Void) {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.allowsCellularAccess = true
sessionConfig.timeoutIntervalForRequest = 15
sessionConfig.timeoutIntervalForResource = 30
sessionConfig.httpMaximumConnectionsPerHost = 1
let session = URLSession(configuration: sessionConfig)
// Use URLSession to get data from an NSURL
let loadDataTask = session.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(nil, error!)
if kDebugLog { print("API ERROR: \(error!)") }
return
}
guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
completion(nil, nil)
if kDebugLog { print("API: HTTP status code has unexpected value") }
return
}
guard let data = data else {
completion(nil, nil)
if kDebugLog { print("API: No data received") }
return
}
// Success, return data
completion(data, nil)
}
loadDataTask.resume()
}
You should set the session.configuration with a URLSessionConfiguration object which config with the cache policy according to your need which I think you haven't set on above code.

Getting a JSON from the Task

So I know how to parse JSON and retrieve a JSON from a URLRequest. What my objective is to remove this JSON file so I can manipulate it into different UIViewControllers. I have seen some stuff with completion handlers but I run into some issues, and I haven't fully understand. I feel like there is a simple answer, I am just being dumb.
How can I take this JSON outside the task and use it in other Swift files as a variable?
class ShuttleJson: UIViewController{
func getGenres(completionHandler: #escaping (_ genres: [String: Any]) -> ()) {
let urlstring = "_________"
let urlrequest = URLRequest(url: URL(string: urlstring)!)
let config = URLSessionConfiguration.default
let sessions = URLSession(configuration: config)
// request part
let task = sessions.dataTask(with: urlrequest) { (data, response, error) in
guard error == nil else {
print("error getting data")
print(error!)
return
}
guard let responseData = data else {
print("error, did not receive data")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]{
//Something should happen here
}
print("no json sucks")
}
catch{
print("nah")
}
}
task.resume()
}
}
First of all remove the underscore and the parameter label from the completion handler. Both are useless
func getGenres(completionHandler: #escaping ([String: Any]) -> ()) {
Then replace the line
//Something should happen here
with
completionHandler(json)
and call the function
getGenres() { json in
print(json)
}
Notes:
The check guard let responseData = data else is redundant and it will never fail. If error is nil then data is guaranteed to have a value.
You should print the caught error rather than a meaningless literal string.

Ambiguous reference to member `jsonObject(with:options:)` when trying to get data from json

I'm new to Swift and while making one of the tutorials (fairly old) which involves getting credentials from a server through php which returns a JSON, but I'm stuck with the error Ambiguous reference to member jsonObject(with:options:) in the json var, I've searched and trying applying the different solutions but to no avail. :(
Thank you for your time and help.
here is my code:
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
if((userEmail?.isEmpty)! || (userPassword?.isEmpty)!) {
displayMyAlertMessage(userMessage: "All Fields are required.")
return;
}
let myUrl = URL(string: "/UserLogin.php");
var request = URLRequest(url:myUrl!);
request.httpMethod = "POST";
let postString = "email\(userEmail)&password=\(userPassword)";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, URLResponse, error in
if error != nil {
//print = ("error=\(error)");
return
}
var err: Error?
var json = JSONSerialization.jsonObject(with: data, options: .mutableContainers, error: &err) as? NSDictionary
if let parseJSON = json {
var resultValue:String = parseJSON["status"] as String!;
print("result: \(resultValue)")
if(resultValue == "Success") {
//Login Succesful
UserDefaults.standard.set(true, forKey:"isUserLoggedIn");
UserDefaults.standard.synchronize();
self.dismiss(animated: true, completion: nil);
}
}
}
task.resume()
There are two major issues:
The actual error occurs because the response parameter in the completion block is wrong. Rather than the type URLResponse it must be a parameter label / variable.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
Since you are using Swift 3 there is no error parameter in jsonObject(with. The method does throw, you need a do - catch block. And – as always – the option .mutableContainers is completely useless in Swift. Omit the parameter.
do {
if let parseJSON = try JSONSerialization.jsonObject(with: data) as? [String:Any],
let resultValue = parseJSON["status"] as? String {
print("result: ", resultValue)
if resultValue == "Success" {
//Login Succesful
UserDefaults.standard.set(true, forKey:"isUserLoggedIn")
self.dismiss(animated: true, completion: nil)
}
}
} catch {
print(error)
}
Some other notes:
To check the text fields safely use optional binding
guard let userEmail = userEmailTextField.text, !userEmail.isEmpty, let userPassword = userPasswordTextField.text, !userPassword.isEmpty else {
displayMyAlertMessage(userMessage: "All Fields are required.")
return
}
Declare Swift constants always as let (for example resultValue)
Do not use NSArray / NSDictionary in Swift. Use native types.
Do not use parentheses around if conditions and trailing semicolons. They are not needed in Swift.
UserDefaults.standard.synchronize() is not needed either.
String.Encoding.utf8 can be reduced to just .utf8.