How to access dictionary data outside its method on Swift? - ios

I am creating a weather app with Swift. So I have retrieved the JSON data and stored it in a dictionary:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
///////getting URL:
let mainAddress = NSURL(string: "https://...") //for NY
//Now, getting the data syncronously by creating a session object::
let sharedSession = NSURLSession.sharedSession()
let downloadTask: NSURLSessionDownloadTask =
sharedSession.downloadTaskWithURL(mainAddress!, completionHandler: {
(location:NSURL!, response:NSURLResponse!, error: NSError!) -> Void in
//using the if statement to avoid crashing when the URL is wrong.
if error == nil {
//Now, creating a dataObject for the task:
let dataObject = NSData(contentsOfURL: location)
//getting a formated dictionary of the data from URL:
let weatherDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataObject!, options: nil, error: nil) as NSDictionary //added '!' to NSdata for now
}
})
downloadTask.resume()
I have used a Struct, in a difirent file, in order to organize and initialize the dictionary's data:
import Foundation
import UIKit
import WatchKit
//created the struct just to better organize the data. In the future, if the API keys change, it would be easier to ajust the code, rather than if the data was directly read from the API onto the graph.
struct hourlyData {
///declaring only keys that have Integers as value.
var daylyPop0 : Int
var daylyPop1 : Int
var daylyPop2 : Int
var daylyPop3 : Int
var daylyPop4 : Int
var summaryNowDay : String
var summaryNowNight : String
var iconNow : String
var currentTime: String?
//Initializing the values here. With optional properties:
init(weatherDictionary:NSDictionary){
daylyPop0 = weatherDictionary["daily0_pop"] as Int
daylyPop1 = weatherDictionary["daily1_pop"] as Int
daylyPop2 = weatherDictionary["daily4_pop"] as Int
daylyPop3 = weatherDictionary["daily3_pop"] as Int
daylyPop4 = weatherDictionary["daily2_pop"] as Int
}
Now, I am implementing a chart for it. So I need to access the values from the dictionary in order to implement them on the chart. However, I've been unsuccessfull after many attemps.
The code lets me access the hourlyData struct, but not the weatherDictionary, since it was declared inside the session declaration.
Anyone knows an effective way to do it?
Any tips would be appreciated, thanks.

Try this for asynchronous request:
var request = NSURLRequest(URL: url)
var dict = NSDictionary()
var yourSavedData = hourlyData()
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue()) { (response, data, error) -> Void in
if data == nil
{
println("Error in connection")
return
}
var error = NSErrorPointer()
dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: error) as NSDictionary
if error != nil
{
println(error.debugDescription)
return
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
if let yourDict = dict as? NSDictionary
{
yourSavedData = hourlyData(yourDict!)
}
}
})
Not tested, but should work.

You need to use if let to parse dictionary.

