This question already has an answer here:
JSON parsing swift, array has no value outside NSURLSession
(1 answer)
Closed 6 years ago.
I collected JSON data from an API and am able to initialize my array (class instance variable), but after the block, data is destroyed from array. How can I return data from the task block that can be used to initialize my array?
override func viewDidLoad() {
// var movies=[Movie]()
let requestURL: NSURL = NSURL(string: "https://yts.ag/api/v2/list_movies.json")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("File downloaded successfully.")
do {
let jsonYts = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
let jsonDataTag = jsonYts["data"]
let jsonMovie = jsonDataTag!!["movies"]!
let movies = [Movie].fromJSONArray(jsonMovie as! [JSON])
// self.moviesList = movies as! [Movie] // this is my array. I want to add data to it
for data in self.moviesList {
// print(data.title)
}
} catch {
}
}
}
task.resume()
}
Asynchronous methods don't return to the invoking function because they are not called directly in the first place. They are placed on a task queue to be called later (on a different thread!). It's the hardest thing for those new to asynchronous programming to master. Your async method must mutate a class variable. So you'll have:
class MyContainer: UIViewController {
var myInstanceData: [SomeObject] // In your case moviesList
func startAsyncRequest() { // In your case viewDidLoad()
// In your case NSMutableURLRequest
doSomethingAsynchronous(callback: { [weak self] (result) in
// in your case JSON parsing
self?.myInstanceData = parseResult(result) // share it back to the containing class
// This method returns nothing; it is not called until well
// after startAsyncRequest() has returned
})
}
}
Once the callback finishes, myInstanceData is available to all the other methods in the class. Does that make sense as a general pattern?
After that you have to learn this material:
Swift performSegueWithIdentifier not working
To get the result back into the UI from the background thread on which your callback runs.
Addendum
Taking a second look after editing your question for clarity, it seems you may already know this, but you've commented out the correct line, and if you put it back in, things just work, that is, the movies won't be destroyed but will be recorded in the class property moviesList.
// self.moviesList = movies as! [Movie] // this is my array. I want to add data to it
Why not just re-enable it? Perhaps the real problem is: Do you mean to append instead of overwrite, perhaps? Then the answer may be as simple as:
self.moviesList += movies as! [Movie]
You can just use "SwiftyJson" to handle the response from the API if it returns JSON data; if it uses XML then use "SWXMLHash". They are both pods (libraries) designed to handle the response from their respective formats.
Installation help for SwiftyJson. If you still have problems comment below.
At this point you can just import SwiftyJson and use it.
Let response be any object, and suppose your JSON data is in this format:
{"text": {
"data": "Click Here",
"size": 36,
"style": "bold",
"name": "text1",
"hOffset": 250,
"vOffset": 100,
"alignment": "center"
}
}
Then you can just write code like this
func response(data){
let dt = JSON(data : data) // the second data is the response from api
response = dt.object // for accessing the values as object from json format
let style = response["text"]["data"]!! as? String // style = "bold"
let n = response["text"]["name"]!! as? String // n = "text1"
let size = response["text"]["hOffset"]!! as? Int // size = 250
}
// Done !! Easy.. if you guys need help or snapshots for the same do comment..
Related
I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.
I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)
I am making an API call for muscles relating to an exercise, the call looks like this:
func loadPrimaryMuscleGroups(primaryMuscleIDs: [Int]) {
print(primaryMuscleIDs)
let url = "https://wger.de/api/v2/muscle"
Alamofire.request(url).responseJSON { response in
let jsonData = JSON(response.result.value!)
if let resData = jsonData["results"].arrayObject {
let resData1 = resData as! [[String:AnyObject]]
if resData1.count == 0 {
print("no primary muscle groups")
self.musclesLabel.isHidden = true
} else {
print("primary muscles used for this exercise are")
print(resData)
self.getMuscleData(muscleUrl: resData1[0]["name"] as! String)
}
}
}
}
This returns me a whole list of all the muscles available, I need it to just return the muscles the exercise requires. This is presented in exercises as an array of muscle id's, which I feed in via the viewDidLoad below
self.loadPrimaryMuscleGroups(primaryMuscleIDs: (exercise?.muscles)!)
So I am feeding the exercises muscle array into the func as an [Int] but at this point im stumped on how to filter the request so that the resulting muscle data are only the ones needed for the exercise.
I was thinking it would be something like using primaryMuscleIDs to filter the id property of a muscle in the jsonData response, but im not sure how to go about that?
Thanks for any clarify here, hopefully I have explained it clearly enough to come across well
You'd want to do something like this in your else block:
var filteredArray = resData1.filter { item in
//I'm not exactly sure of the structure of your json object,
//but you'll need to get the id of the item as an Int
if let itemId = item["id"] as? Int {
return primaryMuscleIDs.contains(itemId)
}
return false
}
//Here with the filtered array
And since the Alamofire request is asynchronous, you won't be able to return a result synchronously. Your method will need to take a callback that gets executed in the Alamofire response callback with the filtered response.
Something like (but hopefully with something more descriptive than an array of Any:
func loadPrimaryMuscleGroups(primaryMuscleIDs: [Int], callback: ([Any]) -> ()) {
//...
Alamofire.request(url).responseJSON { response in
//...
//once you get the filtered response:
callback(filteredArray)
}
}
Finally, check out How to parse JSON response from Alamofire API in Swift? for the proper way to handle a JSON response from Alamofire.
I just wrote some Swift code for accessing Riot API, with Alamofire and SwiftyJSON.
I wrote a function func getIDbyName(SummName: String) -> String to get the summoner id.
As you can see from the code below, I am assigning the id to self.SummID.
After executing the function, I am able to println the correct id, for example "1234567". However, the return self.SummID returns "0", the same as assigned in the beginning.
I tried to mess with the code, but I simply cannot get the correct value of self.SummID outside of the Alamofire.request closure. It always remain "0" anywhere outside.
I think it has something to do with the scope of the variable. Does anyone know what is going on here?
import Foundation
import Alamofire
import SwiftyJSON
class SummInfo {
var SummName = "ThreeSmokingGuns"
var SummID = "0"
var SummChamp = "akali"
var SummS1 = "flash"
var SummS2 = "ignite"
var SummRank = "Unranked"
var SummWR = "-" //summoner's winrate
let api_key = "key"
let URLinsert = "?api_key="
init(SummName: String, SummChamp: String, SummS1: String, SummS2: String, SummRank: String, SummWR: String) {
self.SummName = SummName
self.SummChamp = SummChamp
self.SummS1 = SummS1
self.SummS2 = SummS2
self.SummRank = SummRank
self.SummWR = SummWR
}
init(SummName: String) {
self.SummName = SummName
}
func getIDbyName(SummName: String) -> String
{
let SummURL = "https://na.api.pvp.net/api/lol/na/v1.4/summoner/by-name/"
var fullURL = "\(SummURL)\(SummName)\(URLinsert)\(api_key)"
Alamofire.request(.GET, fullURL)
.responseJSON { (request, response, data, error) in
if let anError = error
{
// got an error in getting the data, need to handle it
println("error calling GET on /posts/1")
println(error)
}
else if let data: AnyObject = data // hate this but responseJSON gives us AnyObject? while JSON() expects AnyObject
// JSON(data!) will crash if we get back empty data, so we keep the one ugly unwrapping line
{
// handle the results as JSON, without a bunch of nested if loops
let post = JSON(data)
self.tempJ = post
var key = post.dictionaryValue.keys.array //not necessary
var key2 = post[SummName.lowercaseString].dictionaryValue.keys.array
self.SummID = post[key[0],key2[2]].stringValue //[profileIconId, revisionDate, id, summonerLevel, name]
//test console output
println("The post is: \(post.description)")
println(SummName.lowercaseString)
println(key)
println(key2)
println(self.SummID)
}
}
return self.SummID
}
}
The reason is that
Alamofire.request(.GET, fullURL)
.responseJSON
is an asynchronous call. This means that the call to getIDbyName will immediately return without waiting the responseJSON to finish. This is the exact reason why you get a the '0' value for ID that you have set initially.
Having said that, the solution is to have a call back closure in the getIDbyName method:
func getIDbyName(SummName: String, callback: (id:String?) ->() ) -> ()
{
let SummURL = "https://na.api.pvp.net/api/lol/na/v1.4/summoner/by-name/"
var fullURL = "\(SummURL)\(SummName)\(URLinsert)\(api_key)"
Alamofire.request(.GET, fullURL)
.responseJSON { (request, response, data, error) in
if let anError = error
{
// got an error in getting the data, need to handle it
println("error calling GET on /posts/1")
println(error)
//Call back closure with nil value
callback(nil) //Can additionally think of passing actual error also here
}
else if let data: AnyObject = data // hate this but responseJSON gives us AnyObject? while JSON() expects AnyObject
// JSON(data!) will crash if we get back empty data, so we keep the one ugly unwrapping line
{
// handle the results as JSON, without a bunch of nested if loops
let post = JSON(data)
self.tempJ = post
var key = post.dictionaryValue.keys.array //not necessary
var key2 = post[SummName.lowercaseString].dictionaryValue.keys.array
self.SummID = post[key[0],key2[2]].stringValue //[profileIconId, revisionDate, id, summonerLevel, name]
//test console output
println("The post is: \(post.description)")
println(SummName.lowercaseString)
println(key)
println(key2)
println(self.SummID)
//Pass the actual ID got.
callback(self.SummID)
}
}
return self.SummID
}
And clients should always use this API to fetch the latest ID, and can refer the attribute directly to get whatever is cached so far in SummID member.
Here is how to call this method-
object.getIDbyName(sumName){ (idString :String) in
//Do whatever with the idString
}
I tried a lot of methods from stackoverflow and from googling
but I got no luck of reaching what I need.
simply I want to store the response from api request to some variable
so that I can use it freely.
here is the code
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var responseLabel: UILabel!
let apiUrlStr = "http://jsonplaceholder.typicode.com/posts"
var resData = NSArray()
override func viewDidLoad() {
super.viewDidLoad()
request(.GET, apiUrlStr, encoding: .JSON).responseJSON { (_, _, JSON, _) in
let arr = JSON as NSArray
let user = arr[0] as NSDictionary
self.responseLabel.text = user.objectForKey("title") as? String
self.resData = JSON as NSArray //i cant get this from outside
}
println(resData) //prints nothing but empty parentheses while inside the request it prints the data right
}
}
because I have multiple classes and every class need to handle the response in a different way I want to create an api class with a response method that will return the response.
something like this maybe:
class api{
func req(url: String, params: NSDictionary){
//method goes here
}
func res() -> NSDictionary{
//method goes here
var response = NSDictionary()
return response
}
}
then use it like this in viewdidload
let params = ["aaa","bbb"]
let api = api()
api.req(apiUrlStr, params)
let res = api.res()
***by the way the request method I'm using right now is from Alamofire.swift and I dont mind using another method
here is some of the posts and sites I tried to follow without a luck
proper-way-to-pull-json-api-resoponse
link2
all lead to the same result response only from inside the method.
The problem is that the call to the network (looks like you're using alamofire) is asynchronous. So the network request is started and before it completes, your
println(resData)
line executes. At this point the response from the network hasn't returned so resData is still an empty array.
Try doing this instead:
Add a new function
func handleResponse(resData: NSArray) {
self.resData = resData
println(resData)
}
Then in the response code replace
self.resData = JSON as NSArray
with
let resData = JSON as NSArray
self.handleResponse(resData)
Assuming you need more than just a println statement to change when the data is loaded, you will also need to inform things like table views or other UI components to reload their data source in this method.