2

I have the following code:

    func mapView(_ mapView: MGLMapView, annotation: MGLAnnotation, calloutAccessoryControlTapped control: UIControl) {
    var bus = [String]()
    let headers = [
      "content-type": "application/x-www-form-urlencoded",
      "cache-control": "no-cache",
      "postman-token": "23cb4108-e24b-adab-b979-e37fd8f78622"
    ]

    let postData = NSMutableData(data: "bus_stop=Science Hill".data(using: String.Encoding.utf8)!)
    let request = NSMutableURLRequest(url: NSURL(string: "https://ucsc-bts3.soe.ucsc.edu/bus_stops/inner_eta.php?%22bus_stop%22=%22Science%20Hill%22")! as URL,
                                      cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                                        timeoutInterval: 10.0)
    request.httpMethod = "POST"
    request.allHTTPHeaderFields = headers
    request.httpBody = postData as Data

    let session = URLSession.shared
    let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
        if (error != nil) {
            print(error!)
      } else {
            _ = response as? HTTPURLResponse
      }
        do {
            let jsonObject = try JSONSerialization.jsonObject(with: data!)
            guard let jsonArray = jsonObject as? [String: Any] else{
                print("JsonSerialization Failed")
                return
                }
            if let etaTableRows = jsonArray["rows"] as? NSArray{
                for etaData in etaTableRows{
                    let etaDictionary = etaData as? NSDictionary
                    bus.append(etaDictionary!["bus_type"] as! String)
                }
            }
        } catch {
            print("JSONSerialization error:", error)
        }
    })
    dataTask.resume()
    print(bus) //bus array should be updated here
    mapView.deselectAnnotation(annotation, animated: false)
    let schedule = ScheduleVC()
    schedule.data.append(annotation.title!! + " ETAs")
    self.present(schedule, animated: true, completion: nil)
}

It appears that print(bus) is being run before the http response is received and bus array is filled. My goal would be to fill the bus array with the http response data, then print it. I'm not sure how to accomplish this.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
rjmitty1000
  • 101
  • 1
  • 6

3 Answers3

2

A few observations:

  1. You should pull this network code into its own method.
  2. You are specifying the request in both the body and the URL. The body is sufficient. Remove it from the URL.
  3. You should excise the NSMutableData (use Data), NSMutableURLRequest (use URLRequest), NSURL (use URL), NSArray, NSDictionary, etc. It’s generally best to stay within the Swift types where possible.
  4. Rather than manually iterating through JSONSerialization results, use JSONDecoder.
  5. You also need to percent encode the body of your request, as bus_stop=Science Hill is not valid in a x-www-form-urlrequest. See https://stackoverflow.com/a/26365148/1271826.
  6. You said:

    My goal would be to fill the bus array with the http response data, then print it

    You need to move your code dependent upon the request inside the completion handler closure.

Thus:

func fetchBuses(completion: @escaping (Result<[Bus], Error>) -> Void) {
    let headers = [
        "content-type": "application/x-www-form-urlencoded",
        "cache-control": "no-cache",
        "postman-token": "23cb4108-e24b-adab-b979-e37fd8f78622"
    ]

    let url = URL(string: "https://ucsc-bts3.soe.ucsc.edu/bus_stops/inner_eta.php")!
    var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10)

    request.httpBody = ["bus_stop": "Science Hill"].percentEncoded()
    request.httpMethod = "POST"
    request.allHTTPHeaderFields = headers

    let session = URLSession.shared
    let dataTask = session.dataTask(with: request) { data, response, error in
        guard
            error == nil,
            let responseData = data,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode
        else {
            completion(.failure(error ?? BusError.unknown(data, response)))
            return
        }

        do {
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let responseObject = try decoder.decode(ResponseObject.self, from: responseData)
            completion(.success(responseObject.rows))
        } catch let jsonError {
            completion(.failure(jsonError))
        }
    }
    dataTask.resume()
}

And

func mapView(_ mapView: MGLMapView, annotation: MGLAnnotation, calloutAccessoryControlTapped control: UIControl) {
    fetchBuses { result in
        switch result {
        case .failure(let error):
            print(error)

        case .success(let buses):
            print(buses)

            DispatchQueue.main.async {
                mapView.deselectAnnotation(annotation, animated: false)
                let schedule = ScheduleVC()
                schedule.data.append(annotation.title!! + " ETAs")
                self.present(schedule, animated: true, completion: nil)
            }
        }
    }
}

Where

enum BusError: Error {
    case unknown(Data?, URLResponse?)
}

struct Bus: Decodable {
    let busId: Int
    let busType: String
    let nextBusStop: String
    let timeAway: Int
}

struct ResponseObject: Decodable {
    let rows: [Bus]
}

And

extension Dictionary {
    func percentEncoded() -> Data? {
        return map { key, value in
            let escapedKey = "\(key)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
            let escapedValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) ?? ""
            return escapedKey + "=" + escapedValue
        }
        .joined(separator: "&")
        .data(using: .utf8)
    }
}

extension CharacterSet {
    static let 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
    }()
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
1

I would recommend you to read, what is asynchronous.

To understand the programmer’s misconception, let’s look at how he thinks the code goes. He thinks the code runs in this order:

http://www.programmingios.net/what-asynchronous-means/

You need to pass callback, and call callback inside asynchronous call

Example:

class func getBusArray(completionHandler: (bus: NSArray) -> ()) {
    ...
    let task = session.dataTaskWithURL(url) {
        data, response, error in
        ...
        resultsArray = results
        completionHandler(bus: resultsArray)
    }
    ...
    task.resume()
}

You can call like,

override func viewDidLoad() {
    MyModel.getBusArray {
        bus in
        println("Response: \(bus)")     
    }
}
Harish
  • 2,496
  • 4
  • 24
  • 48
0

In this particular case the solution is quite easy: Run the code after receiving the data inside the closure.

And this is Swift: Don't use NSURL, NSMutableData, NSMutableURLRequest, NSArray and NSDictionary

func mapView(_ mapView: MGLMapView, annotation: MGLAnnotation, calloutAccessoryControlTapped control: UIControl) {
    var bus = [String]()
    let headers = [
      "content-type": "application/x-www-form-urlencoded",
      "cache-control": "no-cache",
      "postman-token": "23cb4108-e24b-adab-b979-e37fd8f78622"
    ]

    let postData = Data("bus_stop=Science%20Hill".utf8)
    var request = URLRequest(url: URL(string: "https://ucsc-bts3.soe.ucsc.edu/bus_stops/inner_eta.php?%22bus_stop%22=%22Science%20Hill%22")!,
                                      cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
                                        timeoutInterval: 10.0)
    request.httpMethod = "POST"
    request.allHTTPHeaderFields = headers
    request.httpBody = postData

    let session = URLSession.shared
    let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) in
        if let error = error {
            print(error); return
        }
        do {
           let jsonObject = try JSONSerialization.jsonObject(with: data!)
           guard let jsonArray = jsonObject as? [String: Any] else {
                print("JsonSerialization Failed")
                return
           }
           if let etaTableRows = jsonArray["rows"] as? [[String:Any]] {
                for etaData in etaTableRows {
                    bus.append(etaData["bus_type"] as! String)
                }
                print(bus)
                self.mapView.deselectAnnotation(annotation, animated: false)
                let schedule = ScheduleVC()
                schedule.data.append(annotation.title!! + " ETAs")
                self.present(schedule, animated: true, completion: nil)
          }
       } catch {
           print("JSONSerialization error:", error)
       }
    })
    dataTask.resume()
}
vadian
  • 274,689
  • 30
  • 353
  • 361