Alright, so after the answers you guys posted I've updated the code a little. This is how the viewDidLoad looks like:
override func viewDidLoad() {
super.viewDidLoad()
///////getting URL:
let url = NSURL(string: "http://..........") //for NY
var request = NSURLRequest(URL: url!)
var dict = NSDictionary()
var yourSavedData = hourlyData(weatherDictionary: NSDictionary())
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue()) { (response, data, error) -> Void in
if data == nil
{
println("Error in connection")
return
}
var error = NSErrorPointer()
dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: error) as! NSDictionary
if error != nil
{
println(error.debugDescription)
return
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
if let yourDict = dict as? NSDictionary
{
yourSavedData = hourlyData(weatherDictionary: yourDict)
}
}
})
And this is how the other swift file with the Struct looks like:
struct hourlyData {
///declaring only keys that have Integers as value.
var daylyPop0 : Int
var daylyPop1 : Int
var daylyPop2 : Int
var daylyPop3 : Int
var daylyPop4 : Int
var summaryNowDay : String
var summaryNowNight : String
var iconNow : String
var currentTime: String?
//Initializing the values here. With optional properties:
init(weatherDictionary:NSDictionary){
daylyPop0 = weatherDictionary["hourly10_pop"] as! Int
daylyPop1 = weatherDictionary["hourly11_pop"] as! Int
daylyPop2 = weatherDictionary["hourly12_pop"] as! Int
daylyPop3 = weatherDictionary["hourly13_pop"] as! Int
daylyPop4 = weatherDictionary["hourly14_pop"] as! Int
summaryNowDay = weatherDictionary["today_day_fcttext_metric"] as! String
summaryNowNight = weatherDictionary["today_night_fcttext_metric"] as! String
iconNow = weatherDictionary["current_icon"] as! String
let currentTimeIntValue = weatherDictionary["forecast_time"] as! Int
currentTime = dateStringFromUnixTime(currentTimeIntValue)
}
//Converting unixTime to a desired style:::used ShortStyle in this case:
func dateStringFromUnixTime(unixTime: Int) -> String{
let timeInSeconds = NSTimeInterval(unixTime)
let weatherDate = NSDate(timeIntervalSince1970: timeInSeconds)
let dateFormatter = NSDateFormatter()
dateFormatter.timeStyle = .ShortStyle
return dateFormatter.stringFromDate(weatherDate)
}
}
Now the code looks fine, and it does not show any error, besides a warning under the 'if let', which says: Conditional cast from 'NSDictionary' to 'NSDictionary' always succeeds.
When I run the simulator, it crashes and displays: fatal error: unexpectedly found nil while unwrapping an Optional value. Highlighting, in green, the code line: daylyPop0 = weatherDictionary["hourly10_pop"] as! Int

Related

Exception thrown if no data are received

I am using following Class to receive data from an external database:
import Foundation
protocol HomeModelProtocal: class {
func itemsDownloaded(items: NSArray)
}
class HomeModel: NSObject, NSURLSessionDataDelegate {
//properties
weak var delegate: HomeModelProtocal!
var data : NSMutableData = NSMutableData()
var mi_movil: String = ""
let misDatos:NSUserDefaults = NSUserDefaults.standardUserDefaults()
var urlPath: String = "http:...hidden here.."
let parametros = "?id="
func downloadItems() {
mi_movil = misDatos.stringForKey("ID_IPHONE")!
print ("mi_movil en HOMEMODEL:",mi_movil)
urlPath = urlPath + parametros + mi_movil
let url: NSURL = NSURL(string: urlPath)!
var session: NSURLSession!
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
print ("LA URL ES: ",url)
session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session.dataTaskWithURL(url)
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.data.appendData(data);
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
func parseJSON() {
var jsonResult: NSMutableArray = NSMutableArray()
do{
jsonResult = try NSJSONSerialization.JSONObjectWithData(self.data, options:NSJSONReadingOptions.AllowFragments) as! NSMutableArray
} catch let error as NSError {
print(error)
}
var jsonElement: NSDictionary = NSDictionary()
let locations: NSMutableArray = NSMutableArray()
for(var i = 0; i < jsonResult.count; i++)
{
jsonElement = jsonResult[i] as! NSDictionary
print (jsonElement)
let location = MiAutoModel()
//the following insures none of the JsonElement values are nil through optional binding
if let id_mis_autos = jsonElement["id_mis_autos"] as? String,
let modelo = jsonElement["modelo"] as? String,
let ano = jsonElement["ano"] as? String,
let id_movil = jsonElement["id_movil"] as? String
{
location.id_mis_autos = id_mis_autos
location.modelo = modelo
location.ano = ano
location.id_movil = id_movil
}
locations.addObject(location)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.delegate.itemsDownloaded(locations)
})
}
}
If there are received data, it works fine but if there are no data an exception is thrown:
Could not cast value of type '__NSArray0' (0x1a0dd2978) to 'NSMutableArray' (0x1a0dd3490)
What should I change to detect if there are no data to avoid the exception?
Since you don't seem to be modifying jsonResult anywhere, the obvious choice is to make it an NSArray instead of an NSMutableArray, and change the downcasting to match that.
I'm not sure why you're using NSDictionary and NSMutableArray but this is how I would do it:
for result in jsonResult {
guard let jsonElement = result as? [String:AnyObject] else { return }
let locations: [MiAutoModel] = []
let location = MiAutoModel()
//the following insures none of the JsonElement values are nil through optional binding
let id_mis_autos = jsonElement["id_mis_autos"] as? String ?? ""
let modelo = jsonElement["modelo"] as? String ?? ""
let ano = jsonElement["ano"] as? String ?? ""
let id_movil = jsonElement["id_movil"] as? String ?? ""
location.id_mis_autos = id_mis_autos
location.modelo = modelo
location.ano = ano
location.id_movil = id_movil
locations.append(location)
}
You might have to change some of the code depending on your situation.

