Reputation: 191
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
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
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
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