William
William

Reputation: 191

Passing object from URLSession to ViewController

I'm trying to pass an Object I've created called detail from a swift file named RestManager.swift into a ViewController. The object contains all the elements but when I call it in my view controller it is empty. From what I've gathered online it may have something to do with URLSession working on a background thread

My RestManager.swift looks like this.

class RestManager {


func reqDetails(id: Int) {
    // Create URL
    let id = String(id)
    let url = "https://website.example.com/"
    let url = URL(string: url + id)!

    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

        if error != nil
        {
            print ("ERROR")
        }

        else
        {
            if let content = data
            {

                    let jsonData = JSON(data: content)

                    let id = jsonData["id"].intValue
                    let name = jsonData["title"]["rendered"].string!
                    let link = jsonData["link"].url!
                    let content = jsonData["content"]["rendered"].string!


                    // Create Object
                    let detail = Detail(id: id, name: name, content: content, thumbnailUrl: link)
                    self.details.append(detail)

            }
        }
    }
    task.resume()
}
}

My View Controller looks like so:

class DetailViewController: UIViewController {

var ListingID = Int()
let restManager = RestManager()

@IBOutlet weak var ContentLabel: UILabel!

override func viewDidLoad() {
    super.viewDidLoad()
    restManager.reqDetails(id: ListingID)
    ContentLabel.text? = restManager.details[0].name // After running the app this index value is out of range.


}

..

}

Upvotes: 2

Views: 1391

Answers (3)

Lars Christoffersen
Lars Christoffersen

Reputation: 1739

Hi I think you have just encountered the typical pitfall of asynchronous programming. What you do is asking for the return value of the task, before it ever has come back from the URLSession. You can solve it by making a completion handler yourself like this example, where I get some weather data from Darksky. Note you dont even need to make a class for this, just a function.

PS note i use Alamofire, SwiftyJSON and Gloss which greatly reduces complexity using REST interfaces. And this is Swift 3!

import Alamofire
import SwiftyJSON
import Gloss


typealias DarkSkyWeatherForecast = ( _ json : Gloss.JSON?,  _ error : Error?) -> Void


func getWeatherForcast( latitude:Double, longitude:Double,  completionHandler:@escaping DarkSkyWeatherForecast) -> Void {

    let urlString =  "https://api.darksky.net/forecast/"+darkSkyKey+"/"+String(latitude)+","+String(longitude)+"?units=si"

    Alamofire.request(urlString).responseJSON { (response) in
        if let resp = response.result.value {
            let json = JSON(resp)
            let glossJson = json.dictionaryObject
            completionHandler( glossJson, nil)
        }else{
            completionHandler( nil, response.error)
        }
    }
}

And the call the function like this:

getWeatherForcast(latitude: lat, longitude: lon) { (jsonArg, error) in
            if error == nil{
                guard let weather = DarkSkyWeather(json: jsonArg!) else {
                    self.effectView.removeFromSuperview()
                    return
                }
                if let ambTemp = weather.currently?.temperature, let windspd = weather.currently?.windSpeed, let windDir = weather.currently?.windBearing{
                    self.effectView.removeFromSuperview()
                    self.ambTemperature.text = String(ambTemp)
                    self.windSpeed.text = String( windspd )
                    self.windDirection.text = String( windDir )
                    self.getSpeed()
                }
            }else{

               self.effectView.removeFromSuperview()
                let alert = UIAlertController(title: "Error", message: "Could not get weather forecast", preferredStyle: .alert)
                let okAction = UIAlertAction(title: "OK", style: .default, handler: { (action) in
                    self.dismiss(animated: true, completion: nil)
                })
                alert.addAction(okAction)
                self.present(alert, animated: true, completion: nil)
            }
        }

Note, I only fill the textfields once the completion handler is finished and actually returns either some data or an error. Disregard the "effectView" stuff, that is for a special curser wait spinner :-)

And also to know these structs which is mapped to the json data might be helpfull:

//common struct for weather data
public struct WeatherDataStruct : Decodable{
    let time : Int?
    let summary : String?
    let icon : String?
    let precipIntensity : Double?
    let precipProbability : Double?
    let precipType : String?
    let temperature : Double?
    let apparentTemperature : Double?
    let dewPoint : Double?
    let humidity: Double?
    let windSpeed : Double?
    let windBearing : Int?
    let visibility : Double?
    let cloudCover : Double?
    let pressure : Double?
    let ozone : Double?

    public init?( json: JSON){
        self.time = "time" <~~ json
        self.summary = "summary" <~~ json
        self.icon = "icon" <~~ json
        self.precipIntensity = "precipIntensity" <~~ json
        self.precipProbability = "precipProbability" <~~ json
        self.precipType = "precipType" <~~ json
        self.temperature = "temperature" <~~ json
        self.apparentTemperature = "apparantTemperature" <~~ json
        self.dewPoint = "dewPoint" <~~ json
        self.humidity = "humidity" <~~ json
        self.windSpeed = "windSpeed" <~~ json
        self.windBearing = "windBearing" <~~ json
        self.visibility = "visibility" <~~ json
        self.cloudCover = "cloudCover" <~~ json
        self.pressure = "pressure" <~~ json
        self.ozone = "ozone" <~~ json
    }
}

//hourly weather struct
public struct HourlyStruct : Decodable{
    let summary : String?
    let icon : String?
    let data : [WeatherDataStruct]?

    public init?(json: JSON) {
        self.summary = "summary" <~~ json
        self.icon = "icon" <~~ json
        self.data = "data" <~~ json
    }
}

//total struct for the whole json answer from darksky weather
public struct DarkSkyWeather : Decodable{
    let latitude : Double?
    let longitude : Double?
    let timezone : String?
    let offset : Int?
    let currently : WeatherDataStruct?
    let hourly : HourlyStruct?

    public init?(json: JSON) {
        self.latitude = "latitude" <~~ json
        self.longitude = "longitude" <~~ json
        self.timezone = "timezone" <~~ json
        self.offset = "offset" <~~ json
        self.currently = "currently" <~~ json
        self.hourly = "hourly" <~~ json
    }
}

Upvotes: 0

Irshad Ahmad
Irshad Ahmad

Reputation: 1383

Use the closer in function to pass data like this

'

func reqDetails(id: Int,completionHandler:@escaping (_ detilObject:Detail)->Void) {
    // Create URL
    let id = String(id)
    let url = "https://website.example.com/"
    let url = URL(string: url + id)!

    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

        if error != nil
        {
            print ("ERROR")
        }

        else
        {
            if let content = data
            {

                    let jsonData = JSON(data: content)

                    let id = jsonData["id"].intValue
                    let name = jsonData["title"]["rendered"].string!
                    let link = jsonData["link"].url!
                    let content = jsonData["content"]["rendered"].string!


                    // Create Object
                    let detail = Detail(id: id, name: name, content: content, thumbnailUrl: link)
                    self.details.append(detail)
                    completionHandler(self.details)

            }
        }
    }
    task.resume()
}

'

and call your function like this. '

restManager.reqDetails(id: ListingID , completionHandler: { (detail) in
                // here is your detail object
            })

'

Upvotes: 3

Joshua Nozzi
Joshua Nozzi

Reputation: 61228

You're correct that your problem is due to an attempt to read the response immediately (as it's highly unlikely to be ready immediately). You need a way to notify your controller (on the main thread/queue!) when your data are ready (or an error occurred).

A simple if inelegant solution would be to add a weak var controller: DetailViewController to your RestManager (weak so it doesn't cause a retain cycle) that's passed on init ( RestManager(controller: self) ) and use that reference in the URL task closure to tell the controller it's ready or errored out.

You should probably use notifications or the delegate pattern though to avoid tight coupling of the RestManager to DetailViewController.

Upvotes: 0

Related Questions