Need to return array from a function inside my class

I have a UIPickerView I want to fill with an array of values. The array of values is coming from a function inside of one of my classes (using json to grab the values and then put them into an array). The data is being grabbed successfully, and added to an array inside the function, but it's not returning for some reason.
Here's my class:
class Supplier {
var supplierId: Int
var supplierName: String
init(id: Int, name: String){
supplierId = id
supplierName = name
}
static func arrayOfSupplierNames() -> [String] {
let urlString = Constants.Urls.Suppliers.List;
let session = NSURLSession.sharedSession();
let url = NSURL(string: urlString)!;
var suppliers: Array<String> = []
session.dataTaskWithURL(url) { (data: NSData?, response:NSURLResponse?, error: NSError?) -> Void in
if let responseData = data {
do {
let json = try NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions.AllowFragments) as! Dictionary<String, AnyObject>;
if let suppliersDictionary = json["suppliers"] as? [Dictionary<String, AnyObject>] {
for aSupplier in suppliersDictionary {
if let id = aSupplier["id"] as? Int, let name = aSupplier["supplierName"] as? String {
let supplier = Supplier(id: id, name: name)
suppliers.append(supplier.supplierName)
}
}
}
}catch {
print("Could not serialize");
}
}
}.resume()
return suppliers
}
}
This seems to work because when I debug I can see the values being added to the array. I have another function in my ViewController that runs this function and adds it to a local array but the array returned from the function doesn't seem to get added to the array in the view controller:
func populateSuppliersArray() {
let sup:Array = Supplier.arrayOfSupplierNames()
for s in sup {
supplierArray.append(s) //supplierArray is at the top scope of view controller.
}
}
I even made the class function static so I wouldn't have to initialize the class just to use the function. I'm not sure this is the correct way. When I look at the sup variable while debugging it has zero values.
the json data is received inside an asynchronous block. your function returns as soon as you call resume on dataTaskWithURL. you should pass a completion block as an argument to your arrayOfSupplierNames and pass the array to that completion block instead. You could modify your function like this:
// take a completion block as an argument
func arrayOfSupplierNames(completion: (([String]) -> Void)) -> Void {
let urlString = Constants.Urls.Suppliers.List;
let session = NSURLSession.sharedSession();
let url = NSURL(string: urlString)!;
var suppliers: Array<String> = []
session.dataTaskWithURL(url) { (data: NSData?, response:NSURLResponse?, error: NSError?) -> Void in
if let responseData = data {
do {
let json = try NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions.AllowFragments) as! Dictionary<String, AnyObject>;
if let suppliersDictionary = json["suppliers"] as? [Dictionary<String, AnyObject>] {
for aSupplier in suppliersDictionary {
if let id = aSupplier["id"] as? Int, let name = aSupplier["supplierName"] as? String {
let supplier = Supplier(id: id, name: name)
suppliers.append(supplier.supplierName)
}
}
completion(suppliers) // pass the array to completion block
}
}catch {
print("Could not serialize");
}
}
}.resume()
}
You'll call it like this:
Supplier.arrayOfSupplierNames { (suppliers) -> Void in
// use suppliers as appropriate
}
please note that completion block is called asynchronously (some time in future).

Update TableView from Asynchronous Data in Swift

I'm currently working on an iOS Application and I recently came across the issue that I was downloading and parsing data synchronously and as a result making the app completely unresponsive.
I've updated my code to download my data and parse it asynchronously however I'm now hitting another issue.
I've been trying to split my code up and follow the MVC pattern. I have my TableViewController as the delegate and DataSource of the TableView. I have another class that is managing downloading+parsing my data.
My issue is, once the data is parsed - how do I update the TableView since it's all being done asynchronously.
TableViewController
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
println(searchBar.text)
searchBar.resignFirstResponder() //Hide the keyboard
let result = ShowsDataParser.newSearchShows(searchBar.text) //Get search result based on input
searchResults = result.results
maxPages = result.maxPages
currentPage = 1
self.tableView.reloadData()
}
I've implemented all required methods(e.g cellForRowAtIndexPath) within this class too.
ShowsDataParser
class func newSearchShows(input: String, page:Int = 1) -> (results:[Show], maxPages: Int)
{
let searchUrl = "/search/tv" //Search URL
var searchResults = [Show]()
var totalPages = 1
let posterSize = "w92"
if let escapedSearch = escapeString(input) {
var search = apiLocation + searchUrl + "?query=" + escapedSearch + "&api_key=" + APIKey + "&page=\(page)"
if let url = NSURL(string: search)
{
let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler:{data,response,error -> Void in
if let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as? NSDictionary
{
var name: String
var id: Int
var status: String
var overview = "meow"
if let results = json["results"] as? NSArray
{
for result in results
{
id = result["id"] as! Int
name = result["original_name"] as! String
//overview = result["overview"] as! String
status = ""
var newShow = Show(showId: id, showName: name, showDesc: overview, showStatus: status)
if let date = result["first_air_date"] as? String
{
newShow.firstAired = self.formatDate(date)
}
if let poster = result["poster_path"] as? String
{
if let baseUrl = AppDelegate.config?.baseUrl
{
if let data = NSData(contentsOfURL: NSURL(string: baseUrl + posterSize + poster)!)
{
//newShow.posterImage = UIImage(data: data)
}
}
}
searchResults.append(newShow)
}
}
if let pages = json["total_pages"] as? Int
{
totalPages = pages
}
}
})
}
}
return (results: searchResults, maxPages: totalPages)
}
As you can see, the above class gets and parses the data from the specified url. I'm simply unsure of how to update the tableview after this is done. Calling upon self.tableView.reloadData() within searchBarSearchButtonClicked isn't working.
place the reloadData() call after the if statement
if let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as? NSDictionary
{
.... more code
if let pages = json["total_pages"] as? Int
{
totalPages = pages
}
}
self.myTableView.reloadData()

Swift - append to array that is in an array

I am writing a class that returns JSON data. I create an array of the type NSArray. To this Array I add arrays of the type String.
I later want to append strings to the array of type String that is within the array of type NSArray.
However it does not allow this, I have no idea why because the append function does work when appending NSArrays to the main array.
This is the entire class:
import Foundation
class JSON {
struct Static {
let hostname = "http://192.168.2.115:8888/tcn/";
func getJSON(script:String, jsonMainElement:String, elements:[String]) -> [NSArray] {
var returnObject = [NSArray]()
let urlAsString = hostname + script;
let url = NSURL(string: urlAsString)!
let urlSession = NSURLSession.sharedSession()
let jsonQuery = urlSession.dataTaskWithURL(url, completionHandler: { data, response, error -> Void in
if (error != nil) {
println(error.localizedDescription);
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary
if (err != nil) {
println("JSON Error \(err!.localizedDescription)");
}
var i = 0;
let arrayFromJson: [[String:String]] = jsonResult[jsonMainElement] as [[String:String]]
dispatch_async(dispatch_get_main_queue(), {
for item in arrayFromJson {
returnObject.append([String]());
for jsonObject in elements {
returnObject[i].append(item[jsonObject]!);
// ^ THIS LINE GIVES ERROR
}
i++;
}
if (arrayFromJson.count > 0) {
//k
}
})
})
jsonQuery.resume()
return returnObject;
}
}
}
You may need to define the array as containing a mutable array as below.
var returnObject = [NSMutableArray]()
You could also use a Swift 'sub' array, and define it more like this.
var returnObject = Array<Array<String>>()
Don't forget to update your return on the method.
func getJSON() -> Array<Array<String>>

Setting class variable values inside functions but are returning nil when called in any other class/functon

EDIT: Answer is in a below comment, I had to use a completionHandler in order for the asynchronous call to happen. Big thanks to Gasim for the help.
In a class called NetworkManager that has a sharedInstance struct which allows me to call the functions within other classes I am declaring public var urlArray = [] which is an empty Array not part of any function.
In a function called fetchLatestPosts I am parsing JSON data and downloading some relevant URL's that are returned in an Array like so var photoURLArray: NSArray = responseDictionary.valueForKeyPath("data.images.standard_resolution.url") as NSArray and then setting the self.urlArray = photoURLArray in attempt for it to allow me to set the value of photoURLArray outside of the function.
The issue lies in the fact when I am trying to set the value of NetworkManager.sharedInstance.urlArray to a function within a different method the results return nil.
Is this because xCode thinks that I am calling the variable with the value before I set it to the value of photoURLArray in my fetchLatestPosts function? Is there a way for me to set the value of the array that is called within the function in a way that allows me to access it's values within other classes?
The reason I am asking this is because I do not want to do the JSON parsing in my tableViewCell class because otherwise I would be making heavy calls on the Instagram API every time a new cell appears. I have been stuck on this issue for a day or so and would appreciate if anyone could give me a work-around to this issue!
public func fetchLatestPosts(tag: String) {
var userDefaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
var accessTokenQuery: AnyObject? = userDefaults.objectForKey("accessToken")
if (accessTokenQuery == nil)
{
// Logging user in to IG
SimpleAuth.authorize("instagram", completion: { (responseObject: AnyObject!, error: NSError!) -> Void in
var IGDictionary: NSDictionary = responseObject as NSDictionary
let credentials: AnyObject! = IGDictionary["credentials"]
let accessToken: AnyObject! = credentials["token"]
println("Access token: \(accessToken)")
userDefaults.setObject(accessToken, forKey: "accessToken")
userDefaults.synchronize()
})
} else {
println("User is already logged in")
// Load some posts
var session: NSURLSession = NSURLSession.sharedSession()
var accessToken: NSString = userDefaults.objectForKey("accessToken") as NSString
var urlString: NSString = NSString(format: "https://api.instagram.com/v1/tags/\(tag)/media/recent?access_token=%#", accessToken)
// println(urlString)
var url = NSURL(string: urlString)!
var request: NSURLRequest = NSURLRequest(URL: url)
var task: NSURLSessionDownloadTask = session.downloadTaskWithRequest(request, completionHandler: { (location: NSURL!, response: NSURLResponse!, error: NSError!) -> Void in
// Sorting JSON into dictionary
var data: NSData = NSData(contentsOfURL: location)!
var responseDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as [String: AnyObject]!
// Getting photo URLs
var photos: NSArray = responseDictionary.valueForKeyPath("data.images.standard_resolution.url") as NSArray
// println("photos: \(photos)")
self.IGDictionary = responseDictionary
self.photoItems = photos
// println("photoItems: \(self.photoItems)")
})
task.resume()
}
}
This is how I like to do it as I like to keep the request part separate from what I really want to do, once it is completed:
public func fetchLatestPosts(tag: String, successHandler: (NSDictionary, NSArray) -> Void) {
var userDefaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
var accessTokenQuery: AnyObject? = userDefaults.objectForKey("accessToken")
if (accessTokenQuery == nil)
// ...
} else {
// ...
var task: NSURLSessionDownloadTask = session.downloadTaskWithRequest(request, completionHandler: { (location: NSURL!, response: NSURLResponse!, error: NSError!) -> Void in
var data: NSData = NSData(contentsOfURL: location)!
var responseDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as [String: AnyObject]!
var photos: NSArray = responseDictionary.valueForKeyPath("data.images.standard_resolution.url") as NSArray
successHandler(responseDictionary, photos);
})
task.resume()
}
}
and example usage:
override func viewDidLoad() {
super.viewDidLoad();
self.fetchLatestPosts("SomeTag", successHandler: {
(responseDictionary, photos) in
self.photos = photos;
self.IGDictionary = responseDictionary;
// reload collection view data or do whatever you need
self.collectionView.reloadData();
});
}
Also, I would recommend checking out SwiftyJSON. You will fall in love with it the moment you use it.

Resources